在我的上一篇文章中,我展示了如何使用ASP.NET Core创建Quartz.NET托管服务并使用它来按计划运行后台任务。不幸的是,由于Quartz.NET API的工作方式,在Quartz作业中使用Scoped依赖项注入服务有些麻烦。说明下这篇文章部分采用机翻。

作者:依乐祝

译文地址:https://www.cnblogs.com/yilezhu/p/12757411.html

原文地址:https://andrewlock.net/using-scoped-services-inside-a-quartz-net-hosted-service-with-asp-net-core/

在这篇文章中,我将展示一种简化工作中使用Scoped服务的方法。您可以使用相同的方法来管理EF Core的工作单元模式和其他面向切面的模型。

这篇文章是上篇文章引申出来的,因此,如果您还没有阅读的话,建议您先阅读上篇文章。

回顾-自定义JobFactory和单例的IJob

在上篇博客的最后,我们有一个实现了IJob接口并向控制台简单输出信息的HelloWorldJob

public class HelloWorldJob : IJob
{
private readonly ILogger<HelloWorldJob> _logger;
public HelloWorldJob(ILogger<HelloWorldJob> logger)
{
_logger = logger;
}
public Task Execute(IJobExecutionContext context)
{
_logger.LogInformation("Hello world!");
return Task.CompletedTask;
}
}

我们还有一个IJobFactory的实现,以便我们在需要时从DI容器中检索作业的实例:

public class SingletonJobFactory : IJobFactory
{
private readonly IServiceProvider _serviceProvider;
public SingletonJobFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
return _serviceProvider.GetRequiredService(bundle.JobDetail.JobType) as IJob;
}
public void ReturnJob(IJob job) { }
}

这些服务都在Startup.ConfigureServices()中以单例形式注册:

services.AddSingleton<IJobFactory, SingletonJobFactory>();
services.AddSingleton<HelloWorldJob>();

对于这个非常基本的示例来说,这很好,但是如果您需要在IJob内部使用一些范围服务呢?例如,也许您需要使用EF Core DbContext遍历所有客户,并向他们发送电子邮件,并更新客户记录。我们假设这个任务为EmailReminderJob

权宜之计

我在上一篇文章中展示的解决方案是将IServiceProvider注入到您的IJob的文档中,手动创建一个范围,并从中检索必要的服务。例如:

public class EmailReminderJob : IJob
{
private readonly IServiceProvider _provider;
public EmailReminderJob( IServiceProvider provider)
{
_provider = provider;
}
public Task Execute(IJobExecutionContext context)
{
using(var scope = _provider.CreateScope())
{
var dbContext = scope.ServiceProvider.GetService<AppDbContext>();
var emailSender = scope.ServiceProvider.GetService<IEmailSender>();
// fetch customers, send email, update DB
}
return Task.CompletedTask;
}
}

在许多情况下,这种方法绝对可以。如果不是将实现直接放在工作内部(如我上面所做的那样),而是使用中介者模式来处理诸如工作单元或消息分发之类的跨领域问题,则尤其如此。

如果不是这种情况,您可能会受益于创建一个可以为您管理这些工作的帮助类。

QuartzJobRunner

要解决这些问题,您可以创建一个IJob的“中间” 实现,这里我们命名为QuartzJobRunner,该实现位于IJobFactory和要运行的IJob之间。我将很快介绍作业实现,但是首先让我们更新现有的IJobFactory实现以无论请求哪个作业,始终返回QuartzJobRunner的实例,:

using Microsoft.Extensions.DependencyInjection;
using Quartz;
using Quartz.Spi;
using System;
public class JobFactory : IJobFactory
{
private readonly IServiceProvider _serviceProvider;
public JobFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
return _serviceProvider.GetRequiredService<QuartzJobRunner>();
}
public void ReturnJob(IJob job) { }
}

如您所见,该NewJob()方法始终返回QuartzJobRunner的实例。我们将在Startup.ConfigureServices()中将QuartzJobRunner注册为单例模式,因此我们不必担心它没有被明确释放。

services.AddSingleton<QuartzJobRunner>();

我们将在QuartzJobRunner中创建实际所需的IJob实例。QuartzJobRunner中的job会创建范围,实例化IJob的请求并执行它:

