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

查看:133
本文介绍了如何在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软件包,因为我想要一个轻量级的调度程序在我的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:00
  • SendAdminEmails 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.

  • 例如,如果在星期五的9:00,则该进程已关闭,因为由于不活动,IIS已将其卸载了几个小时,因此直到再次启动该进程之前,都不会发送任何管理电子邮件.为避免这种情况,请将IIS配置为最大程度地减少卸载/回收(请参阅此答案).
    • 根据我的经验,以上配置仍不能100%保证IIS永远不会卸载应用程序.为了100%保证您的进程正常运行,您可以设置一个命令,该命令定期将请求发送到您的应用程序,从而使其保持活动状态.
    • If for example, on Friday 9:00 the process is down, because several hours earlier it was unloaded by IIS due to inactivity - no admin emails will be sent until the process is up again. To avoid that, configure IIS to minimize unloads/recyclings (see this answer).
      • From my experience, the above configuration still doesn't give a 100% guarantee that IIS will never unload the application. For 100% guarantee that your process is up, you can setup a command that periodically sends requests to your application, and thus keeps it alive.

      尽管上面列出了问题,但我可以想到将这些电子邮件作业托管在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中使用名为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<bool>.两个作业类都应获取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应用程序项目时会被构架. 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接口.根据此帖子

      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天全站免登陆