在这篇文章中,我将介绍如何使用ASP.NET Core托管服务运行Quartz.NET作业。这样的好处是我们可以在应用程序启动和停止时很方便的来控制我们的Job的运行状态。接下来我将演示如何创建一个简单的 IJob,一个自定义的 IJobFactory和一个在应用程序运行时就开始运行的QuartzHostedService。我还将介绍一些需要注意的问题,即在单例类中使用作用域服务。

作者:依乐祝

首发地址:https://www.cnblogs.com/yilezhu/p/12644208.html

参考英文地址:https://andrewlock.net/creating-a-quartz-net-hosted-service-with-asp-net-core/

简介-什么是Quartz.NET?

在开始介绍什么是Quartz.NET前先看一下下面这个图,这个图基本概括了Quartz.NET的所有核心内容。

注:此图为百度上获取,旨在学习交流使用,如有侵权,联系后删除。

以下来自他们的网站的描述:

Quartz.NET是功能齐全的开源作业调度系统,适用于从最小型的应用程序到大型企业系统。

对于许多ASP.NET开发人员来说它是首选,用作在计时器上以可靠、集群的方式运行后台任务的方法。将Quartz.NET与ASP.NET Core一起使用也非常相似-因为Quartz.NET支持.NET Standard 2.0,因此您可以轻松地在应用程序中使用它。

Quartz.NET有两个主要概念:

  • Job。这是您要按某个特定时间表运行的后台任务。
  • Scheduler。这是负责基于触发器,基于时间的计划运行作业。

ASP.NET Core通过托管服务对运行“后台任务”具有良好的支持。托管服务在ASP.NET Core应用程序启动时启动,并在应用程序生命周期内在后台运行。通过创建Quartz.NET托管服务,您可以使用标准ASP.NET Core应用程序在后台运行任务。

虽然可以创建“定时”后台服务(例如,每10分钟运行一次任务),但Quartz.NET提供了更为强大的解决方案。通过使用Cron触发器,您可以确保任务仅在一天的特定时间(例如,凌晨2:30)运行,或仅在特定的几天运行,或任意组合运行。它还允许您以集群方式运行应用程序的多个实例,以便在任何时候只能运行一个实例(高可用)。

在本文中,我将介绍创建Quartz.NET作业的基本知识并将其调度为在托管服务中的计时器上运行。

安装Quartz.NET

Quartz.NET是.NET Standard 2.0 NuGet软件包,因此非常易于安装在您的应用程序中。对于此测试,我创建了一个ASP.NET Core项目并选择了Empty模板。您可以使用dotnet add package Quartz来安装Quartz.NET软件包。这时候查看该项目的.csproj,应如下所示:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup> <ItemGroup>
<PackageReference Include="Quartz" Version="3.0.7" />
</ItemGroup> </Project>

创建一个IJob

对于我们正在安排的实际后台工作,我们将通过向注入的ILogger<>中写入“ hello world”来进行实现进而向控制台输出结果)。您必须实现包含单个异步Execute()方法的Quartz接口IJob。请注意,这里我们使用依赖注入将日志记录器注入到构造函数中。

using Microsoft.Extensions.Logging;
using Quartz;
using System;
using System.Threading.Tasks; namespace QuartzHostedService
{
[DisallowConcurrentExecution]
public class HelloWorldJob : IJob
{
private readonly ILogger<HelloWorldJob> _logger; public HelloWorldJob(ILogger<HelloWorldJob> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
} public Task Execute(IJobExecutionContext context)
{
_logger.LogInformation("Hello world by yilezhu at {0}!",DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
return Task.CompletedTask;
}
}
}

我还用[DisallowConcurrentExecution]属性装饰了该作业。该属性可防止Quartz.NET尝试同时运行同一作业

创建一个IJobFactory

接下来,我们需要告诉Quartz如何创建IJob的实例。默认情况下,Quartz将使用Activator.CreateInstance创建作业实例,从而有效的调用new HelloWorldJob()。不幸的是,由于我们使用构造函数注入,因此无法正常工作。相反,我们可以提供一个自定义的IJobFactory挂钩到ASP.NET Core依赖项注入容器(IServiceProvider)中:

using Microsoft.Extensions.DependencyInjection;
using Quartz;
using Quartz.Spi;
using System; namespace QuartzHostedService
{
public class SingletonJobFactory : IJobFactory
{
private readonly IServiceProvider _serviceProvider; public SingletonJobFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
} public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
return _serviceProvider.GetRequiredService(bundle.JobDetail.JobType) as IJob;
} public void ReturnJob(IJob job)
{ }
}
}

