C# 最詳細 ASP.NET Core 6 使用 Quartz.net 動態更新 排程

Quartz.NET 是一個開源的任務管理系統,能夠延遲執行、定期執行任務,簡單來說,如果你需要建立排程來執行任務,Quartz.net 非常適合你!

Quartz.NET 的核心包含: Scheduler、Job、Trigger、JobDetail、JobDataMap 官方說明解釋的不夠詳細,筆者花了很多時間才搞懂。 在使用前,至少需要先了解以下幾點,
  • Job: 定義要執行的工作
  • JobDetail: 儲存一個或多個 Job
  • Trigger: 定義什麼時候要執行 JobDetail
註:在 Trigger 有時候是直接傳入 JobKey ,看起沒有用到 JobDetail ,但其實內部會依據 Job 自動建立 JobDetail

安裝

在 ASP.NET Core 6 上使用 Quartz,最少只需要安裝 Quartz 即可,不過如果想要使用依賴注入和配合 ASP.NET Core 的生命週期,則還需要安裝 Quartz.Extensions.Hosting ,在本篇示範中請兩個都安裝。

安裝 QuartzQuartz.Extensions.Hosting
    
dotnet add package Quartz
dotnet add package Quartz.Extensions.Hosting
    

建立 Job

Job 就是任務,在類別中繼承 IJob 介面(interface),實作 Execute 即可,執行 Job 時 Execute 方法就會被呼叫。

建立一個 HelloWorldJob ,執行時會顯示當前時間:
    
public class HelloWorldJob : IJob
{
    public Task Execute(IJobExecutionContext context)
    {
        Console.WriteLine($"Hello World! {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
        return Task.CompletedTask;
    }
}
    

宣告任務

    
builder.Services.AddQuartzHostedService(options => options.WaitForJobsToComplete = true);


builder.Services.AddQuartz(quartz =>
{
    quartz.UseMicrosoftDependencyInjectionJobFactory();

    // 建立 Job
    var jobKey = new JobKey("HelloWord", "HelloWordGroup");
    quartz.AddJob<HelloWorldJob>(opts =>
    {
        opts.WithIdentity(jobKey);
        opts.StoreDurably();
    });

    // 建立觸發器,自動執行 Job
    quartz.AddTrigger(opts =>
    {
        opts.ForJob(jobKey);
        opts.WithIdentity("HelloWordTrigger", "HelloWordGroup");
        opts.WithSimpleSchedule(x => x.WithIntervalInSeconds(1).RepeatForever());
    });
});
    

在上方 17-21 行的地方建立了觸發器,每秒鐘就會觸發一次,一直執行。

RepeatForever 是一直重複執行,也可以替換為 WithRepeatCount(10) 執行 10 次,或自訂次數

WithIntervalInSeconds 是使用秒來設定週期,也可以使用 WithIntervalInMinutes 指定分鐘、WithIntervalInHours 指定小時等,或是不使用 WithSimpleSchedule ,直接使用 WithCronSchedule 透過 cron 指定週期
    
.WithCronSchedule("* * * * * ?")); // 每秒重複
    

查看所有 Job

    
[Route("api")]
public class JobController : ControllerBase
{
    private readonly ISchedulerFactory _schedulerFactory;

    public JobController(ISchedulerFactory schedulerFactory)
    {
        _schedulerFactory = schedulerFactory;
    }

    /// <summary>
    /// 顯示所有 Job
    /// </summary>
    [HttpGet("jobs")]
    public async Task<IActionResult> GetScheduledJobs()
    {
        var scheduler = await _schedulerFactory.GetScheduler();

        List<Dictionary<string, string>> jobs = new();

        var allJobKeys = await scheduler.GetJobKeys(GroupMatcher<JobKey>.AnyGroup());
        foreach (JobKey? jobKey in allJobKeys)
        {
            IJobDetail? jobDetail = await scheduler.GetJobDetail(jobKey);

            jobs.Add(new Dictionary<string, string>
            {
                { "name", jobDetail.Key.Name },
                { "group", jobDetail.Key.Group },
                { "type", jobDetail.JobType.ToString() },
                { "description", jobDetail.Description ?? string.Empty },
            });
        }

        return Ok(jobs);
    }
}
    

註: 不使用 dynamic 或 object 是因為 System.Text.Json 不能很方便的處理 範例輸出:
    
