在ASP.NET Core中創建基於Quartz.NET托管服務輕鬆實現作業調度

来源:https://www.cnblogs.com/yilezhu/archive/2020/04/07/12644208.html
-Advertisement-
Play Games

在這篇文章中,我將介紹如何使用 "ASP.NET Core托管服務" 運行Quartz.NET作業。這樣的好處是我們可以在應用程式啟動和停止時很方便的來控制我們的Job的運行狀態。接下來我將演示如何創建一個簡單的 ,一個自定義的 和一個在應用程式運行時就開始運行的 。我還將介紹一些需要註意的問題,即 ...


在這篇文章中,我將介紹如何使用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

以下來自他們的網站的描述:

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秒運行一次後臺任務並寫入控制臺中(或配置日誌記錄的任何地方)

image-20200406151153107

在作業中使用作用域服務

這篇文章中描述的實現存在一個大問題:您只能創建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實戰”公眾號第一時間獲取更新。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在帶鍵盤滑鼠的電腦上編寫應用於觸屏電腦的項目,為了能輸入中文、英文、數字等各種庸人自擾。 一、自己畫了個鍵盤 為了實現能輸入中文,還簡單編寫了個拼音輸入法,各種折騰,始終不是很舒服。最後客戶要求手寫輸入中文,於是就完全放棄了。 二、折騰TabTip.exe win10的虛擬鍵盤是一個程式,即c:\P ...
  • 五、C#流程式控制制 5.1.if語句 1)結構 if(條件判斷表達式) { func1 }else { func2 } 5.2.switch語句 1)結構 switch(表達式) { case 常量表達式:條件語句;break; case 常量表達式:條件語句;break; case 常量表達式:條件 ...
  • IdentityServer 部署踩坑記 Intro 周末終於部署了 以及 項目,踩了幾個坑,在此記錄分享一下。 部署架構 項目是基於 "IdentityServerAdmin" 項目修改的,感謝作者的開源付出,有需要 IdentityServer 管理需求的可以關註一下,覺得好用的可以給個 sta ...
  • Insus.NET有在angularjs中把ng-repeat顯示數據的同時又讓其能更新數據。 html代碼如下: 當用戶點擊更新時,能獲取按鈕當前行的更新數據進行更新。 ...
  • @[toc] 開發 web api 的時候,寫文檔是個痛苦的事情,而沒有文檔別人就不知道怎麼調用,所以又不得不寫。 swagger 可以自動生成介面文檔,並測試介面,極大的解放了程式員的生產力。 1 安裝 通過 NuGet 安裝 Swashbuckle。 安裝完成後,App_Start 文件夾下會多 ...
  • 1.先上效果圖: 2.1t提示框界面。 主視窗界面沒什麼內容,就放了一個觸發按鈕。先繪製通知視窗(一個關閉按鈕,倆個文本控制項),可以設置下ResizeMode="NoResize" WindowStyle="None" Topmost="True", 去掉視窗標題,並使提示視窗始終處於最上層。 <B ...
  • 前言 本文介紹另一種學習ABP框架的方法,該方法為正面硬鋼學習法。。。 我們不去官網下載模板,直接引用DLL,直接使用。 WebApi項目創建 首先創建一個WebApi項目,結構如下。 然後Nuget搜索ABP,安裝ABP框架。(我這裡安裝的是5.1.0,因為最高版本安裝不上) 在安裝ABP前先檢查 ...
  • 1. 問題 好像很少人會遇到這種需求。假設有一個文件夾,用戶有幾乎所有許可權,但沒有刪除的許可權,如下圖所示: 這時候使用SaveFileDialog在這個文件夾里創建文件居然會報如下錯誤: 這哪裡是網路位置了,我又哪裡去找個管理員?更奇怪的是,雖然報錯了,但文件還是會創建出來,不過這是個空文件。不僅W ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...