该工厂将一个IServiceProvider传入构造函数中,并实现IJobFactory接口。这里最重要的方法是NewJob()方法。在这个方法中工厂必须返回Quartz调度程序所请求的IJob。在此实现中,我们直接委托给IServiceProvider,并让DI容器找到所需的实例。由于GetRequiredService的非泛型版本返回的是一个对象,因此我们必须在末尾将其强制转换成IJob

ReturnJob方法是调度程序尝试返回(即销毁)工厂创建的作业的地方。不幸的是,使用内置的IServiceProvider没有这样做的机制。我们无法创建适合Quartz API所需的新的IScopeService,因此我们只能创建单例作业。

这个很重要。使用上述实现,仅对创建单例(或瞬态)的IJob实现是安全的。

配置作业

我在IJob这里仅显示一个实现,但是我们希望Quartz托管服务是适用于任何数量作业的通用实现。为了解决这个问题,我们创建了一个简单的DTO JobSchedule,用于定义给定作业类型的计时器计划:

using System;
using System.ComponentModel; namespace QuartzHostedService
{
/// <summary>
/// Job调度中间对象
/// </summary>
public class JobSchedule
{
public JobSchedule(Type jobType, string cronExpression)
{
this.JobType = jobType ?? throw new ArgumentNullException(nameof(jobType));
CronExpression = cronExpression ?? throw new ArgumentNullException(nameof(cronExpression));
}
/// <summary>
/// Job类型
/// </summary>
public Type JobType { get; private set; }
/// <summary>
/// Cron表达式
/// </summary>
public string CronExpression { get; private set; }
/// <summary>
/// Job状态
/// </summary>
public JobStatus JobStatu { get; set; } = JobStatus.Init;
} /// <summary>
/// Job运行状态
/// </summary>
public enum JobStatus:byte
{
[Description("初始化")]
Init=0,
[Description("运行中")]
Running=1,
[Description("调度中")]
Scheduling = 2,
[Description("已停止")]
Stopped = 3, }
}

这里的JobType是该作业的.NET类型(在我们的例子中就是HelloWorldJob),并且CronExpression是一个Quartz.NET的Cron表达。Cron表达式允许复杂的计时器调度,因此您可以设置下面复杂的规则,例如“每月5号和20号在上午8点至10点之间每半小时触发一次”。只需确保检查文档即可,因为并非所有操作系统所使用的Cron表达式都是可以互换的。

我们将作业添加到DI并在Startup.ConfigureServices()中配置其时间表:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Quartz;
using Quartz.Impl;
using Quartz.Spi; namespace QuartzHostedService
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
//添加Quartz服务
services.AddSingleton<IJobFactory, SingletonJobFactory>();
services.AddSingleton<ISchedulerFactory, StdSchedulerFactory>();
//添加我们的Job
services.AddSingleton<HelloWorldJob>();
services.AddSingleton(
new JobSchedule(jobType: typeof(HelloWorldJob), cronExpression: "0/5 * * * * ?")
);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
......
}
}
}

此代码将四个内容作为单例添加到DI容器:

  • SingletonJobFactory 是前面介绍的,用于创建作业实例。
  • 一个ISchedulerFactory的实现,使用内置的StdSchedulerFactory,它可以处理调度和管理作业
  • HelloWorldJob作业本身
  • 一个类型为HelloWorldJob,并包含一个五秒钟运行一次的Cron表达式的JobSchedule的实例化对象。

现在我们已经完成了大部分基础工作,只缺少一个将他们组合在一起的、QuartzHostedService了。

创建QuartzHostedService

QuartzHostedServiceIHostedService的一个实现,设置了Quartz调度程序,并且启用它并在后台运行。由于Quartz的设计,我们可以在IHostedService中直接实现它,而不是从基BackgroundService类派生更常见的方法。该服务的完整代码在下面列出,稍后我将对其进行详细描述。

