在个人上一篇文章中,我展现了如何使用ASP.NET Core建立Quartz.NET托管服务并使用它来按计划运行后台任务。不幸的是,因为Quartz.NET API的工做方式,在Quartz做业中使用Scoped依赖项注入服务有些麻烦。说明下这篇文章部分采用机翻。html
做者:依乐祝git
译文地址:http://www.javashuo.com/article/p-oulvehus-eq.htmlgithub
原文地址:https://andrewlock.net/using-scoped-services-inside-a-quartz-net-hosted-service-with-asp-net-core/数据库
在这篇文章中,我将展现一种简化工做中使用Scoped服务的方法。您可使用相同的方法来管理EF Core的工做单元模式和其余面向切面的模型。c#
这篇文章是上篇文章引伸出来的,所以,若是您尚未阅读的话,建议您先阅读上篇文章。安全
在上篇博客的最后,咱们有一个实现了IJob
接口并向控制台简单输出信息的HelloWorldJob
。async
public class HelloWorldJob : IJob { private readonly ILogger<HelloWorldJob> _logger; public HelloWorldJob(ILogger<HelloWorldJob> logger) { _logger = logger; } public Task Execute(IJobExecutionContext context) { _logger.LogInformation("Hello world!"); return Task.CompletedTask; } }
咱们还有一个IJobFactory
的实现,以便咱们在须要时从DI容器中检索做业的实例:ide
public class SingletonJobFactory : IJobFactory { private readonly IServiceProvider _serviceProvider; public SingletonJobFactory(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) { return _serviceProvider.GetRequiredService(bundle.JobDetail.JobType) as IJob; } public void ReturnJob(IJob job) { } }
这些服务都在Startup.ConfigureServices()
中以单例形式注册:函数
services.AddSingleton<IJobFactory, SingletonJobFactory>(); services.AddSingleton<HelloWorldJob>();
对于这个很是基本的示例来讲,这很好,可是若是您须要在IJob
内部使用一些范围服务呢?例如,也许您须要使用EF Core DbContext
遍历全部客户,并向他们发送电子邮件,并更新客户记录。咱们假设这个任务为EmailReminderJob
。fetch
我在上一篇文章中展现的解决方案是将IServiceProvider
注入到您的IJob
的文档中,手动建立一个范围,并从中检索必要的服务。例如:
public class EmailReminderJob : IJob { private readonly IServiceProvider _provider; public EmailReminderJob( IServiceProvider provider) { _provider = provider; } public Task Execute(IJobExecutionContext context) { using(var scope = _provider.CreateScope()) { var dbContext = scope.ServiceProvider.GetService<AppDbContext>(); var emailSender = scope.ServiceProvider.GetService<IEmailSender>(); // fetch customers, send email, update DB } return Task.CompletedTask; } }
在许多状况下,这种方法绝对能够。若是不是将实现直接放在工做内部(如我上面所作的那样),而是使用中介者模式来处理诸如工做单元或消息分发之类的跨领域问题,则尤为如此。
若是不是这种状况,您可能会受益于建立一个能够为您管理这些工做的帮助类。
QuartzJobRunner
要解决这些问题,您能够建立一个IJob
的“中间” 实现,这里咱们命名为QuartzJobRunner
,该实现位于IJobFactory
和要运行的IJob
之间。我将很快介绍做业实现,可是首先让咱们更新现有的IJobFactory
实现以不管请求哪一个做业,始终返回QuartzJobRunner
的实例,:
using Microsoft.Extensions.DependencyInjection; using Quartz; using Quartz.Spi; using System; public class JobFactory : IJobFactory { private readonly IServiceProvider _serviceProvider; public JobFactory(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) { return _serviceProvider.GetRequiredService<QuartzJobRunner>(); } public void ReturnJob(IJob job) { } }
如您所见,该NewJob()
方法始终返回QuartzJobRunner
的实例。咱们将在Startup.ConfigureServices()
中将QuartzJobRunner
注册为单例模式,所以咱们没必要担忧它没有被明确释放。
services.AddSingleton<QuartzJobRunner>();
咱们将在QuartzJobRunner
中建立实际所需的IJob
实例。QuartzJobRunner
中的job会建立范围,实例化IJob
的请求并执行它:
using Microsoft.Extensions.DependencyInjection; using Quartz; using System; using System.Threading.Tasks; public class QuartzJobRunner : IJob { private readonly IServiceProvider _serviceProvider; public QuartzJobRunner(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public async Task Execute(IJobExecutionContext context) { using (var scope = _serviceProvider.CreateScope()) { var jobType = context.JobDetail.JobType; var job = scope.ServiceProvider.GetRequiredService(jobType) as IJob; await job.Execute(context); } } }
在这一点上,您可能想知道,经过添加这个额外的间接层,咱们得到了什么好处?主要有如下两个主要优势:
EmailReminderJob
注册为范围服务,并直接将任何依赖项注入其构造函数中QuartzJobRunner
类中。因为做业实例是从IServiceProvder
做用域中解析来的,所以您能够在做业实现的构造函数中安全地使用做用域服务。这使的EmailReminderJob
的实现更加清晰,并遵循构造函数注入的典型模式。若是您不熟悉DI范围界定问题,则可能很难理解它们,所以任何对您不利的事情在我看来都是一个好主意:
[DisallowConcurrentExecution] public class EmailReminderJob : IJob { private readonly AppDbContext _dbContext; private readonly IEmailSender _emailSender; public EmailReminderJob(AppDbContext dbContext, IEmailSender emailSender) { _dbContext = dbContext; _emailSender = emailSender; } public Task Execute(IJobExecutionContext context) { // fetch customers, send email, update DB return Task.CompletedTask; } }
这些IJob
的实现可使用如下任何生存期(做用域或瞬态)来在Startup.ConfigureServices()
中注册(JobSchedule
仍然能够是单例):
services.AddScoped<EmailReminderJob>(); services.AddSingleton(new JobSchedule( jobType: typeof(EmailReminderJob), cronExpression: "0 0 12 * * ?")); // every day at noon
QuartzJobRunner
处理正在执行的IJob
的整个生命周期:它从容器中获取,执行并释放它(在释放范围时)。所以,它很适合处理其余跨领域问题。
例如,假设您有一个须要更新数据库并将事件发送到消息总线的服务。您能够在每一个单独的IJob
实现中处理全部这些问题,也能够将跨领域的“提交更改”和“调度消息”操做移到QuartzJobRunner
中。
这个例子显然是很是基础的。若是这里的代码适合您,我建议您观看吉米·博加德(Jimmy Bogard)的“六小段失败线”演讲,其中描述了一些问题!
public class QuartzJobRunner : IJob { private readonly IServiceProvider _serviceProvider; public QuartzJobRunner(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public async Task Execute(IJobExecutionContext context) { using (var scope = _serviceProvider.CreateScope()) { var jobType = context.JobDetail.JobType; var job = scope.ServiceProvider.GetRequiredService(jobType) as IJob; var dbContext = _serviceProvider.GetRequiredService<AppDbContext>(); var messageBus = _serviceProvider.GetRequiredService<IBus>(); await job.Execute(context); // job completed, save dbContext changes await dbContext.SaveChangesAsync(); // db transaction succeeded, send messages await messageBus.DispatchAsync(); } } }
这里的QuartzJobRunner
实现与上一个很是类似,可是在执行的咱们请求的IJob
以前,咱们从DI容器中解析了DbContext
和消息总线服务。看成业成功执行后(即未抛出异常),咱们将全部未提交的更改保存在中DbContext
,并在消息总线上调度事件。
将这些方法移到QuartzJobRunner
中应该能够减小IJob实现中的重复代码,而且能够更容易地移到更正式的管道和其余模式(若是您但愿之后这样作的话)。
我喜欢本文中显示的方法(使用中间QuartzJobRunner
类),主要有两个缘由:
IJob
实现不须要任何有关建立做用域的基础结构的知识,只需完成标准构造函数注入便可IJobFactory
中不须要作作任何特殊处理工做。该QuartzJobRunner
经过建立和处理做用域隐式地处理这个问题。可是,此处显示的方法并非在工做中使用范围服务的惟一方法。马修·阿伯特(Matthew Abbot) 在这个文章中演示了一种方法,该方法旨在以正确处理运行后的做业的方式实现IJobFactory。它有点笨拙,由于你必须匹配接口API,但能够说它更接近你应该实现它的方式!我我的认为我会坚持使用这种QuartzJobRunner
方法,可是你能够选择最适合您的方法🙂
在本文中,我展现了如何建立中间层IJob
,该中间层QuartzJobRunner
在调度程序须要执行做业时建立。该运行程序负责建立一个DI范围,实例化请求的做业并执行它,所以最终IJob
实现能够在其构造函数中使用做用域中的服务。您也可使用此方法在QuartzJobRunner
中配置基本管道,尽管对此有更好的解决方案,例如装饰器或MediatR库中的行为。