Вступление
Знаю, что на эту тему есть очень много статей и своего рода туториоалов, я уже и не говорю об официальной документации, но при работе над своим последним проектом я столкнулся с очень занятной проблемой, о которой мало где говорится. Речь сегодня пойдет о проблеме использования Dependency Injection и Quartz в проекте на платформе ASP.NET Core.
Началось всё с того, что я не думал, что могут возникнуть какие-то проблемы и скажу сразу, что пробовал использовать различные подходы: добавлял все классы, которые включал в себя Quartz в services и юзать их через DI — мимо (но не полностью, как потом оказалось), пробовал добавить HostedService — тоже не работало (в конце прикреплю несколько хороших ссылок на полезные статьи о работе с Quartz) и так далее. Я уже думал, что у меня проблема с триггером — тоже нет. В этой короткой статье я попытаюсь помочь тем, у кого, возможно, была такая же проблема и надеюсь мое решение поможет им в дальнейшей работе. Под конец вступления хочу добавить, что буду весьма признателен если в комментариях те, кто хорошо знаком с технологией, дадут несколько советов, которые помогут улучшить то, что я предложил.
Quartz
Создадим проект (или возьмём готовый — неважно) и добавим в него две папки и несколько классов:
Quartz
--DataJob.cs
--DataScheduler.cs
--JobFactory.cs
Workers
--EmailSender
--IEmailSender
В интерфейсе IEmailSender, который будет служить примером, создадим один метод для отправки писем на почту:
public interface IEmailSender { Task SendEmailAsync(string email, string subject, string message); }
Теперь опишем класс, который будет реализовывать этот интерфейс:
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); } }
Теперь опишем классы DataJob.cs, DataScheduler.cs, JobFactory.cs. Класс DataJob будет реализовывать интерфейс IJob.
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") } } }
Как видим у нас поле типа IServiceScopeFactory, отдда мы будем доставать сервисы напрямую из Startup. Именно этот подход помог решить мне мою проблему, идём далее и опишем клас DataScheduler в котором будем в Sheduler самого кварца добавлять job и trigger:
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); }
И теперь клас JobFactory, который реализовывает интерфейс IJobFactory:
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 } }
Как видим, я, фактически, все зависимости получаю сразу напрямую из serviceScopeFactory. Всё почти готово, осталось изменить класс Program:
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(); }
И добавить в Startup в метод ConfigureServices следующее:
services.AddTransient<JobFactory>(); services.AddScoped<DataJob>(); services.AddScoped<IEmailSender,EmailSender>();
Готово. Теперь при запуске приложение мы создаем задачу, которая будет срабатывать каждую минуту. Значение можно поменять в DataScheduler.Start (также можно указывать в секундах, часах или использовать CRON). Для каждой новой задачи при таком подходе нужно создавать новый клас, который будет реализовывать IJob и прописывать новую задачу DataScheduler. Также можно и создавать отдельный Scheduler клас для новой задачи.
Буду очень рад, если смог кому-то помочь, а вот пару полезных статей о Quartz и его использовании:
Creating a Quartz.NET hosted service with ASP.NET Core
Using scoped services inside a Quartz.NET hosted service with ASP.NET Core
