TL;DR(完整回答在下面)
假定使用的工具:Visual Studio 2017 RTM、.NET Core 1.1、.NET Core SDK 1.0、SQL Server Express 2016 LocalDB。
在Web应用程序.csproj中:
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<PackageReference Include="Quartz" Version="3.0.0-alpha2" />
<PackageReference Include="Quartz.Serialization.Json" Version="3.0.0-alpha2" />
</ItemGroup>
</Project>
在Visual Studio默认生成的
Program
类中:
public class Program
{
private static IScheduler _scheduler;
public static void Main(string[] args)
{
var host = new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseStartup<Startup>()
.UseApplicationInsights()
.Build();
StartScheduler();
host.Run();
}
private static void StartScheduler()
{
var properties = new NameValueCollection {
["quartz.serializer.type"] = "json",
["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",
["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)
{
IMyEmailService emailService = new MyEmailService();
return emailService.SendUserEmails();
}
}
完整答案
.NET Core的Quartz
首先,根据此公告,您必须使用v3的Quartz,因为它针对.NET Core。
目前,只有v3版本的alpha版可在NuGet上获得。看起来团队花了很多精力来发布2.5.0,但它并不针对.NET Core。尽管如此,在他们的GitHub存储库中,master
分支已经专门用于v3,基本上,v3版本的未解决问题似乎并不重要,大多是旧的愿望清单项目,依我看。由于最近的提交活动相当低,我预计v3版本将在几个月内发布,或者可能半年 - 但没有人知道。
作业和IIS回收
如果Web应用程序将托管在IIS下,则必须考虑工作进程的回收/卸载行为。 ASP.NET Core Web应用程序作为常规的.NET Core进程运行,与w3wp.exe分开 - IIS仅充当反向代理。尽管如此,当w3wp.exe的实例被回收或卸载时,相关的.NET Core应用程序进程也会被通知退出(根据此)。
Web应用程序也可以在非IIS反向代理(例如NGINX)后面进行自托管,但我将假定您使用IIS,并相应地缩小我的答案范围。
回收/卸载引入的问题在@darin-dimitrov提到的文章中很好地解释了:
- 例如,在星期五9:00,由于几个小时前没有活动而被IIS卸载,进程已经停止,直到进程再次启动才会发送管理电子邮件。为了避免这种情况发生,请配置IIS以最小化卸载/回收(请参见此答案)。
- 从我的经验来看,上述配置仍然不能保证IIS永远不会卸载应用程序。为了确保您的进程正常运行,您可以设置定期向您的应用程序发送请求的命令,从而保持其处于活动状态。
- 当主机进程被回收/卸载时,必须优雅地停止作业,以避免数据损坏。
为什么要在Web应用程序中托管定时作业
我认为有一种正当理由将这些电子邮件作业托管在Web应用程序中,尽管存在上述问题。这是决策只有一种应用程序模型(ASP.NET)。这种方法简化了学习曲线、部署程序、生产监控等方面的工作。
如果您不想引入后端微服务(将电子邮件作业移动到其中的一个好地方),那么在Web应用程序中运行Quartz,克服IIS回收/卸载行为是有意义的。
或者你可能有其他理由。
持久作业存储
在您的情况下,作业执行的状态必须保存在进程外。因此,默认的RAMJobStore不适合,您必须使用ADO.NET作业存储。
由于您在问题中提到了SQL Server,我将提供适用于SQL Server数据库的设置示例。
如何启动(和优雅地停止)调度程序
我假设您使用Visual Studio 2017和最新/最近版本的.NET Core工具。我的.NET Core Runtime 1.1和.NET Core SDK 1.0。
对于数据库设置示例,我将在SQL Server 2016 Express LocalDB中使用名为Quartz
的数据库。可以在这里找到DB设置脚本。
首先,在Web应用程序.csproj中添加所需的包引用(或使用Visual Studio中的NuGet包管理器GUI完成):
<Project Sdk="Microsoft.NET.Sdk.Web">
<ItemGroup>
<PackageReference Include="Quartz" Version="3.0.0-alpha2" />
<PackageReference Include="Quartz.Serialization.Json" Version="3.0.0-alpha2" />
</ItemGroup>
</Project>
通过迁移指南和V3教程的帮助,我们可以了解如何启动和停止调度程序。 我们建议将此封装在一个单独的类中,命名为QuartzStartup
。
using System;
using System.Collections.Specialized;
using System.Threading.Tasks;
using Quartz;
using Quartz.Impl;
namespace WebApplication1
{
public class QuartzStartup
{
private IScheduler _scheduler;
public void Start()
{
if (_scheduler != null)
{
throw new InvalidOperationException("Already started.");
}
var properties = new NameValueCollection {
["quartz.serializer.type"] = "json",
["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",
["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 void Stop()
{
if (_scheduler == null)
{
return;
}
if (_scheduler.Shutdown(waitForJobsToComplete: true).Wait(30000))
{
_scheduler = null;
}
else
{
}
}
}
}
注意1:在上面的例子中,
SendUserEmailsJob
和
SendAdminEmailsJob
是实现
IJob
接口的类。
IJob
接口与
IMyEmailService
略有不同,因为它返回void
Task
而不是
Task<bool>
。这两个作业类都应该将
IMyEmailService
作为依赖项(可能是构造函数注入)。
注意2:为了让长时间运行的作业能够及时退出,在
IJob.Execute
方法中,它应该观察
IJobExecutionContext.CancellationToken
的状态。这可能需要更改
IMyEmailService
接口,使其方法接收
CancellationToken
参数:
public interface IMyEmailService
{
Task<bool> SendAdminEmails(CancellationToken cancellation);
Task<bool> SendUserEmails(CancellationToken cancellation);
}
何时和在哪里启动和停止调度程序
在ASP.NET Core中,应用程序引导代码位于Program
类中,就像在控制台应用程序中一样。调用Main
方法创建Web主机,运行它,并等待其退出:
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中所做的那样。但由于我们还必须正确地处理进程关闭,因此我更喜欢以一种更一致的方式挂钩启动和关闭代码。
这行代码:
.UseStartup<Startup>()
指的是一个名为Startup
的类,在使用Visual Studio创建新的ASP.NET Core Web应用程序项目时生成。 Startup
类如下所示:
public class Startup
{
public Startup(IHostingEnvironment env)
{
}
public IConfigurationRoot Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
}
}
很明显,在
Startup
类的某个方法中应该插入对
QuartzStartup.Start
的调用。问题是,
QuartzStartup.Stop
应该在哪里挂接。
在传统的 .NET Framework 中,ASP.NET 提供了
IRegisteredObject
接口。根据
这篇文章和
文档,在 ASP.NET Core 中,它被替换为
IApplicationLifetime
。很好。可以通过参数将
IApplicationLifetime
实例注入到
Startup.Configure
方法中。
为了保持一致性,我将同时将
QuartzStartup.Start
和
QuartzStartup.Stop
挂接到
IApplicationLifetime
上:
public class Startup
{
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory,
IApplicationLifetime lifetime)
{
var quartz = new QuartzStartup();
lifetime.ApplicationStarted.Register(quartz.Start);
lifetime.ApplicationStopping.Register(quartz.Stop);
}
}
请注意,我已经扩展了
Configure
方法的签名,增加了一个额外的
IApplicationLifetime
参数。根据
文档,
ApplicationStopping
将阻塞,直到注册的回调完成。
在IIS Express和ASP.NET Core模块上进行优雅的关闭
我只能在安装了最新版本的ASP.NET Core模块的IIS上观察到
IApplicationLifetime.ApplicationStopping
钩子的预期行为。与Visual Studio 2017 Community RTM一起安装的IIS Express(用于开发环境)和带有过时版本ASP.NET Core模块的IIS没有始终调用
IApplicationLifetime.ApplicationStopping
。我认为这是因为已解决的
此错误。
您可以从
这里安装最新版本的ASP.NET Core模块。请按照“安装最新ASP.NET Core模块”部分中的说明进行操作。
Quartz vs. FluentScheduler
我还看了一下FluentScheduler,因为@Brice Molesti提出了它作为替代库。我的第一印象是,与Quartz相比,FluentScheduler是一个相当简单和不成熟的解决方案。例如,FluentScheduler没有提供作业状态持久性和集群执行等基本功能。
JobScheduler.Start();
等效的代码示例,该示例解释了http://www.mikesdotnetting.com/article/254/scheduled-tasks-in-asp-net-with-quartz-net。我应该能够解决调度问题,但在ASP.NET Core中启动Quartz仍然是一个谜。Google没有找到相关信息。 - dev2go