[
  {
    "name": "HelloWord",
    "group": "HelloWordGroup",
    "type": "WebApplicationDynamicQuartz.Jobs.HelloWorldJob",
    "description": ""
  }
]
    

查看所有 Trigger

    
[Route("api")]
public class TriggerController : ControllerBase
{
    private readonly ISchedulerFactory _schedulerFactory;

    public TriggerController(ISchedulerFactory schedulerFactory)
    {
        _schedulerFactory = schedulerFactory;
    }

    [HttpGet("triggers")]
    public async Task<IActionResult> GetScheduledTriggers()
    {
        var scheduler = await _schedulerFactory.GetScheduler();

        List<Dictionary<string, string>> triggers = new();

        var allTriggerKeys = await scheduler.GetTriggerKeys(GroupMatcher<TriggerKey>.AnyGroup());
        foreach (var triggerKey in allTriggerKeys)
        {
            var trigger = await scheduler.GetTrigger(triggerKey);

            TriggerState triggerState = await scheduler.GetTriggerState(trigger.Key);
            triggers.Add(new Dictionary<string, string>
            {
                { "name", trigger.Key.Name },
                { "group", trigger.Key.Group },
                { "type", trigger.GetType().ToString() },
                { "status", triggerState.ToString() },
                { "description", trigger.Description ?? string.Empty },
                { "startTimeUtc", trigger.StartTimeUtc.ToString() },
                { "endTimeUtc", trigger.EndTimeUtc.ToString() ?? string.Empty },
                { "nextFireTimeUtc", trigger.GetNextFireTimeUtc().ToString() ?? string.Empty },
                { "previousFireTimeUtc", trigger.GetPreviousFireTimeUtc().ToString() ?? string.Empty },
            });
        }

        return Ok(triggers);
    }
}
    

範例輸出:
    
[
  {
    "name": "HelloWordTrigger",
    "group": "HelloWordGroup",
    "type": "Quartz.Impl.Triggers.SimpleTriggerImpl",
    "status": "Normal",
    "description": "",
    "startTimeUtc": "2023/3/19 下午 03:27:55 +00:00",
    "endTimeUtc": "",
    "nextFireTimeUtc": "2023/3/19 下午 03:28:00 +00:00",
    "previousFireTimeUtc": "2023/3/19 下午 03:27:55 +00:00"
  }
]
    

TriggerState 內容:
  • Normal: 普通,等待下次執行
  • Paused: 暫停
  • Complete: 執行完畢
  • Error: 錯誤,不會再執行
  • Blocked: 鎖定
  • None

依照 Job 建立新的 Trigger

    
string jobName = "HelloWord";
string jobGroup = "HelloWordGroup";
string cron = "* * * * * ?"; // 每秒執行
    
var scheduler = await _schedulerFactory.GetScheduler();
var jobKey = new JobKey(jobName, jobGroup);
var jobExists = await scheduler.CheckExists(jobKey);
if (!jobExists)
{
    Console.WriteLine($"Job {jobName} does not exist");
    return;
}

var triggerKey = new TriggerKey($"{jobName}Trigger", jobGroup);
var triggerExists = await scheduler.CheckExists(triggerKey);
if (triggerExists)
{
    Console.WriteLine($"Trigger {triggerKey} already exists");
    return;
}

var trigger = TriggerBuilder.Create()
    .WithIdentity(triggerKey)
    .StartNow()
    .WithCronSchedule(cron)
    .ForJob(jobKey)
    .Build();

await scheduler.ScheduleJob(trigger);
    

依照 Job 移除 Trigger

    
string jobName = "HelloWord";
string jobGroup = "HelloWordGroup";

var scheduler = await _schedulerFactory.GetScheduler();
var jobKey = new JobKey(jobName, jobGroup);
var jobExists = await scheduler.CheckExists(jobKey);
if (!jobExists)
{
    Console.WriteLine($"Job {jobName} does not exist");
    return;
}

var triggerKey = new TriggerKey($"{jobName}Trigger", jobGroup);
var triggerExists = await scheduler.CheckExists(triggerKey);
if (!triggerExists)
{
    Console.WriteLine($"Trigger {triggerKey} does not exist");
    return;
}

await scheduler.UnscheduleJob(triggerKey);

Console.WriteLine($"Trigger {triggerKey} removed");
    



參考資料:
Quartz.NET

留言

張貼留言

如果有任何問題、建議、想說的話或文章題目推薦,都歡迎留言或來信: a@ruyut.com