在 ASP.NET Core和Worker Service中使用Quartz.Net
現在有了一個官方包Quartz.Extensions.Hosting實現使用Quartz.Net執行後臺任務,所以把Quartz.Net新增到ASP.NET Core或Worker Service要簡單得多。
我將展示如何把Quartz.Net HostedService新增到你的應用,如何建立一個簡單的IJob,以及如何註冊它與trigger。
簡介——什麼是Quartz.Net
Quartz.Net是一個功能齊全的開源作業排程系統,可以在最小規模的應用程式到大型企業系統使用。
有許多ASP.NET的釘子戶,他們以一種可靠的、叢集的方式在定時器上執行後臺任務。使用在ASP.NET Core中使用的Quartz.Net支援了.NET Standar 2.0,因此你可以輕鬆地在應用程式中使用它。
Quartz.Net有三個主要概念:
- job。這是你想要執行的後臺任務。
- trigger。trigger控制job何時執行,通常按某種排程規則觸發。
- scheduler。它負責協調job和trigger,根據trigger的要求執行job。
ASP.NET Core很好地支援通過hosted services(託管服務)執行“後臺任務”。當你的ASP.NET Core應用程式啟動,託管服務也啟動,並在應用程式的生命週期中在後臺執行。Quartz.Net 3.2.0通過Quartz.Extensions.Hosting引入了對該模式的直接支援。Quartz.Extensions.Hosting即可以用在ASP.NET Core應用程式,也可以用在基於“通用主機”的Worker Service。
雖然可以建立一個“定時”後臺服務(例如,每10分鐘執行一個任務),但Quartz.NET提供了一個更加健壯的解決方案。通過使用Crontrigger,你可以確保任務只在一天的特定時間(例如凌晨2:30)執行,或者只在特定的日子執行,或者這些時間的任意組合執行。Quartz.Net還允許你以叢集的方式執行應用程式的多個例項,以便在任何時候只有一個例項可以執行給定的任務。
Quartz.Net託管服務負責Quartz的排程。它將在應用程式的後臺執行,檢查正在執行的觸發器,並在必要時執行相關的作業。你需要配置排程程式,但不需要擔心啟動或停止它,IHostedService會為你管理。
在這篇文章中,我將展示建立Quartz.Net job的基礎知識。並將其排程到託管服務中的定時器上執行。
安裝Quartz.Net
Quartz.Net是一個.NET Standar 2.0的NuGet包,所以它很容易安裝在你的應用程式中。對於這個測試,我建立了一個Worker Service專案。你可以通過使用dotnetadd package Quartz.Extensions.Hosting命令安裝Quartz.Net託管包。如果你檢視專案的.csproj,它應該是這樣的:
<Project Sdk="Microsoft.NET.Sdk.Worker"> <PropertyGroup> <TargetFramework>net5.0</TargetFramework> <UserSecretsId>dotnet-QuartzWorkerService-9D4BFFBE-BE06-4490-AE8B-8AF1466778FD</UserSecretsId> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" /> <PackageReference Include="Quartz.Extensions.Hosting" Version="3.2.3" /> </ItemGroup> </Project>
這將新增託管服務包,從而引入Quartz.Net。接下來,我們需要在應用程式中註冊Quartz.Net的服務和 IHostedService。
新增Quartz.Net託管服務
註冊Quartz.Net需要做兩件事:
- 註冊Quartz.Net需要的DI容器服務。
- 註冊託管服務。
在ASP.NET Core中,通常會在Startup.ConfigureServices()方法中完成這兩項操作。WorkerServices不使用Startup類,所以我們在Program.cs中的IHostBuilder的ConfigureServices方法中註冊它們:
public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureServices((hostContext, services) => { // Add the required Quartz.NET services services.AddQuartz(q => { // Use a Scoped container to create jobs. I'll touch on this later q.UseMicrosoftDependencyInjectionScopedJobFactory(); }); // Add the Quartz.NET hosted service services.AddQuartzHostedService( q => q.WaitForJobsToComplete = true); // other config }); }
這裡有幾個有趣的點:
- UseMicrosoftDependencyInjectionScopedJobFactory:它告訴Quartz.NET註冊一個IJobFactory,該IJobFactory通過從DI容器中建立job。方法中的Scoped部分意味著你的作業可以使用scoped服務,而不僅僅是single或transient服務。
- WaitForJobsToComplete:此設定確保當請求關閉時,Quartz.NET在退出之前等待作業優雅地結束。
如果你現在執行應用程式,將看到Quartz服務啟動,並將大量日誌轉儲到控制檯:
info: Quartz.Core.SchedulerSignalerImpl[0] Initialized Scheduler Signaller of type: Quartz.Core.SchedulerSignalerImpl info: Quartz.Core.QuartzScheduler[0] Quartz Scheduler v.3.2.3.0 created. info: Quartz.Core.QuartzScheduler[0] JobFactory set to: Quartz.Simpl.MicrosoftDependencyInjectionJobFactory info: Quartz.Simpl.RAMJobStore[0] RAMJobStore initialized. info: Quartz.Core.QuartzScheduler[0] Scheduler meta-data: Quartz Scheduler (v3.2.3.0) 'QuartzScheduler' with instanceId 'NON_CLUSTERED' Scheduler class: 'Quartz.Core.QuartzScheduler' - running locally. NOT STARTED. Currently in standby mode. Number of jobs executed: 0 Using thread pool 'Quartz.Simpl.DefaultThreadPool' - with 10 threads. Using job-store 'Quartz.Simpl.RAMJobStore' - which does not support persistence. and is not clustered. info: Quartz.Impl.StdSchedulerFactory[0] Quartz scheduler 'QuartzScheduler' initialized info: Quartz.Impl.StdSchedulerFactory[0] Quartz scheduler version: 3.2.3.0 info: Quartz.Core.QuartzScheduler[0] Scheduler QuartzScheduler_$_NON_CLUSTERED started. info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down....
此時,你已經讓Quartz作為託管服務在你的應用程式中執行,但是沒有任何job讓它執行。在下一節中,我們將建立並註冊一個簡單的job。
建立job
對於我們正在排程的實際後臺工作,我們將使用一個"hello world"實現它寫入一個ILogger<T>。你應該實現Quartz.NET的介面IJob,它包含一個非同步的Execute()方法。注意,我們在這裡使用依賴注入將日誌程式注入到建構函式中。
using Microsoft.Extensions.Logging; using Quartz; using System.Threading.Tasks; [DisallowConcurrentExecution] 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; } }
我還用[DisallowConcurrentExecution]屬性裝飾了job。此屬性防止Quartz.NET試圖同時執行相同的作業。
現在我們已經建立了作業,我們需要將它與trigger一起註冊到DI容器中。
配置job
Quartz.NET為執行job提供了一些簡單的schedule,但最常見的方法之一是使用Quartz.NETCron表示式。Cron表示式允許複雜的計時器排程,所以你可以設定規則,比如“每個月的5號和20號,在早上8點到10點之間每半小時觸發一次”。使用時請確保檢查示例文件,因為不同系統使用的所有Cron表示式都是可互換的。
下面的示例展示瞭如何使用每5秒執行一次的trggier來註冊HelloWorldJob:
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureServices((hostContext, services) => { services.AddQuartz(q => { q.UseMicrosoftDependencyInjectionScopedJobFactory(); // Create a "key" for the job var jobKey = new JobKey("HelloWorldJob"); // Register the job with the DI container q.AddJob<HelloWorldJob>(opts => opts.WithIdentity(jobKey)); // Create a trigger for the job q.AddTrigger(opts => opts .ForJob(jobKey) // link to the HelloWorldJob .WithIdentity("HelloWorldJob-trigger") // give the trigger a unique name .WithCronSchedule("0/5 * * * * ?")); // run every 5 seconds }); services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); // ... });
在本程式碼中,我們:
- 為job建立唯一的JobKey。這用於將job與其trggier連線在一起。還有其他連線job和trggier的方法,但我認為這和其他方法一樣好。
- 用AddJob<T>註冊HelloWorldJob。這做了兩件事—它將HelloWorldJob新增到DI容器中,這樣就可以建立它;它在內部向Quartz註冊job。
- 新增一個觸發器,每5秒執行一次作業。我們使用JobKey將trigger與一個job關聯起來,併為trigger提供唯一的名稱(在本例中不是必需的,但如果你在叢集模式下執行quartz,這很重要)。最後,我們為trigger設定了Cron排程,使作業每5秒執行一次。
這就實現了功能!不再需要建立自定義的IJobFactory,也不用擔心是否支援scoped的服務。
預設的包為你處理所有這些問題——你可以在IJob中使用scoped的服務,它們將在job完成時被刪除。
如果你現在執行你的應用程式,你會看到和以前一樣的啟動訊息,然後每5秒你會看到HelloWorldJob寫入控制檯:
這就是啟動和執行所需的全部內容,但是根據我的喜好,在ConfigureServices方法中添加了太多內容。你也不太可能想在應用程式中硬編碼作業排程。例如,如果將其提取到配置中,可以在每個環境中使用不同的排程。
最起碼,我們希望將Cron排程提取到配置中。例如,你可以在appsettings.json中新增以下內容:
{ "Quartz": { "HelloWorldJob": "0/5 * * * * ?" } }
然後,你可以輕鬆地在不同環境中覆蓋HelloWorldJob的觸發器排程。
為了方便註冊,我們可以建立一個擴充套件方法來封裝在Quartz上註冊IJob,並設定它的trigger排程。這段程式碼與前面的示例基本相同,但是它使用job的名稱在IConfiguration中載入Cron排程。
public static class ServiceCollectionQuartzConfiguratorExtensions{ public static void AddJobAndTrigger<T>( this IServiceCollectionQuartzConfigurator quartz, IConfiguration config) where T : IJob { // Use the name of the IJob as the appsettings.json key string jobName = typeof(T).Name; // Try and load the schedule from configuration var configKey = $"Quartz:{jobName}"; var cronSchedule = config[configKey]; // Some minor validation if (string.IsNullOrEmpty(cronSchedule)) { throw new Exception($"No Quartz.NET Cron schedule found for job in configuration at {configKey}"); } // register the job as before var jobKey = new JobKey(jobName); quartz.AddJob<T>(opts => opts.WithIdentity(jobKey)); quartz.AddTrigger(opts => opts .ForJob(jobKey) .WithIdentity(jobName + "-trigger") .WithCronSchedule(cronSchedule)); // use the schedule from configuration } }
現在我們可以使用擴充套件方法清理應用程式的Program.cs:
public class Program { public static void Main(string[] args) => CreateHostBuilder(args).Build().Run(); public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureServices((hostContext, services) => { services.AddQuartz(q => { q.UseMicrosoftDependencyInjectionScopedJobFactory(); // Register the job, loading the schedule from configuration q.AddJobAndTrigger<HelloWorldJob>(hostContext.Configuration); }); services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); }); }
這本質上與我們的配置相同,但是我們已經使新增新job和排程的細節移到配置中變得更容易。
再次執行應用程式會給出相同的輸出:job每5秒寫一次輸出。
總結
在這篇文章中,我介紹了Quartz.NET並展示瞭如何使用新的Quartz.Extensions.Hosting輕鬆新增一個ASP.NET Core託管服務執行Quartz排程器。我展示瞭如何使用trigger實現一個簡單的job,以及如何將其註冊到應用程式中,以便託管的服務按計劃執行它。
歡迎關注我的公眾號,如果你有喜歡的外文技術文章,可以通過公眾號留言推薦給我。
原文連結:https://andrewlock.net/using-quartz-net-with-asp-net-core-and-worker-services/