asp.net-core – 如何在ASP.NET Core中启动Quartz?

我有以下课程
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 package,因为我想在我的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.

我可以find a code sample用于以前版本的ASP.NET,但我不知道如何在ASP.NET Core中启动Quartz来开始测试.
我在哪里放置JobScheduler.Start();在ASP.NET核心?

解决方法

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">

  <!-- .... 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搭建):

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.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();
    }
}

完整答案

Quartz for .NET Core

首先,根据this announcement,你必须使用Quartz的v3,因为它的目标是.NET Core.

目前,只有v3软件包的alpha版本是available on NuGet.看起来团队花了很多精力发布2.5.0,而不是针对.NET Core.尽管如此,在他们的GitHub回购中,主分支已经专注于v3,基本上,open issues for v3 release似乎并不重要,主要是旧的愿望清单项目,恕我直言.由于最近commit activity相当低,我预计v3会在几个月内发布,或者可能是半年 – 但没有人知道.

乔布斯和IIS回收

如果Web应用程序将在IIS下托管,则必须考虑工作进程的回收/卸载行为. ASP.NET Core Web应用程序作为常规.NET Core进程运行,与w3wp.exe分开 – IIS仅用作反向代理.然而,当循环或卸载w3wp.exe的实例时,相关的.NET Core应用程序进程也会发出信号以退出(根据this).

Web应用程序也可以在非IIS反向代理(例如Nginx)后面自行托管,但我会假设您使用IIS,并相应地缩小我的答案.

回收/卸载引入的问题在post referenced by @darin-dimitrov中得到了很好的解释:

>例如,如果在星期五9:00,该进程已关闭,因为几个小时之前它由于不活动而被IIS卸载 – 在该进程再次启动之前不会发送管理员电子邮件.为避免这种情况,请配置IIS以最小化卸载/重新循环(see this answer).

>根据我的经验,上述配置仍然没有100%保证IIS永远不会卸载应用程序.为了100%保证您的进程已启动,您可以设置一个命令,定期向您的应用程序发送请求,从而使其保持活动状态.

>回收/卸载主机进程时,必须正常停止作业,以避免数据损坏.

为什么要在Web应用程序中托管预定作业

尽管存在上面列出的问题,我仍然可以想到将这些电子邮件作业托管在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.

对于数据库设置示例,我将在sql Server 2016 Express LocalDB中使用名为Quartz的数据库.数据库设置脚本可以是found here.

首先,向Web应用程序.csproj添加必需的包引用(或者在Visual Studio中使用NuGet包管理器GUI执行此操作):

<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>

Migration GuideV3 Tutorial的帮助下,我们可以弄清楚如何启动和停止调度程序.我更喜欢将它封装在一个单独的类中,让我们将它命名为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,// 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,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略有不同,因为它返回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 Application项目时被搭建. 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...
    }
}

很明显,应该在Startup类的一个方法中插入对QuartzStartup.Start的调用.问题是,应该挂起QuartzStartup.Stop.

在旧版.NET Framework中,ASP.NET提供了IRegisteredobject接口.根据this postdocumentation,在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,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方法的签名.根据documentation,ApplicationStopping将阻止,直到注册的回调完成.

在IIS Express和ASP.NET Core模块上正常关闭

我能够在IIS上观察IApplicationLifetime.ApplicationStopping挂钩的预期行为,并安装了最新的ASP.NET Core模块. IIS Express(与Visual Studio 2017社区RTM一起安装)和具有过时版本的ASP.NET Core模块的IIS都没有始终如一地调用IApplicationLifetime.ApplicationStopping.我相信这是因为this bug修复了.

您可以安装最新版本的ASP.NET Core模块from here.请按照“安装最新的ASP.NET核心模块”部分中的说明进行操作.

Quartz vs. FluentScheduler

我还看了一下FluentScheduler,因为它被@Brice Molesti提议作为替代库.我的第一印象是,与Quartz相比,FluentScheduler是一个相当简单且不成熟的解决方案.例如,FluentScheduler不提供作业状态持久性和集群执行等基本功能.

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


本文将从上往下,循序渐进的介绍一系列相关.NET的概念,先从类型系统开始讲起,我将通过跨语言操作这个例子来逐渐引入一系列.NET的相关概念,这主要包括:CLS、...
基于 .NET 的一个全新的、好用的 PHP SDK + Runtime: PeachPie 来啦!
.NET 异步工作原理介绍。
引子 .NET 6 开始初步引入 PGO。PGO 即 Profile Guided Optimization,通过收集运行时信息来指导 JIT 如何优化代码,相比以前没有 PGO 时可以做更多以前难以
前言 2021/4/8 .NET 6 Preview 3 发布,这个版本的改进大多来自于底层,一起来看看都有什么新特性和改进吧。 库改进 新增值类型作为字典值时更快的处理方法 .NET 6 Previ
前言 开头防杠:.NET 的基础库、语言、运行时团队从来都是相互独立各自更新的,.NET 6 在基础库、运行时上同样做了非常多的改进,不过本文仅仅介绍语言部分。 距离上次介绍 C# 10 的特性已经有
直接使用 CIL - .NET 上的汇编语言编写 .NET Standard 类库
前言 不知不觉中,.NET Framework 已经更新到 4.8,.NET Core 也更新到了 3.0 版本。那么 .NET 的未来怎么样呢? 计划 2019 年 Build 大会上,微软宣布下一
本文带你穿越到未来一起看看未来的 C# 到底长什么样子。
前言 TypedocConverter 是我先前因帮助维护 monaco-editor-uwp 但苦于 monaco editor 的 API 实在太多,手写 C# 的类型绑定十分不划算而发起的一个项
前言 在 2021 年 3 月 11 日, .NET 6 Preview 2 发布,这次的改进主要涉及到 MAUI、新的基础库和运行时、JIT 改进。 .NET 6 正式版将会在 2021 年 11
前言 命名空间已经在 .NET 中使用了多年,一直追溯到 .NET Framework 1.1。它在 .NET 实施本身的数百个位置中使用,并且直接被成千上万个应用程序使用。在所有这些方面,它也是 C
.NET 上的统一跨平台 UI 框架来啦
使用 F# 手写一个 Typedoc 转 C# 代码生成器,方便一切 C# 项目对 TypeScript 项目的封装。
LINQ + SelectMany = Monad!
C# 10 主要特性一览
C# 的编译期反射终于来啦!
前言 2021 年 2 月 17 日微软发布了 .NET 6 的 Preview 1 版本,那么来看看都有什么新特性和改进吧,由于内容太多了因此只介绍一些较为重点的项目。ASP.NET Core 6
前言 有一个东西叫做鸭子类型,所谓鸭子类型就是,只要一个东西表现得像鸭子那么就能推出这玩意就是鸭子。 C 里面其实也暗藏了很多类似鸭子类型的东西,但是很多开发者并不知道,因此也就没法好好利用这些东西,
经过五年半的持续维护,Senparc.Weixin SDK 逐步丰满和完善,在升级的过程中,我们为基础库(Senparc.Weixin.dll)加入了许多通用的功能,例如加密/解密算法、通用缓存方法等