如何在 ASP.NET Core 中启动 Quartz? [英] How to start Quartz in ASP.NET Core?
问题描述
我有以下课程
public class MyEmailService
{
public async Task<bool> SendAdminEmails()
{
...
}
public async Task<bool> SendUserEmails()
{
...
}
}
public interface IMyEmailService
{
Task<bool> SendAdminEmails();
Task<bool> SendUserEmails();
}
我已经安装了最新的 Quartz 2.4.1 Nuget 包,因为我想要一个轻量级的调度程序在没有单独 SQL Server 数据库的 Web 应用中.
I have installed the latest Quartz 2.4.1 Nuget package as I wanted a lightweight scheduler in my web app without a separate SQL Server database.
我需要安排方法
SendUserEmails
每周星期一 17:00、星期二 17:00 和运行周三 17:00SendAdminEmails
每周周四 09:00、周五 9:00 运行
SendUserEmails
to run every week on Mondays 17:00,Tuesdays 17:00 & Wednesdays 17:00SendAdminEmails
to run every week on Thursdays 09:00, Fridays 9:00
在 ASP.NET Core 中使用 Quartz 来调度这些方法需要什么代码?我还需要知道如何在 ASP.NET Core 中启动 Quartz,因为互联网上的所有代码示例仍然参考以前版本的 ASP.NET.
What code do I need to schedule these methods using Quartz in ASP.NET Core? I also need to know how to start Quartz in ASP.NET Core as all code samples on the internet still refer to previous versions of ASP.NET.
我可以找到代码示例 之前版本的ASP.NET 但是我不知道如何在ASP.NET Core 中启动Quartz 来开始测试.我应该把 JobScheduler.Start();
放在 ASP.NET Core 的什么位置?
I can find a code sample for the previous version of ASP.NET but I don't know how to start Quartz in ASP.NET Core to start testing.
Where do I put the JobScheduler.Start();
in ASP.NET Core?
推荐答案
TL;DR(完整答案可以在下面找到)
假设工具:Visual Studio 2017 RTM、.NET Core 1.1、.NET Core SDK 1.0、SQL Server Express 2016 LocalDB.
TL;DR (full answer can be found below)
Assumed tooling: Visual Studio 2017 RTM, .NET Core 1.1, .NET Core SDK 1.0, SQL Server Express 2016 LocalDB.
在 Web 应用程序 .csproj 中:
In web application .csproj:
<Project Sdk="Microsoft.NET.Sdk.Web">
<!-- .... existing contents .... -->
<!-- add the following ItemGroup element, it adds required packages -->
<ItemGroup>
<PackageReference Include="Quartz" Version="3.0.0-alpha2" />
<PackageReference Include="Quartz.Serialization.Json" Version="3.0.0-alpha2" />
</ItemGroup>
</Project>
在 Program
类中(默认情况下由 Visual Studio 搭建):
In the Program
class (as scaffolded by Visual Studio by default):
public class Program
{
private static IScheduler _scheduler; // add this field
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseApplicationInsights()
.Build();
StartScheduler(); // add this line
host.Run();
}
// add this method
private static void StartScheduler()
{
var properties = new NameValueCollection {
// json serialization is the one supported under .NET Core (binary isn't)
["quartz.serializer.type"] = "json",
// the following setup of job store is just for example and it didn't change from v2
// according to your usage scenario though, you definitely need
// the ADO.NET job store and not the RAMJobStore.
["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz",
["quartz.jobStore.useProperties"] = "false",
["quartz.jobStore.dataSource"] = "default",
["quartz.jobStore.tablePrefix"] = "QRTZ_",
["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz",
["quartz.dataSource.default.provider"] = "SqlServer-41", // SqlServer-41 is the new provider for .NET Core
["quartz.dataSource.default.connectionString"] = @"Server=(localdb)MSSQLLocalDB;Database=Quartz;Integrated Security=true"
};
var schedulerFactory = new StdSchedulerFactory(properties);
_scheduler = schedulerFactory.GetScheduler().Result;
_scheduler.Start().Wait();
var userEmailsJob = JobBuilder.Create<SendUserEmailsJob>()
.WithIdentity("SendUserEmails")
.Build();
var userEmailsTrigger = TriggerBuilder.Create()
.WithIdentity("UserEmailsCron")
.StartNow()
.WithCronSchedule("0 0 17 ? * MON,TUE,WED")
.Build();
_scheduler.ScheduleJob(userEmailsJob, userEmailsTrigger).Wait();
var adminEmailsJob = JobBuilder.Create<SendAdminEmailsJob>()
.WithIdentity("SendAdminEmails")
.Build();
var adminEmailsTrigger = TriggerBuilder.Create()
.WithIdentity("AdminEmailsCron")
.StartNow()
.WithCronSchedule("0 0 9 ? * THU,FRI")
.Build();
_scheduler.ScheduleJob(adminEmailsJob, adminEmailsTrigger).Wait();
}
}
作业类的示例:
public class SendUserEmailsJob : IJob
{
public Task Execute(IJobExecutionContext context)
{
// an instance of email service can be obtained in different ways,
// e.g. service locator, constructor injection (requires custom job factory)
IMyEmailService emailService = new MyEmailService();
// delegate the actual work to email service
return emailService.SendUserEmails();
}
}
完整答案
用于 .NET Core 的 Quartz
首先,您必须使用 Quartz v3,因为它针对 .NET Core,根据 本公告.
目前,只有 v3 包的 alpha 版本在 NuGet 上可用.看起来团队花了很多精力来发布 2.5.0,它不针对 .NET Core.尽管如此,在他们的 GitHub 存储库中,master
分支已经专用于 v3,基本上,v3 版本的未解决问题 似乎并不重要,主要是旧的愿望清单项目,恕我直言.由于最近 提交活动 相当低,我预计 v3 会在几个月内发布,或者半年 - 但没有人知道.
Currently, only alpha versions of v3 packages are available on NuGet. It looks like the team put a lot of effort into releasing 2.5.0, which does not target .NET Core. Nevertheless, in their GitHub repo, the master
branch is already dedicated to v3, and basically, open issues for v3 release don't seem to be critical, mostly old wishlist items, IMHO. Since recent commit activity is quite low, I would expect v3 release in few months, or maybe half year - but no one knows.
如果 Web 应用程序将托管在 IIS 下,则必须考虑工作进程的回收/卸载行为.ASP.NET Core Web 应用程序作为常规 .NET Core 进程运行,独立于 w3wp.exe - IIS 仅用作反向代理.然而,当 w3wp.exe 的一个实例被回收或卸载时,相关的 .NET Core 应用进程也会发出退出信号(根据 这个).
If the web application is going to be hosted under IIS, you have to take into consideration recycling/unloading behavior of worker processes. The ASP.NET Core web app runs as a regular .NET Core process, separate from w3wp.exe - IIS only serves as a reverse proxy. Nevertheless, when an instance of w3wp.exe is recycled or unloaded, the related .NET Core app process is also signaled to exit (according to this).
Web 应用程序也可以在非 IIS 反向代理(例如 NGINX)之后自托管,但我假设您确实使用 IIS,并相应地缩小我的回答范围.
Web application can also be self-hosted behind a non-IIS reverse proxy (e.g. NGINX), but I will assume that you do use IIS, and narrow my answer accordingly.
回收/卸载引入的问题在@darin-dimitrov 引用的帖子:
The problems that recycling/unloading introduces are explained well in the post referenced by @darin-dimitrov:
- 例如,如果在星期五 9:00 进程停止运行,因为它在几个小时前由于不活动而被 IIS 卸载 - 在进程再次启动之前不会发送管理员电子邮件.为避免这种情况,请配置 IIS 以最大程度地减少卸载/回收(请参阅此答案).
- 根据我的经验,上述配置仍然不能 100% 保证 IIS 永远不会卸载应用程序.为了 100% 保证您的进程已启动,您可以设置一个命令来定期向您的应用发送请求,从而使其保持活动状态.
尽管存在上面列出的问题,但我可以想到将这些电子邮件作业托管在网络应用程序中的一个理由.决定只有一种应用程序模型 (ASP.NET).这种方法简化了学习曲线、部署程序、生产监控等.
I can think of one justification of having those email jobs hosted in a web app, despite the problems listed above. It is decision to have only one kind of application model (ASP.NET). Such approach simplifies learning curve, deployment procedure, production monitoring, etc.
如果您不想引入后端微服务(这将是将电子邮件作业移动到的好地方),那么克服 IIS 回收/卸载行为并在 Web 应用程序中运行 Quartz 是有意义的.
If you don't want to introduce backend microservices (which would be a good place to move the email jobs to), then it makes sense to overcome IIS recycling/unloading behaviors, and run Quartz inside a web app.
或者你有其他原因.
在您的场景中,作业执行的状态必须在进程外持久化.因此,默认的RAMJobStore 不适合,您必须使用ADO.NET Job Store.
In your scenario, status of job execution must be persisted out of process. Therefore, default RAMJobStore doesn't fit, and you have to use the ADO.NET Job Store.
既然你在问题中提到了 SQL Server,我将提供 SQL Server 数据库的示例设置.
Since you mentioned SQL Server in the question, I will provide example setup for SQL Server database.
我假设您使用 Visual Studio 2017 和最新/最近版本的 .NET Core 工具.我的是 .NET Core Runtime 1.1 和 .NET Core SDK 1.0.
I assume you use Visual Studio 2017 and latest/recent version of .NET Core tooling. Mine is .NET Core Runtime 1.1 and .NET Core SDK 1.0.
对于数据库设置示例,我将在 SQL Server 2016 Express LocalDB 中使用名为
Quartz
的数据库.可以在此处找到数据库设置脚本.For DB setup example, I will use a database named
Quartz
in SQL Server 2016 Express LocalDB. DB setup scripts can be found here.首先,将所需的包引用添加到 Web 应用程序 .csproj(或使用 Visual Studio 中的 NuGet 包管理器 GUI 来完成):
First, add required package references to web application .csproj (or do it with NuGet package manager GUI in Visual Studio):
<Project Sdk="Microsoft.NET.Sdk.Web"> <!-- .... existing contents .... --> <!-- the following ItemGroup adds required packages --> <ItemGroup> <PackageReference Include="Quartz" Version="3.0.0-alpha2" /> <PackageReference Include="Quartz.Serialization.Json" Version="3.0.0-alpha2" /> </ItemGroup> </Project>
借助迁移指南和 V3 教程,我们可以弄清楚如何启动和停止调度程序.我更喜欢将它封装在一个单独的类中,让我们将其命名为
QuartzStartup
.With the help of Migration Guide and the V3 Tutorial, we can figure out how to start and stop the scheduler. I prefer to encapsulate this in a separate class, let's name it
QuartzStartup
.using System; using System.Collections.Specialized; using System.Threading.Tasks; using Quartz; using Quartz.Impl; namespace WebApplication1 { // Responsible for starting and gracefully stopping the scheduler. public class QuartzStartup { private IScheduler _scheduler; // after Start, and until shutdown completes, references the scheduler object // starts the scheduler, defines the jobs and the triggers public void Start() { if (_scheduler != null) { throw new InvalidOperationException("Already started."); } var properties = new NameValueCollection { // json serialization is the one supported under .NET Core (binary isn't) ["quartz.serializer.type"] = "json", // the following setup of job store is just for example and it didn't change from v2 ["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz", ["quartz.jobStore.useProperties"] = "false", ["quartz.jobStore.dataSource"] = "default", ["quartz.jobStore.tablePrefix"] = "QRTZ_", ["quartz.jobStore.driverDelegateType"] = "Quartz.Impl.AdoJobStore.SqlServerDelegate, Quartz", ["quartz.dataSource.default.provider"] = "SqlServer-41", // SqlServer-41 is the new provider for .NET Core ["quartz.dataSource.default.connectionString"] = @"Server=(localdb)MSSQLLocalDB;Database=Quartz;Integrated Security=true" }; var schedulerFactory = new StdSchedulerFactory(properties); _scheduler = schedulerFactory.GetScheduler().Result; _scheduler.Start().Wait(); var userEmailsJob = JobBuilder.Create<SendUserEmailsJob>() .WithIdentity("SendUserEmails") .Build(); var userEmailsTrigger = TriggerBuilder.Create() .WithIdentity("UserEmailsCron") .StartNow() .WithCronSchedule("0 0 17 ? * MON,TUE,WED") .Build(); _scheduler.ScheduleJob(userEmailsJob, userEmailsTrigger).Wait(); var adminEmailsJob = JobBuilder.Create<SendAdminEmailsJob>() .WithIdentity("SendAdminEmails") .Build(); var adminEmailsTrigger = TriggerBuilder.Create() .WithIdentity("AdminEmailsCron") .StartNow() .WithCronSchedule("0 0 9 ? * THU,FRI") .Build(); _scheduler.ScheduleJob(adminEmailsJob, adminEmailsTrigger).Wait(); } // initiates shutdown of the scheduler, and waits until jobs exit gracefully (within allotted timeout) public void Stop() { if (_scheduler == null) { return; } // give running jobs 30 sec (for example) to stop gracefully if (_scheduler.Shutdown(waitForJobsToComplete: true).Wait(30000)) { _scheduler = null; } else { // jobs didn't exit in timely fashion - log a warning... } } } }
注意 1. 在上面的例子中,
SendUserEmailsJob
和SendAdminEmailsJob
是实现IJob
的类.IJob
接口与IMyEmailService
略有不同,因为它返回 voidTask
而不是Task
.两个作业类都应该将IMyEmailService
作为依赖项(可能是构造函数注入).Note 1. In the above example,
SendUserEmailsJob
andSendAdminEmailsJob
are classes that implementIJob
. TheIJob
interface is slightly different fromIMyEmailService
, because it returns voidTask
and notTask<bool>
. Both job classes should getIMyEmailService
as a dependency (probably constructor injection).注意2.为了让长时间运行的作业能够及时退出,在
IJob.Execute
方法中,应该观察IJobExecutionContext.CancellationToken
.这可能需要更改IMyEmailService
接口,使其方法接收CancellationToken
参数:Note 2. For a long-running job to be able to exit in timely fashion, in the
IJob.Execute
method, it should observe the status ofIJobExecutionContext.CancellationToken
. This may require change inIMyEmailService
interface, to make its methods receiveCancellationToken
parameter:public interface IMyEmailService { Task<bool> SendAdminEmails(CancellationToken cancellation); Task<bool> SendUserEmails(CancellationToken cancellation); }
何时何地启动和停止调度程序
在 ASP.NET Core 中,应用程序引导代码驻留在类
Program
中,就像在控制台应用程序中一样.调用Main
方法来创建 Web 主机,运行它,然后等待它退出:When and where to start and stop the scheduler
In ASP.NET Core, application bootstrap code resides in class
Program
, much like in console app. TheMain
method is called to create web host, run it, and wait until it exits:public class Program { public static void Main(string[] args) { var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseIISIntegration() .UseStartup<Startup>() .UseApplicationInsights() .Build(); host.Run(); } }
最简单的方法就是在
Main
方法中直接调用QuartzStartup.Start
,就像我在 TL;DR 中所做的那样.但由于我们也必须正确处理进程关闭,我更喜欢以更一致的方式挂钩启动和关闭代码.The simplest thing to do is just put a call to
QuartzStartup.Start
right in theMain
method, much like as I did in TL;DR. But since we have to properly handle process shutdown as well, I prefer to hook both startup and shutdown code in a more consistent manner.这一行:
.UseStartup<Startup>()
指的是一个名为
Startup
的类,它是在Visual Studio中新建ASP.NET Core Web Application项目时搭建的.Startup
类如下所示:refers to a class named
Startup
, which is scaffolded when creating new ASP.NET Core Web Application project in Visual Studio. TheStartup
class looks like this:public class Startup { public Startup(IHostingEnvironment env) { // scaffolded code... } public IConfigurationRoot Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { // scaffolded code... } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { // scaffolded code... } }
很明显,对
QuartzStartup.Start
的调用应该插入到Startup
类的方法之一中.问题是,QuartzStartup.Stop
应该挂在哪里.It is clear that a call to
QuartzStartup.Start
should be inserted in one of methods in theStartup
class. The question is, whereQuartzStartup.Stop
should be hooked.在旧版 .NET Framework 中,ASP.NET 提供了
IRegisteredObject
接口.根据这篇文章,以及文档,在 ASP.NET Core 中它被替换为IApplicationLifetime
.答对了.IApplicationLifetime
的实例可以通过参数注入到Startup.Configure
方法中.In the legacy .NET Framework, ASP.NET provided
IRegisteredObject
interface. According to this post, and the documentation, in ASP.NET Core it was replaced withIApplicationLifetime
. Bingo. An instance ofIApplicationLifetime
can be injected intoStartup.Configure
method through a parameter.为了一致性,我将
QuartzStartup.Start
和QuartzStartup.Stop
都挂接到IApplicationLifetime
:For consistency, I will hook both
QuartzStartup.Start
andQuartzStartup.Stop
toIApplicationLifetime
:public class Startup { // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure( IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime lifetime) // added this parameter { // the following 3 lines hook QuartzStartup into web host lifecycle var quartz = new QuartzStartup(); lifetime.ApplicationStarted.Register(quartz.Start); lifetime.ApplicationStopping.Register(quartz.Stop); // .... original scaffolded code here .... } // ....the rest of the scaffolded members .... }
请注意,我已使用附加的
IApplicationLifetime
参数扩展了Configure
方法的签名.根据文档,ApplicationStopping
将阻塞,直到注册的回调完成.Note that I have extended the signature of the
Configure
method with an additionalIApplicationLifetime
parameter. According to documentation,ApplicationStopping
will block until registered callbacks are completed.我只能在安装了最新 ASP.NET Core 模块的 IIS 上观察到
IApplicationLifetime.ApplicationStopping
挂钩的预期行为.IIS Express(与 Visual Studio 2017 Community RTM 一起安装)和带有过时版本的 ASP.NET Core 模块的 IIS 都没有始终调用IApplicationLifetime.ApplicationStopping
.我相信这是因为此错误已修复.I was able to observe expected behavior of
IApplicationLifetime.ApplicationStopping
hook only on IIS, with the latest ASP.NET Core module installed. Both IIS Express (installed with Visual Studio 2017 Community RTM), and IIS with an outdated version of ASP.NET Core module didn't consistently invokeIApplicationLifetime.ApplicationStopping
. I believe it is because of this bug that was fixed.您可以从这里安装最新版本的 ASP.NET Core 模块.按照安装最新的 ASP.NET Core 模块" 部分中的说明进行操作.
You can install latest version of ASP.NET Core module from here. Follow the instructions in the "Installing the latest ASP.NET Core Module" section.
我还查看了 FluentScheduler,因为它被 @Brice Molesti 提议作为替代库.在我的第一印象中,与 Quartz 相比,FluentScheduler 是一个相当简单且不成熟的解决方案.例如,FluentScheduler 不提供作业状态持久化和集群执行等基本功能.
I also took a look at FluentScheduler, as it was proposed as an alternative library by @Brice Molesti. To my first impression, FluentScheduler is quite a simplistic and immature solution, compared to Quartz. For example, FluentScheduler doesn't provide such fundamental features as job status persistence and clustered execution.
这篇关于如何在 ASP.NET Core 中启动 Quartz?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!