using Microsoft.Extensions.Hosting;
using Quartz;
using Quartz.Spi;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks; namespace QuartzHostedService
{
public class QuartzHostedService : IHostedService
{
private readonly ISchedulerFactory _schedulerFactory;
private readonly IJobFactory _jobFactory;
private readonly IEnumerable<JobSchedule> _jobSchedules; public QuartzHostedService(ISchedulerFactory schedulerFactory, IJobFactory jobFactory, IEnumerable<JobSchedule> jobSchedules)
{
_schedulerFactory = schedulerFactory ?? throw new ArgumentNullException(nameof(schedulerFactory));
_jobFactory = jobFactory ?? throw new ArgumentNullException(nameof(jobFactory));
_jobSchedules = jobSchedules ?? throw new ArgumentNullException(nameof(jobSchedules));
}
public IScheduler Scheduler { get; set; } public async Task StartAsync(CancellationToken cancellationToken)
{
Scheduler = await _schedulerFactory.GetScheduler(cancellationToken);
Scheduler.JobFactory = _jobFactory;
foreach (var jobSchedule in _jobSchedules)
{
var job = CreateJob(jobSchedule);
var trigger = CreateTrigger(jobSchedule);
await Scheduler.ScheduleJob(job, trigger, cancellationToken);
jobSchedule.JobStatu = JobStatus.Scheduling;
}
await Scheduler.Start(cancellationToken);
foreach (var jobSchedule in _jobSchedules)
{
jobSchedule.JobStatu = JobStatus.Running;
}
} public async Task StopAsync(CancellationToken cancellationToken)
{
await Scheduler?.Shutdown(cancellationToken);
foreach (var jobSchedule in _jobSchedules)
{ jobSchedule.JobStatu = JobStatus.Stopped;
}
} private static IJobDetail CreateJob(JobSchedule schedule)
{
var jobType = schedule.JobType;
return JobBuilder
.Create(jobType)
.WithIdentity(jobType.FullName)
.WithDescription(jobType.Name)
.Build();
} private static ITrigger CreateTrigger(JobSchedule schedule)
{
return TriggerBuilder
.Create()
.WithIdentity($"{schedule.JobType.FullName}.trigger")
.WithCronSchedule(schedule.CronExpression)
.WithDescription(schedule.CronExpression)
.Build();
}
}
}

QuartzHostedService有三个依存依赖项:我们在Startup中配置的ISchedulerFactoryIJobFactory,还有一个就是IEnumerable<JobSchedule>。我们仅向DI容器中添加了一个JobSchedule对象(即HelloWorldJob),但是如果您在DI容器中注册更多的工作计划,它们将全部注入此处(当然,你也可以通过数据库来进行获取,再加以UI控制,是不是就实现了一个可视化的后台调度了呢?自己想象吧~)。

StartAsync方法将在应用程序启动时被调用,因此这里就是我们配置Quartz的地方。我们首先一个IScheduler的实例,将其分配给属性以供后面使用,然后将注入的JobFactory实例设置给调度程序:

 public async Task StartAsync(CancellationToken cancellationToken)
{
Scheduler = await _schedulerFactory.GetScheduler(cancellationToken);
Scheduler.JobFactory = _jobFactory;
...
}

接下来,我们循环注入作业计划,并为每一个作业使用在类的结尾处定义的CreateJobCreateTrigger辅助方法在创建一个Quartz的IJobDetailITrigger。如果您不喜欢这部分的工作方式,或者需要对配置进行更多控制,则可以通过按需扩展JobScheduleDTO 来轻松自定义它。

public async Task StartAsync(CancellationToken cancellationToken)
{
// ...
foreach (var jobSchedule in _jobSchedules)
{
var job = CreateJob(jobSchedule);
var trigger = CreateTrigger(jobSchedule);
await Scheduler.ScheduleJob(job, trigger, cancellationToken);
jobSchedule.JobStatu = JobStatus.Scheduling;
}
// ...
} private static IJobDetail CreateJob(JobSchedule schedule)
{
var jobType = schedule.JobType;
return JobBuilder
.Create(jobType)
.WithIdentity(jobType.FullName)
.WithDescription(jobType.Name)
.Build();
} private static ITrigger CreateTrigger(JobSchedule schedule)
{
return TriggerBuilder
.Create()
.WithIdentity($"{schedule.JobType.FullName}.trigger")
.WithCronSchedule(schedule.CronExpression)
.WithDescription(schedule.CronExpression)
.Build();
}

