在我的 "上一篇文章中" ,我展示瞭如何使用ASP.NET Core創建Quartz.NET托管服務並使用它來按計劃運行後臺任務。不幸的是,由於Quartz.NET API的工作方式,在Quartz作業中使用Scoped依賴項註入服務有些麻煩。說明下這篇文章部分採用機翻。 作者:依樂祝 譯文地址:h ...
在我的上一篇文章中,我展示瞭如何使用ASP.NET Core創建Quartz.NET托管服務並使用它來按計劃運行後臺任務。不幸的是,由於Quartz.NET API的工作方式,在Quartz作業中使用Scoped依賴項註入服務有些麻煩。說明下這篇文章部分採用機翻。
作者:依樂祝
譯文地址:https://www.cnblogs.com/yilezhu/p/12757411.html
原文地址:https://andrewlock.net/using-scoped-services-inside-a-quartz-net-hosted-service-with-asp-net-core/
在這篇文章中,我將展示一種簡化工作中使用Scoped服務的方法。您可以使用相同的方法來管理EF Core的工作單元模式和其他面向切麵的模型。
這篇文章是上篇文章引申出來的,因此,如果您還沒有閱讀的話,建議您先閱讀上篇文章。
回顧-自定義JobFactory和單例的IJob
在上篇博客的最後,我們有一個實現了IJob
介面並向控制台簡單輸出信息的HelloWorldJob
。
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容器中檢索作業的實例:
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
。
權宜之計
我在上一篇文章中展示的解決方案是將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可以處理橫切關註點
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
方法,但是你可以選擇最適合您的方法