Вступление
Знаю, что на эту тему есть очень много статей и своего рода туториоалов, я уже и не говорю об официальной документации, но при работе над своим последним проектом я столкнулся с очень занятной проблемой, о которой мало где говорится. Речь сегодня пойдет о проблеме использования 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