如何在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软件包,因为我想要一个轻量级的调度程序在我的Web应用程序中没有单独的SQL Server数据库.
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:00 -
SendAdminEmails
每周星期四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,因为Internet上的所有代码示例仍然引用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 v3,因为它针对.NET Core,根据当前,只有alpha版本的v3软件包在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.
尽管上面列出了问题,但我可以想到将这些电子邮件作业托管在Web应用程序中的一种理由.决定只有一种应用程序模型(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. 或者也许您还有其他原因. Or maybe you have other reasons. 在您的方案中,作业执行的状态必须保留在流程之外.因此,默认的 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中使用名为 For DB setup example, I will use a database named 首先,将必需的程序包引用添加到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): 借助迁移指南和 V3教程,我们可以弄清楚如何启动和停止调度程序.我更喜欢将其封装在一个单独的类中,将其命名为 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 注1.在上面的示例中, Note 1. In the above example, 注2.为了使长时间运行的作业能够及时退出,在 Note 2. For a long-running job to be able to exit in timely fashion, in the
在ASP.NET Core中,应用程序引导代码位于类
In ASP.NET Core, application bootstrap code resides in class 最简单的方法是直接在 The simplest thing to do is just put a call to 此行: 指的是名为 refers to a class named 很明显,应该将对 It is clear that a call to 在旧版.NET Framework中,ASP.NET提供了 In the legacy .NET Framework, ASP.NET provided 为了保持一致,我将把 For consistency, I will hook both 请注意,我已经通过附加的 Note that I have extended the signature of the 在安装了最新的ASP.NET Core模块的情况下,我只能在IIS上观察到 I was able to observe expected behavior of 您可以从此处安装最新版本的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屋!
Quartz
的数据库.可以在此处找到.Quartz
in SQL Server 2016 Express LocalDB. DB setup scripts can be found here.<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>
QuartzStartup
.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...
}
}
}
}
SendUserEmailsJob
和SendAdminEmailsJob
是实现IJob
的类. IJob
接口与IMyEmailService
略有不同,因为它返回void Task
而不是Task<bool>
.两个作业类都应获取IMyEmailService
作为依赖项(可能是构造函数注入).SendUserEmailsJob
and SendAdminEmailsJob
are classes that implement IJob
. The IJob
interface is slightly different from IMyEmailService
, because it returns void Task
and not Task<bool>
. Both job classes should get IMyEmailService
as a dependency (probably constructor injection).IJob.Execute
方法中,应观察IJobExecutionContext.CancellationToken
的状态.这可能需要更改IMyEmailService
接口,以使其方法接收CancellationToken
参数:IJob.Execute
method, it should observe the status of IJobExecutionContext.CancellationToken
. This may require change in IMyEmailService
interface, to make its methods receive CancellationToken
parameter:public interface IMyEmailService
{
Task<bool> SendAdminEmails(CancellationToken cancellation);
Task<bool> SendUserEmails(CancellationToken cancellation);
}
何时何地启动和停止调度程序
Program
中,与控制台应用程序中的代码非常相似.调用Main
方法来创建Web主机,运行它,然后等待它退出:When and where to start and stop the scheduler
Program
, much like in console app. The Main
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中所做的一样.但是,由于我们还必须正确处理进程关闭,因此我更喜欢以更一致的方式挂接启动和关闭代码.QuartzStartup.Start
right in the Main
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应用程序项目时会被构架. Startup
类如下所示:Startup
, which is scaffolded when creating new ASP.NET Core Web Application project in Visual Studio. The Startup
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
应该挂在哪里.QuartzStartup.Start
should be inserted in one of methods in the Startup
class. The question is, where QuartzStartup.Stop
should be hooked.IRegisteredObject
接口.根据此帖子和IRegisteredObject
interface. According to this post, and the documentation, in ASP.NET Core it was replaced with IApplicationLifetime
. Bingo. An instance of IApplicationLifetime
can be injected into Startup.Configure
method through a parameter.QuartzStartup.Start
和QuartzStartup.Stop
都钩到IApplicationLifetime
:QuartzStartup.Start
and QuartzStartup.Stop
to IApplicationLifetime
: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
将阻塞直到注册的回调完成.Configure
method with an additional IApplicationLifetime
parameter. According to documentation, ApplicationStopping
will block until registered callbacks are completed.IApplicationLifetime.ApplicationStopping
挂钩的预期行为. IIS Express(随Visual Studio 2017 Community RTM一起安装)和带有ASP.NET Core模块过时版本的IIS都没有一致地调用IApplicationLifetime.ApplicationStopping
.我相信是由于此错误已修复.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 invoke IApplicationLifetime.ApplicationStopping
. I believe it is because of this bug that was fixed.