最后,一旦所有作业都被安排好,您就可以调用它的Scheduler.Start()来在后台实际开始Quartz.NET计划程序的处理。当应用程序关闭时,框架将调用StopAsync(),此时您可以调用Scheduler.Stop()以安全地关闭调度程序进程。

public async Task StopAsync(CancellationToken cancellationToken)
{
await Scheduler?.Shutdown(cancellationToken);
}

您可以使用AddHostedService()扩展方法在托管服务Startup.ConfigureServices中注入我们的后台服务:

public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddHostedService<QuartzHostedService>();
}

如果运行该应用程序,则应该看到每隔5秒运行一次后台任务并写入控制台中(或配置日志记录的任何地方)

在作业中使用作用域服务

这篇文章中描述的实现存在一个大问题:您只能创建Singleton或Transient作业。这意味着您不能使用注册为作用域服务的任何依赖项。例如,您将无法将EF Core的 DatabaseContext注入您的IJob实现中,因为您会遇到Captive Dependency问题。

解决这个问题也不是很难:您可以注入IServiceProvider并创建自己的作用域。例如,如果您需要在HelloWorldJob中使用作用域服务,则可以使用以下内容:

public class HelloWorldJob : IJob
{
// 注入DI provider
private readonly IServiceProvider _provider;
public HelloWorldJob( IServiceProvider provider)
{
_provider = provider;
} public Task Execute(IJobExecutionContext context)
{
// 创建一个新的作用域
using(var scope = _provider.CreateScope())
{
// 解析你的作用域服务
var service = scope.ServiceProvider.GetService<IScopedService>();
_logger.LogInformation("Hello world by yilezhu at {0}!",DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
} return Task.CompletedTask;
}
}

这样可以确保在每次运行作业时都创建一个新的作用域,因此您可以在IJob中检索(并处理)作用域服务。糟糕的是,这样的写法确实有些混乱。在下一篇文章中,我将展示另一种比较优雅的实现方式,它更简洁,有兴趣的可以关注下“DotNetCore实战”公众号第一时间获取更新。

总结

在这篇文章中,我介绍了Quartz.NET,并展示了如何使用它在ASP.NET Core中的IHostedService中来调度后台作业。这篇文章中显示的示例最适合单例或瞬时作业,这并不理想,因为使用作用域服务显得很笨拙。在下一篇文章中,我将展示另一种比较优雅的实现方式,它更简洁,并使得使用作用域服务更容易,有兴趣的可以关注下“DotNetCore实战”公众号第一时间获取更新。

在ASP.NET Core中创建基于Quartz.NET托管服务轻松实现作业调度的更多相关文章

  1. 在ASP.NET Core中创建自定义端点可视化图

    在上篇文章中,我为构建自定义端点可视化图奠定了基础,正如我在第一篇文章中展示的那样.该图显示了端点路由的不同部分:文字值,参数,动词约束和产生结果的端点: 在本文中,我将展示如何通过创建一个自定义的D ...

  2. Asp.Net Core中创建多DbContext并迁移到数据库

    在我们的项目中我们有时候需要在我们的项目中创建DbContext,而且这些DbContext之间有明显的界限,比如系统中两个DbContext一个是和整个数据库的权限相关的内容而另外一个DbConte ...

  3. 【半译】在ASP.NET Core中创建内部使用作用域服务的Quartz.NET宿主服务

    在我的上一篇文章中,我展示了如何使用ASP.NET Core创建Quartz.NET托管服务并使用它来按计划运行后台任务.不幸的是,由于Quartz.NET API的工作方式,在Quartz作业中使用 ...

  4. Windows IIS ASP.NET Core中创建和使用HTTPS自签名证书

    为什么要用Https就不说了. 第一步:创建自签名的证书.在Windows下开启PowerShell,将以下文字粘贴进去: # setup certificate properties includi ...

  5. ASP.NET Core中Middleware的使用

    https://www.cnblogs.com/shenba/p/6361311.html   ASP.NET 5中Middleware的基本用法 在ASP.NET 5里面引入了OWIN的概念,大致意 ...

  6. 重学ASP.NET Core 中的标记帮助程序

    标记帮助程序是什么 标记帮助程序使服务器端代码可以在 Razor 文件中参与创建和呈现 HTML 元素. 例如,内置的 ImageTagHelper 可以将版本号追加到图片名称.  每当图片发生变化时 ...

