在ASP.NET Core中创建基于Quartz.NET托管服务轻松实现作业调度

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

简介-什么是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>();//添加我们的Jobservices.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 providerprivate 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实战”公众号第一时间获取更新。

首发地址:https://www.yuque.com/yilezhu/etg3w3/aspnetcore-hostservice-quartz-net-1

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

往期精彩回顾

【推荐】.NET Core开发实战视频课程 ★★★

.NET Core实战项目之CMS 第一章 入门篇-开篇及总体规划

【.NET Core微服务实战-统一身份认证】开篇及目录索引

Redis基本使用及百亿数据量中的使用技巧分享(附视频地址及观看指南)

.NET Core中的一个接口多种实现的依赖注入与动态选择看这篇就够了

10个小技巧助您写出高性能的ASP.NET Core代码

用abp vNext快速开发Quartz.NET定时任务管理界面

现身说法:实际业务出发分析百亿数据量下的多表查询优化

关于C#异步编程你应该了解的几点建议

C#异步编程看这篇就够了

给我好看 
您看此文用  · 秒,转发只需1秒呦~
好看你就点点我

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/310685.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

分享一些支持多租户的开源框架

如果你在开发sass应用程序&#xff0c;可以参考或者直接使用这些开源的框架。ASP.NET Boilerplate — Web应用程序框架ASP.NET Boilerplate是专门为新的现代Web应用程序设计的通用应用程序框架。它使用已经熟悉的工具并围绕它们实施最佳实践&#xff0c;以为您提供SOLID开发经验…

服务器操作系统用什么好,服务器操作系统一般用什么

服务器操作系统一般用什么 内容精选换一换客户的SAP系统部署在第三方云&#xff0c;服务器操作系统在华为云支持的列表内&#xff0c;数据库为任意数据库&#xff0c;同时还有如对象存储等的其他云服务。迁移到华为云后&#xff0c;数据库以及操作系统保持不变&#xff0c;系统…

一张大图了解ASP.NET Core 3.1 中的Authentication与Authorization

下面是一张ASP.NET Core 3.1 中关于Authentication与Authorization的主流程框线图&#xff0c;点击这里查看全图&#xff1a;https://johnnyqian.net/images/202004/aspnet-core-3.1-request-processing-pipeline.png重要组件一些重要的组件及其源码链接如下&#xff1a;Authen…

C++ class实现完全二叉树的顺序存储结构

代码如下: #include <iostream> using namespace std; const int maxsize 100; typedef char ElemType;class QbTree {public:void CreateBTree(int n);int vislchild(int i);int visrchild(int i);int visparent(int i);ElemType viselem(int i);void LevelOrder();p…

研发协同平台持续集成Jenkins作业设计演进

源宝导读&#xff1a;Jenkins作为一个开源的持续集成工具&#xff0c;被大家广泛使用。本文将分享&#xff0c;Jenkins在明源云研发协同平台中的运用&#xff0c;以及在其作业设计方面的演进历程。一、作业设计1.0起初&#xff0c;为了尽快推出研发协同平台v1.0&#xff0c;我们…

一切都要从华为云 CloudIDE 酷似 VS Code 说起

前不久&#xff0c;有个关于华为云 CloudIDE 的问题在知乎、朋友圈、微博等圈子引起了广泛的讨论&#xff0c;甚至上了知乎热榜。那么&#xff0c;背后的真实情况到底是如何的&#xff1f;且听韩老师娓娓道来。华为云 CloudIDE 酷似 VS Code&#xff1f;首先要明确一点&#xf…

C++ class实现双亲表示法

#include <iostream> using namespace std; typedef char ElemType; const int MAXSIZE 100;class TreeNode {friend class Tree; private:ElemType data;int parent; };class Tree { private:TreeNode elem[MAXSIZE];int n;//树中当前的节点个数 }Tree;

单元测试:如何编写可测试的代码及其重要性

原文来自互联网&#xff0c;由长沙DotNET技术社区编译。如译文侵犯您的署名权或版权&#xff0c;请联系小编&#xff0c;小编将在24小时内删除。限于译者的能力有限&#xff0c;个别语句翻译略显生硬&#xff0c;还请见谅。作者&#xff1a;谢尔盖科洛迪&#xff08;SERGEY KOL…

C++ class实现孩子表示法

代码如下: #include <iostream> using namespace std; typedef char ElemType; const int MAXSIZE 100;class link {friend class Node; private:int child;link *next; };class Node {friend class Tree; private:ElemType data;link *first; };class Tree { private:…

