如何在 ASP.NET Core 中启动 Quartz? [英] How to start Quartz in ASP.NET Core?

查看:34
本文介绍了如何在 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 包,因为我想要一个轻量级的调度程序在没有单独 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:00
  • SendAdminEmails 每周周四 09:00、周五 9:00 运行
  • SendUserEmails to run every week on Mondays 17:00,Tuesdays 17:00 & Wednesdays 17:00
  • SendAdminEmails 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. 在上面的例子中,SendUserEmailsJobSendAdminEmailsJob 是实现 IJob 的类.IJob 接口与 IMyEmailService 略有不同,因为它返回 void Task 而不是 Task.两个作业类都应该将 IMyEmailService 作为依赖项(可能是构造函数注入).

    Note 1. In the above example, 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).

    注意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 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);
    }
    

    何时何地启动和停止调度程序

    在 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. 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 中所做的那样.但由于我们也必须正确处理进程关闭,我更喜欢以更一致的方式挂钩启动和关闭代码.

    The simplest thing to do is just put a call to 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 Application项目时搭建的.Startup 类如下所示:

    refers to a class named 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 应该挂在哪里.

    It is clear that a call to QuartzStartup.Start should be inserted in one of methods in the Startup class. The question is, where QuartzStartup.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 with IApplicationLifetime. Bingo. An instance of IApplicationLifetime can be injected into Startup.Configure method through a parameter.

    为了一致性,我将 QuartzStartup.StartQuartzStartup.Stop 都挂接到 IApplicationLifetime:

    For consistency, I will hook both 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 将阻塞,直到注册的回调完成.

    Note that I have extended the signature of the Configure method with an additional IApplicationLifetime 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 invoke IApplicationLifetime.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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