  7. 如何在ASP.NET Core 中使用IHttpClientFactory

    利用IHttpClientFactory可以无缝创建HttpClient实例,避免手动管理它们的生命周期. 当使用ASP.Net Core开发应用程序时,可能经常需要通过HttpClient调用Web ...

  8. 项目开发中的一些注意事项以及技巧总结 基于Repository模式设计项目架构—你可以参考的项目架构设计 Asp.Net Core中使用RSA加密 EF Core中的多对多映射如何实现? asp.net core下的如何给网站做安全设置 获取服务端https证书 Js异常捕获

    项目开发中的一些注意事项以及技巧总结   1.jquery采用ajax向后端请求时,MVC框架并不能返回View的数据,也就是一般我们使用View().PartialView()等,只能返回json以 ...

  9. C# 嵌入dll 动软代码生成器基础使用 系统缓存全解析 .NET开发中的事务处理大比拼 C#之数据类型学习 【基于EF Core的Code First模式的DotNetCore快速开发框架】完成对DB First代码生成的支持 基于EF Core的Code First模式的DotNetCore快速开发框架 【懒人有道】在asp.net core中实现程序集注入

    C# 嵌入dll   在很多时候我们在生成C#exe文件时,如果在工程里调用了dll文件时,那么如果不加以处理的话在生成的exe文件运行时需要连同这个dll一起转移,相比于一个单独干净的exe,这种形 ...

随机推荐

  1. Mac笔记本使用小道解答集

    如何设置Mac默认应用程序 https://www.jianshu.com/p/0f912e6c846c 苹果本安装微软雅黑 下载微软雅黑字体Mac版 解压.ttf 拖拽放入 我的电脑/资源库/fon ...

  2. Yuchuan_Linux_C编程之九目录操作相关函数

    一.整体大纲 二.相关函数 1. getcwd 函数作用:获取当前目录 头文件 #include <unistd.h> 函数原型 char *getcwd(char *buf, size_ ...

  3. JAVA有关位运算的全套梳理

    一.在计算机中数据是如何进行计算的? 1.1:java中的byte型数据取值范围 我们最开始学习java的时候知道,byte类型的数据占了8个bit位,每个位上或0或1,左边第一位表示符号位,符号位如 ...

  4. Webpack和Gulp,Webpack和Gulp的基本区别:

    Gulp和Webpack的基本区别: gulp可以进行js,html,css,img的压缩打包,是自动化构建工具,可以将多个js文件或是css压缩成一个文件,并且可以压缩为一行,以此来减少文件体积,加 ...

  5. go结构体继承组合和匿名字段

    1.结构体方法 go不是纯粹的面向对象的,在go里面函数是一等公民,但是go也有结构体实现类似java一样类的功能来提供抽象.结构体的方法分为值方法和指针方法,前者在方法中做的改变不会改变调用的实例对 ...

  6. for循环结合range使用方法

    range概念:表示一个数据范围 基本的语法格式:range(开始数据,结束数据(不包括结束数据),步长) 记住一个公式:下一个数据=开始数据+步长 步长:表示的是数据前后的间隔 OK,基本的概念和语 ...

  7. JVM01——JVM内存区域的构成

    从本文开始将为各位带来JVM方面的知识点,关注我的公众号「Java面典」了解更多Java相关知识点. JVM内存主要分为三部分线程私有(Thread Local).线程共享(Thread Shared ...

  8. [转载]Linux服务器丢包故障的解决思路及引申的TCP/IP协议栈理论

    Linux服务器丢包故障的解决思路及引申的TCP/IP协议栈理论 转载至:https://www.sdnlab.com/17530.html 我们使用Linux作为服务器操作系统时,为了达到高并发处理 ...

  9. javaScript 基础知识汇总 (十四)

    1.回调 什么是回调? 个人理解,让函数有序的执行. 示例: function loadScript(src,callback){ let script = document.createElemen ...

  10. LeetCode-使数组唯一的最小增量

    题目描述: 给定整数数组 A,每次 move 操作将会选择任意 A[i],并将其递增 1. 返回使 A 中的每个值都是唯一的最少操作次数. 示例: 输入:[1,2,2] 输出:1 解释:经过一次 mo ...