我有以下课程
public class MyEmailService { public async TaskSendAdminEmails() { ... } public async Task SendUserEmails() { ... } } public interface IMyEmailService { Task SendAdminEmails(); Task SendUserEmails(); }
我已经安装了最新的Quartz 2.4.1 Nuget包,因为我想在我的Web应用程序中使用轻量级调度程序,而不需要单独的SQL Server数据库.
我需要安排方法
SendUserEmails
每周一至周一17:00,周二17:00和周三17:00
SendAdminEmails
每周星期四09:00,星期五9:00运行
在ASP.NET Core中使用Quartz安排这些方法需要什么代码?我还需要知道如何在ASP.NET Core中启动Quartz,因为互联网上的所有代码示例仍然引用以前版本的ASP.NET.
我可以找到以前版本的ASP.NET 的代码示例,但我不知道如何在ASP.NET Core中启动Quartz以开始测试.我在哪里放入JobScheduler.Start();
ASP.NET Core?
假设工具:Visual Studio 2017 RTM,.NET Core 1.1,.NET Core SDK 1.0,SQL Server Express 2016 LocalDB.
在Web应用程序.csproj中:
在Program
类中(默认情况下由Visual Studio搭建):
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()
.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()
.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()
.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();
}
}
首先,根据这个公告,你必须使用Quartz的v3,因为它的目标是.NET Core .
目前,NuGet上只提供alpha版v3包.看起来团队花了很多精力发布2.5.0,而不是针对.NET Core.尽管如此,在他们的GitHub回购中,该master
分支已经专注于v3,基本上,v3发布的开放性问题似乎并不重要,主要是旧的愿望清单项目,恕我直言.由于最近的提交活动非常低,我预计v3会在几个月内发布,或者可能是半年 - 但没有人知道.
如果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以最小化卸载/重新循环(请参阅此答案).
根据我的经验,上面的配置仍然没有100%保证IIS永远不会卸载应用程序.为了100%保证您的进程已启动,您可以设置一个命令,定期向您的应用程序发送请求,从而使其保持活动状态.
回收/卸载主机进程时,必须正常停止作业,以避免数据损坏.
尽管存在上面列出的问题,我仍然可以想到将这些电子邮件作业托管在Web应用程序中的一个理由.决定只有一种应用程序模型(ASP.NET).这种方法简化了学习曲线,部署程序,生产监控等.
如果您不想引入后端微服务(这将是移动电子邮件作业的好地方),那么克服IIS回收/卸载行为,并在Web应用程序中运行Quartz是有意义的.
或许你有其他原因.
在您的方案中,作业执行的状态必须保持在进程外.因此,默认RAMJobStore不适合,您必须使用ADO.NET作业存储.
由于您在问题中提到了SQL Server,因此我将提供SQL Server数据库的示例设置.
我假设您使用Visual Studio 2017和最新/最新版本的.NET Core工具.我的是.NET Core Runtime 1.1和.NET Core SDK 1.0.
对于数据库设置示例,我将使用Quartz
SQL Server 2016 Express LocalDB中指定的数据库.可以在此处找到数据库设置脚本.
首先,向Web应用程序.csproj添加必需的包引用(或者在Visual Studio中使用NuGet包管理器GUI执行此操作):
在迁移指南和V3教程的帮助下,我们可以弄清楚如何启动和停止调度程序.我更喜欢将它封装在一个单独的类中,让我们来命名它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()
.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()
.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
,因为它返回void Task
而不是Task
.两个作业类都应该IMyEmailService
作为依赖项(可能是构造函数注入).
注意2.对于能够及时退出的长期工作,在该IJob.Execute
方法中,应该观察其状态IJobExecutionContext.CancellationToken
.这可能需要更改IMyEmailService
接口,以使其方法接收 CancellationToken
参数:
public interface IMyEmailService
{
Task SendAdminEmails(CancellationToken cancellation);
Task 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()
.UseApplicationInsights()
.Build();
host.Run();
}
}
最简单QuartzStartup.Start
的Main
方法就是在方法中调用正确的方法,就像我在TL中做的那样; DR.但由于我们必须正确处理进程关闭,因此我更喜欢以更一致的方式挂接启动和关闭代码.
这一行:
.UseStartup()
指的是一个名为的类Startup
,它在Visual Studio中创建新的ASP.NET核心Web应用程序项目时被搭建.这个Startup
类看起来像这样:
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
应该在哪里上钩.
在旧版.NET Framework中,ASP.NET提供了IRegisteredObject
接口.根据这篇文章和文档,在ASP.NET Core中它被替换为IApplicationLifetime
.答对了.IApplicationLifetime
可以Startup.Configure
通过参数将实例注入到方法中.
为了保持一致性,我会大钩QuartzStartup.Start
,并QuartzStartup.Stop
于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 ....
}
请注意,我已Configure
使用附加IApplicationLifetime
参数扩展了方法的签名.根据文档,ApplicationStopping
将阻止,直到注册的回调完成.
我能够IApplicationLifetime.ApplicationStopping
在IIS上观察到钩子的预期行为,并安装了最新的ASP.NET Core模块.IIS Express(与Visual Studio 2017社区RTM一起安装)和具有过时版本的ASP.NET Core模块的IIS都没有始终如一地调用IApplicationLifetime.ApplicationStopping
.我相信这是因为这个错误被修复了.
您可以从此处安装最新版本的ASP.NET Core模块.按照"安装最新的ASP.NET核心模块"部分中的说明进行操作.
我还看了一下FluentScheduler,因为它被@Brice Molesti提议作为替代库.令我的第一印象是,与Quartz相比,FluentScheduler是一个相当简单且不成熟的解决方案.例如,FluentScheduler不提供作业状态持久性和集群执行等基本功能.
除了@ felix-b答案。将DI添加到作业中。也可以使QuartzStartup Start异步。
基于此答案:https : //stackoverflow.com/a/42158004/1235390
public class QuartzStartup { public QuartzStartup(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public async Task Start() { // other code is same _scheduler = await schedulerFactory.GetScheduler(); _scheduler.JobFactory = new JobFactory(_serviceProvider); await _scheduler.Start(); var sampleJob = JobBuilder.Create().Build(); var sampleTrigger = TriggerBuilder.Create().StartNow().WithCronSchedule("0 0/1 * * * ?").Build(); await _scheduler.ScheduleJob(sampleJob, sampleTrigger); } }
JobFactory类
public class JobFactory : IJobFactory { private IServiceProvider _serviceProvider; public JobFactory(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler) { return _serviceProvider.GetService(bundle.JobDetail.JobType) as IJob; } public void ReturnJob(IJob job) { (job as IDisposable)?.Dispose(); } }
启动类:
public void ConfigureServices(IServiceCollection services) { // other code is removed for brevity // need to register all JOBS by their class name services.AddTransient(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime applicationLifetime) { var quartz = new QuartzStartup(_services.BuildServiceProvider()); applicationLifetime.ApplicationStarted.Register(() => quartz.Start()); applicationLifetime.ApplicationStopping.Register(quartz.Stop); // other code removed for brevity }
具有构造函数依赖项注入的SampleJob类:
public class SampleJob : IJob { private readonly ILogger_logger; public SampleJob(ILogger logger) { _logger = logger; } public async Task Execute(IJobExecutionContext context) { _logger.LogDebug("Execute called"); } }