using Microsoft.Extensions.DependencyInjection;
using Quartz;
using System;
using System.Threading.Tasks;
public class QuartzJobRunner : IJob
{
private readonly IServiceProvider _serviceProvider;
public QuartzJobRunner(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task Execute(IJobExecutionContext context)
{
using (var scope = _serviceProvider.CreateScope())
{
var jobType = context.JobDetail.JobType;
var job = scope.ServiceProvider.GetRequiredService(jobType) as IJob;
await job.Execute(context);
}
}
}

在这一点上,您可能想知道,通过添加这个额外的间接层,我们获得了什么好处?主要有以下两个主要优点:

  • 我们可以将EmailReminderJob注册为范围服务,并直接将任何依赖项注入其构造函数中
  • 我们可以将其他横切关注点转移到QuartzJobRunner类中。

作业可以直接使用作用域服务

由于作业实例是从IServiceProvder作用域中解析来的,因此您可以在作业实现的构造函数中安全地使用作用域服务。这使的EmailReminderJob的实现更加清晰,并遵循构造函数注入的典型模式。如果您不熟悉DI范围界定问题,则可能很难理解它们,因此任何对您不利的事情在我看来都是一个好主意:

[DisallowConcurrentExecution]
public class EmailReminderJob : IJob
{
private readonly AppDbContext _dbContext;
private readonly IEmailSender _emailSender;
public EmailReminderJob(AppDbContext dbContext, IEmailSender emailSender)
{
_dbContext = dbContext;
_emailSender = emailSender;
}
public Task Execute(IJobExecutionContext context)
{
// fetch customers, send email, update DB
return Task.CompletedTask;
}
}

这些IJob的实现可以使用以下任何生存期(作用域或瞬态)来在Startup.ConfigureServices()中注册(JobSchedule仍然可以是单例):

services.AddScoped<EmailReminderJob>();
services.AddSingleton(new JobSchedule(
jobType: typeof(EmailReminderJob),
cronExpression: "0 0 12 * * ?")); // every day at noon

QuartzJobRunner可以处理横切关注点

QuartzJobRunner处理正在执行的IJob的整个生命周期:它从容器中获取,执行并释放它(在释放范围时)。因此,它很适合处理其他跨领域问题。

例如,假设您有一个需要更新数据库并将事件发送到消息总线的服务。您可以在每个单独的IJob实现中处理所有这些问题,也可以将跨领域的“提交更改”和“调度消息”操作移到QuartzJobRunner中。

这个例子显然是非常基础的。如果这里的代码适合您,我建议您观看吉米·博加德(Jimmy Bogard)的“六小段失败线”演讲,其中描述了一些问题!

public class QuartzJobRunner : IJob
{
private readonly IServiceProvider _serviceProvider;
public QuartzJobRunner(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task Execute(IJobExecutionContext context)
{
using (var scope = _serviceProvider.CreateScope())
{
var jobType = context.JobDetail.JobType;
var job = scope.ServiceProvider.GetRequiredService(jobType) as IJob;
var dbContext = _serviceProvider.GetRequiredService<AppDbContext>();
var messageBus = _serviceProvider.GetRequiredService<IBus>();
await job.Execute(context);
// job completed, save dbContext changes
await dbContext.SaveChangesAsync();
// db transaction succeeded, send messages
await messageBus.DispatchAsync();
}
}
}

这里的QuartzJobRunner实现与上一个非常相似,但是在执行的我们请求的IJob之前,我们从DI容器中解析了DbContext和消息总线服务。当作业成功执行后(即未抛出异常),我们将所有未提交的更改保存在中DbContext,并在消息总线上调度事件。

将这些方法移到QuartzJobRunner中应该可以减少IJob实现中的重复代码,并且可以更容易地移到更正式的管道和其他模式(如果您希望以后这样做的话)。

可替代解决方案

我喜欢本文中显示的方法(使用中间QuartzJobRunner类),主要有两个原因:

  • 您的其他IJob实现不需要任何有关创建作用域的基础结构的知识,只需完成标准构造函数注入即可
  • IJobFactory中不需要做做任何特殊处理工作。该QuartzJobRunner通过创建和处理作用域隐式地处理这个问题。

但是,此处显示的方法并不是在工作中使用范围服务的唯一方法。马修·阿伯特(Matthew Abbot) 在这个文章中演示了一种方法,该方法旨在以正确处理运行后的作业的方式实现IJobFactory。它有点笨拙,因为你必须匹配接口API,但可以说它更接近你应该实现它的方式!我个人认为我会坚持使用这种QuartzJobRunner方法,但是你可以选择最适合您的方法

【半译】在ASP.NET Core中创建内部使用作用域服务的Quartz.NET宿主服务的更多相关文章

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

