在ASP.NET Core中创建内部使用Scoped服务的Quartz.NET宿主服务

栏目: IT技术 · 发布时间: 4年前

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

在我的上一篇文章中,我展示了如何使用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 方法,但是你可以选择最适合您的方法

总结

在本文中,我展示了如何创建中间层 IJob ,该中间层 QuartzJobRunner 在调度程序需要执行作业时创建。该运行程序负责创建一个DI范围,实例化请求的作业并执行它,因此最终 IJob 实现可以在其构造函数中使用作用域中的服务。您也可以使用此方法在 QuartzJobRunner 中配置基本管道,尽管对此有更好的解决方案,例如装饰器或 MediatR库中的 行为。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

51单片机应用从零开始

51单片机应用从零开始

杨欣、王玉凤、刘湘黔 / 清华大学 / 2008-1 / 39.80元

《51单片机应用与实践丛书•51单片机应用从零开始》在分析初学者认知规律的基础上,结合国内重点大学一线教师的教学经验以及借鉴国外经典教材的写作手法,对51单片机的应用基础知识进行系统而翔实的介绍。读者学习每一章之后,"实例点拨"环节除了可以巩固所学的内容外,还开辟了单片机应用的视野;再加上"器件介绍"环节,又充实了对单片机从基础到应用所需要的知识。8051单片机不仅是国内用得最多的单片机之一,同时......一起来看看 《51单片机应用从零开始》 这本书的介绍吧!

URL 编码/解码
URL 编码/解码

URL 编码/解码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具