Quartz in ASP.NET Core

Introduction


I know that there are a lot of articles and some kind of tutorials on this subject, I'm not even talking about official documentation, but when working on my latest project I came across a very interesting problem, which is not mentioned very much. Today we will talk about the problem of using Dependency Injection and Quartz in an ASP.NET Core platform project.

It all started with the fact that I did not think that any problems could arise and I would say right away that I tried to use various approaches: I added all the classes that Quartz included in services and used them through DI - by (but not completely, as it turned out later), I tried to add HostedService - it didn’t work either (in the end I’ll attach some good links to useful articles about working with Quartz) and so on. I already thought that I had a problem with the trigger - neither. In this short article I will try to help those who may have had the same problem and hope that my solution will help them in their future work. At the end of the introduction, I want to add that I will be very grateful if in the comments those who are well acquainted with the technology give some advice that will help improve what I proposed.

Quartz


Create a project (or take a finished one - it doesn't matter) and add two folders and several classes to it:

Quartz
--DataJob.cs
--DataScheduler.cs
--JobFactory.cs
Workers
--EmailSender
--IEmailSender

In the IEmailSender interface, which will serve as an example , create one method for sending letters to mail:

public interface IEmailSender
    {
        Task SendEmailAsync(string email, string subject, string message);
    }

Now we describe the class that will implement this interface:

public class EmailSender : IEmailSender
    {
        
        public Task SendEmailAsync(string email, string subject, string message)
		{
			var from = "****@gmail.com";
			var pass = "****";
            SmtpClient client = new SmtpClient("smtp.gmail.com", 587);
			client.DeliveryMethod = SmtpDeliveryMethod.Network;
			client.UseDefaultCredentials = false;
			client.Credentials = new System.Net.NetworkCredential(from, pass);
			client.EnableSsl = true;
			var mail = new MailMessage(from, email);
			mail.Subject = subject;
			mail.Body = message;
			mail.IsBodyHtml = true;
			return client.SendMailAsync(mail);
		}
    }

Now we describe the classes DataJob.cs, DataScheduler.cs, JobFactory.cs. The DataJob class will implement the IJob interface.

 public class DataJob : IJob
    {
        private readonly IServiceScopeFactory serviceScopeFactory;

        public DataJob(IServiceScopeFactory serviceScopeFactory)
        {
            this.serviceScopeFactory = serviceScopeFactory;
        }

        public async Task Execute(IJobExecutionContext context)
        {
            using (var scope = serviceScopeFactory.CreateScope())
            {
                var emailsender = scope.ServiceProvider.GetService<IEmailSender>();
                
                 await emailsender.SendEmailAsync("example@gmail.com","example","hello")
            }
        }
    }

As we see a field of type IServiceScopeFactory, we will get services directly from Startup. It was this approach that helped me solve my problem, go ahead and describe the DataScheduler class in which we will add job and trigger in the Sheduler of the quartz itself:

public static class DataScheduler
    {
        
        public static async void Start(IServiceProvider serviceProvider)
        {
            IScheduler scheduler = await StdSchedulerFactory.GetDefaultScheduler();
            scheduler.JobFactory = serviceProvider.GetService<JobFactory>();
            await scheduler.Start();

            IJobDetail jobDetail = JobBuilder.Create<DataJob>().Build();
            ITrigger trigger = TriggerBuilder.Create()
                .WithIdentity("MailingTrigger", "default")
                .StartNow()
                .WithSimpleSchedule(x => x
                .WithIntervalInMinutes(1)
                .RepeatForever())
                .Build();

            await scheduler.ScheduleJob(jobDetail, trigger);
        }

And now the JobFactory class, which implements the IJobFactory interface:

 public class JobFactory : IJobFactory
    {
        protected readonly IServiceScopeFactory serviceScopeFactory;

        
        public JobFactory(IServiceScopeFactory serviceScopeFactory)
        {
            this.serviceScopeFactory = serviceScopeFactory;
        }

        public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
        {
            using (var scope = serviceScopeFactory.CreateScope())
            {
                var job = scope.ServiceProvider.GetService(bundle.JobDetail.JobType) as IJob;
                return job;
            }
            
        }

        public void ReturnJob(IJob job)
        {
           //Do something if need
        }
    }

As you can see, I, in fact, get all the dependencies directly from serviceScopeFactory. Everything is almost ready, it remains to change the Program class:

public class Program
    {
        public static void Main(string[] args)
        {
            var host = BuildWebHost(args);
            using (var scope = host.Services.CreateScope())
            {
                var serviceProvider = scope.ServiceProvider;
                try
                {
                    DataScheduler.Start(serviceProvider);
                }
                catch (Exception)
                {
                    throw;
                }
            }
            host.Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
           WebHost.CreateDefaultBuilder(args)
               .UseStartup<Startup>()
               .Build();
    }

And add the following to Startup in the ConfigureServices method:

   services.AddTransient<JobFactory>();
   services.AddScoped<DataJob>();
   services.AddScoped<IEmailSender,EmailSender>();

Done. Now when you start the application, we create a task that will fire every minute. The value can be changed in DataScheduler.Start (can also be specified in seconds, hours or use CRON). For each new task with this approach, you need to create a new class that will implement IJob and register a new DataScheduler task. You can also create a separate Scheduler class for a new task.

I would be very happy if I could help someone, but here are a couple of useful articles about Quartz and its use:

Creating a Quartz.NET hosted service with ASP.NET Core
Using scoped services inside a Quartz.NET hosted service with ASP.NET Core

Source: https://habr.com/ru/post/undefined/


All Articles