IdentityServer 部署踩坑记

IdentityServer 部署踩坑记Intro周末终于部署了 IdentityServer 以及 IdentityServerAdmin 项目&#xff0c;踩了几个坑&#xff0c;在此记录分享一下。部署架构项目是基于 IdentityServerAdmin 项目修改的&#xff0c;感谢作者的开源付出&#xff0c;有需要 IdentityServer 管…

.Net Core微服务架构技术栈的那些事

一、前言大家一直都在谈论微服务架构&#xff0c;园子里面也有很多关于微服务的文章&#xff0c;前几天也有一些园子的朋友问我微服务架构的一些技术&#xff0c;我这里就整理了微服务架构的技术栈路线图&#xff0c;这里就分享出来和大家一起探讨学习&#xff0c;同时让新手对…

C++ class实现Huffman树(完整代码)

代码如下: #include <iostream> #include <string> using namespace std; const unsigned int n 8;//字符数NUM&#xff0c;这里的字符数为8 const unsigned int m 2 * n - 1;//结点总数 const float MAX 1e8;class HTNode {friend class HuffmanTree; private…

二叉树的遍历(堆栈)

二叉树的遍历&#xff08;堆栈&#xff09; 如何理解用堆栈方式代替递归去遍历二叉树&#xff0c;关键点在于了解每个结点输出时的顺序&#xff0c;以及理解前序中序后序是如何遍历的&#xff0c;这点很重要&#xff0c;可以自己画一个树图&#xff0c;熟练写出遍历的结果 以…

ASP.NET Core分布式项目实战(第三方ClientCredential模式调用)--学习笔记

任务10&#xff1a;第三方ClientCredential模式调用创建一个控制台程序dotnet new console --name ThirdPartyDemo添加 Nuget 包&#xff1a;IdentityModel添加之后还原dotnet restoreClientusing System; using System.Net.Http; using System.Threading.Tasks; using Identit…

C++ class实现邻接矩阵存储的图(完整代码)

代码如下: #include <iostream> #include <queue> using namespace std; typedef int VertexType; typedef int EdgeType; const int MaxVertexNum 30;class MGraph { public:MGraph(){CreatGraph();};void CreatGraph();void Visit(int v);void BFS(int v);void…

二叉搜索树(创建,插入,删除):基础篇,适合新手观看。

1.1 二叉搜索树的插入 二叉搜索树的概念相信大家都很清楚&#xff0c;无非就是左小右大 创建二叉搜索树&#xff0c;其实就是多次调用二叉搜索树的插入方法&#xff0c;所以首先我们来讲讲如何插入节点到二叉搜索树里&#xff0c;假设一颗二叉搜索树如下&#xff0c;现在要插入…

.NET 的过去、现在和未来

作为一名使用了十多年.NET 开发平台的开发者来说&#xff0c;多年来笔者鲜少在各类博客媒体推广布道 .NET平台&#xff0c;但也默默地关注着 .NET 平台的发展变化&#xff0c;为每一次新技术的出现欢呼&#xff0c;为近年来 .NET 的沉默感到惋惜&#xff0c;也为开放开源的 .NE…

AVL树(平衡二叉树)讲解,入门篇,适合新手观看

1.1 概念 平衡二叉树就是为了让二叉搜索树的平均查找长度更短&#xff0c;时间复杂度更靠近logN,如果一个二叉搜索树不平衡了就会出现图1情况&#xff0c;完全变成一个数组&#xff0c;时间复杂度也变为了O(N)。 平衡因子&#xff1a;平衡因子就是针对于树中某一结点&#xff…

读源码,对开发者重要吗?

.NET 5.0 Peview 2发布4月2日&#xff0c;微软发布了.NET 5.0 Preview 2&#xff0c;带来功能和性能方面的改进。这一版本包括.NET 5.0.0 Preview 2与.NET SDK 5.0.100 Preview 2。.NET 5是.NET Framework和.NET Core的未来&#xff0c;最终将成为一个统一平台&#xff0c;.NET…

Redis和DB数据一致性解决方案

问题出现原因 并发时候无法保证读写的先后顺序&#xff0c;如果删掉了缓存还没来得及写库&#xff0c;另外一个县城就多来读取&#xff0c;发现缓存为空就去读取数据库并且写入缓存&#xff0c;这时候缓存中就是脏数据如果先写库&#xff0c;在删除缓存前&#xff0c;写库的线…