    在这篇文章中,我将介绍如何使用ASP.NET Core托管服务运行Quartz.NET作业.这样的好处是我们可以在应用程序启动和停止时很方便的来控制我们的Job的运行状态.接下来我将演示如何创建一个简 ...

  2. [译]在Asp.Net Core 中使用外部登陆(google、微博...)

    原文出自Rui Figueiredo的博文<External Login Providers in ASP.NET Core> 摘要:本文主要介绍了使用外部登陆提供程序登陆的流程,以及身份 ...

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

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

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

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

  5. ASP.NET Core 中的SEO优化(1):中间件实现服务端静态化缓存

    分享 最近在公司成功落地了一个用ASP.NET Core 开发前台的CMS项目,虽然对于表层的开发是兼容MVC5的,但是作为爱好者当然要用尽量多的ASP.NET Core新功能了. 背景 在项目开发的 ...

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

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

  7. Asp.net core中的依赖注入

    使用服务 在Asp.net core的Controller中,可以通过如下两种方式获取系统注入的服务: 构造函数 可以直接在构造函数中传入所依赖的服务,这是非常常见的DI注入方式. public Va ...

  8. ASP.NET Core 中的SEO优化(3):自定义路由匹配和生成

    前言 前两篇文章主要总结了CMS系统两个技术点在ASP.NET Core中的应用: <ASP.NET Core 中的SEO优化(1):中间件实现服务端静态化缓存> <ASP.NET ...

  9. ASP.NET Core 中的SEO优化(2):中间件中渲染Razor视图

    前言 上一篇文章<ASP.NET Core 中的SEO优化(1):中间件实现服务端静态化缓存>中介绍了中间件的使用方法.以及使用中间件实现服务端静态化缓存的功能.本系列文章的这些技巧都是我 ...

随机推荐

  1. HDU 1847-Good Luck in CET-4 Everybody!-博弈SG函数模板

    Problem Description 大学英语四级考试就要来临了,你是不是在紧张的复习?也许紧张得连短学期的ACM都没工夫练习了,反正我知道的Kiki和Cici都是如此.当然,作为在考场浸润了十几载 ...

  2. POJ 3070 Fibonacci矩阵快速幂 --斐波那契

    题意: 求出斐波那契数列的第n项的后四位数字 思路:f[n]=f[n-1]+f[n-2]递推可得二阶行列式,求第n项则是这个矩阵的n次幂,所以有矩阵快速幂模板,二阶行列式相乘, sum[ i ] [ ...

  3. Ali_Cloud++:安装 RabbitMQ安装及环境配置

    注意事项:rabbitMA版本和erlang并不是同步更新的,会出现版本不匹配,安装不了. 两都版本对应   参考官网文档    其它下载地址 1):Erlang安装 (因为是erlant语言编写的, ...

  4. iOS 优化实例

    一.接口请求优化 在工程项目中,多个一级界面包含状态,如:服务入口的动态配置,未读消息数量,图片文字等,因此产品设计要每次切换 tab 时都请求数据,及时的更新页面状态.在实际开发中,频繁的调用接口, ...

  5. RFID 有源,半源和无源的区别

    RFID电子标签是由标签.解读器和数据传输和处理系统组成.内存带有天线的芯片,芯片中存储有能够识别目标的信息,主要作用都是为了识别货物.(更具体的自行搜索,本文单独讲三种的区别) RFID分为三种 有 ...

  6. 一文读懂什么是CA证书

    Normal 0 7.8 磅 0 2 false false false EN-US ZH-CN X-NONE /* Style Definitions */ table.MsoNormalTable ...

  7. 面试中常问的五种IO模型和BIO,NIO,AIO

    一,五种IO模型: 一个IO操作可以分为两个步骤:发起IO请求和实际的IO操作例如:1.操作系统的一次写操作分为两步:第一步,将数据从用户空间拷贝到系统空间:第二步,从系统空间往网卡写.2.一次读操作 ...

  8. Vertica的这些事(八)——-Vertica-管理

    1.版本信息 dbadmin=> SELECT version(); version ------------------------------------ Vertica Analytic ...

  9. 关于C#三层架构中的“分页”功能

    新手上路,请多指教! 今天将分页功能实现了,要特别感谢坐在前面的何同学的指点,不胜感谢!功能的实现采用了三层架构的方式实现该功能,简述如下: 界面: DAL层有两个方法:“当前所在页”和“总页数” 这 ...

  10. block 的内存结构衍生出来的面试题

    今天在群里看到大佬们在讨论一个面试题,问如下代码在 32bit 和 64bit 系统上分别报什么错误: #import <Foundation/Foundation.h> int main ...