Quartz в ASP.NET Core

    Вступление


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

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 7

      0
      Я ользовался такой реализацией, но суть таже
      Spoiler header
      public class MicrosoftDependencyInjectionJobFactory : IJobFactory
      	{
      		private static readonly ILogger Logger = LogManager.GetCurrentClassLogger();
      		private readonly IServiceProvider _container;
      
      		/// <summary>
      		/// 	.ctor
      		/// </summary>
      		/// <param name="container"></param>
      		public MicrosoftDependencyInjectionJobFactory(IServiceProvider container)
      		{
      			_container = container;
      		}
      
      		/// <inheritdoc />
      		public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
      		{
      			// Отлавливаем ошибки получения сервиса из DI 
      			try
      			{
      				var job = _container.GetService(bundle.JobDetail.JobType) as IJob;
      				if (job != null)
      				{
      					return job;
      				}
      				else
      				{
      					var exception = new Exception($"Can't cast {bundle.JobDetail.JobType} to {typeof(IJob)}");
      					Logger.Error(exception);
      					throw exception;
      				}
      			}
      			catch (Exception exception)
      			{
      				Logger.Error(exception, $"Can't get {bundle.JobDetail.JobType} through DI");
      				throw;
      			}
      		}
      
      		/// <inheritdoc />
      		public void ReturnJob(IJob job)
      		{
      			var disposable = job as IDisposable;
      			disposable?.Dispose();
      		}
      	}
      

        0

        Да, спасибо, видел что-то подобное!

        +2
        Вы неправильно используете IServiceScope — вы его уничтожаете сразу после создания, не дождавшись выполнения задачи. А используя ActivatorUtilities из Microsoft.Extensions.DependencyInjection можно избежать регистрации самих задач в контейнере.
        Код
        public class JobFactory : IJobFactory
        {
            private readonly IServiceProvider _provider;
        
            public JobFactory(IServiceProvider provider)
            {
                _provider = provider;
            }
        
            public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
            {
                return new JobWrapper(_provider, bundle.JobDetail.JobType);
            }
        
            public void ReturnJob(IJob job)
            {
                (job as IDisposable)?.Dispose();
            }
        }
        
        public class JobWrapper : IJob, IDisposable
        {
            private readonly IServiceScope _serviceScope;
            private readonly IJob _job;
        
            public JobWrapper(IServiceProvider serviceProvider, Type jobType)
            {
                _serviceScope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope();
                _job = ActivatorUtilities.CreateInstance(_serviceScope.ServiceProvider, jobType) as IJob;
            }
        
            public Task Execute(IJobExecutionContext context)
            {
                return _job.Execute(context);
            }
        
            public void Dispose()
            {
                _serviceScope.Dispose();
            }
        }
        
        public class DataJob : IJob
        {
            private readonly IEmailSender _emailSender;
        
            public DataJob(IEmailSender emailSender)
            {
                _emailSender = emailSender;
            }
        
            public async Task Execute(IJobExecutionContext context)
            {
                await _emailsender.SendEmailAsync("example@gmail.com", "example", "hello");
            }
        }
        

          –1
          Хотите сказать, что может неправильно выполняться (просто у меня никаких проблем не возникало и выполнялись задачи даже после удаления IServiceScope)
            +2
            Это потому что вы не используете IDisposable ресурсов, которые кидают ObjectDisposedException (например: HttpClient).
          0
          Нет ли ничего плохого в том что у вас host.Run() запускается после уничтожения scope?
            –1
            На часом деле, комментатор выше тоже указал на это, но я не видел чтобы это влияло на работу, так-как, полагаю, задачи уже «забиты» в планировщик и выполнялись с указанным интервалом.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое