Quartz.NET 工作排程指南
用途:.NET 的企業級工作排程框架,支援 Cron 表達式、持久化排程、叢集
NuGet:
Quartz(核心)/Quartz.Extensions.Hosting(DI 整合)授權:Apache-2.0
支援:.NET 6+、.NET Framework 4.6.2+
GitHub:quartznet/quartznet
什麼是 Quartz.NET
Quartz.NET 是從 Java 的 Quartz Scheduler 移植過來的 .NET 排程框架。它讓你可以用 Cron 表達式 或時間間隔定義「什麼時候執行什麼任務」,支援排程持久化(應用程式重啟後排程自動恢復)和叢集模式。
在設備控制的場景中,有很多工作是需要定期自動執行的:資料備份、報表產出、資料清理、校驗提醒。手寫 Timer 能做到基本功能,但一旦需要 Cron 排程、錯過補執行、重啟恢復等進階功能,就需要一個正式的排程框架。
vs Hangfire vs BackgroundService vs Timer
| 比較項目 | Quartz.NET | Hangfire | BackgroundService | System.Timers.Timer |
|---|---|---|---|---|
| Cron 表達式 | ✅ 原生支援 | ✅ 支援 | ❌ 需自己算 | ❌ 只有間隔 |
| 持久化排程 | ✅ 支援多種 DB | ✅ 需要 DB | ❌ 不支援 | ❌ 不支援 |
| Web Dashboard | ❌ 需第三方 | ✅ 內建 | ❌ 無 | ❌ 無 |
| 叢集支援 | ✅ 內建 | ✅ 內建 | ❌ 不支援 | ❌ 不支援 |
| 適合場景 | 桌面 + 伺服器 | Web App | 簡單背景服務 | 簡單計時 |
| 外部依賴 | 可選(記憶體即可) | 必須 DB | 無 | 無 |
| 錯過處理 | ✅ Misfire 策略 | ✅ 自動重試 | ❌ 直接跳過 | ❌ 直接跳過 |
公司場景選擇
- WPF 桌面應用:Quartz.NET(不需要 Web Dashboard,但需要 Cron 排程)
- 簡單的定時輪詢(每 N 秒 poll PLC):
BackgroundService或Timer就夠了 - Web API 的背景工作:Hangfire(有 Dashboard 監控)
核心概念
| 概念 | 說明 |
|---|---|
| IScheduler | 排程器,負責管理所有 Job 和 Trigger 的執行 |
| IJob | 工作介面,實作 Execute(IJobExecutionContext) 方法 |
| IJobDetail | Job 的描述(身份、型別、資料),由 JobBuilder 建立 |
| ITrigger | 觸發器,定義何時執行 Job,由 TriggerBuilder 建立 |
| CronExpression | Cron 表達式,精確到秒的排程描述 |
| JobDataMap | 附加在 Job 或 Trigger 上的 Key-Value 資料 |
運作流程
IScheduler
├── IJobDetail (什麼工作) ← JobBuilder.Create<MyJob>()
│ └── JobDataMap (附加資料)
└── ITrigger (什麼時候) ← TriggerBuilder.Create().WithCronSchedule("...")
└── CronExpression / SimpleSchedule
Scheduler → Trigger → Job 時序
上面的結構只描述「誰跟誰有關係」,下圖則展示「執行期怎麼跑」。從註冊排程、等待觸發時點、執行 Job、到結果回寫 JobStore 的完整過程:
關鍵點:
- Scheduler 是中心:Job 不直接認識 Trigger,由 Scheduler 依 Trigger 時點呼叫對應 Job
- JobStore 決定持久化:記憶體模式(預設)重啟即丟失;DB 模式重啟後自動接手未完成的 Trigger
- Misfire 處理:若錯過觸發時點(系統忙、停機),Trigger 狀態會進入 MISFIRED,Quartz 依策略決定跳過或補執行(見下節狀態機)
Trigger 狀態機
每個 ITrigger 在執行期都有內部狀態。下圖是常見的轉換路徑(簡化版,實際 DB 模式會有更多中間狀態):
常用 Misfire 策略(設定於 TriggerBuilder):
WithMisfireHandlingInstructionDoNothing():忽略錯過的時點,等下次WithMisfireHandlingInstructionFireAndProceed():立刻補執行一次,後續依原排程WithMisfireHandlingInstructionIgnoreMisfires():補執行所有錯過的時點(可能會一次跑很多次)
安裝
# 核心套件
dotnet add package Quartz
# DI 整合 + HostedService
dotnet add package Quartz.Extensions.Hosting
# 持久化(可選,依需求選擇)
dotnet add package Quartz.Serialization.Json
Cron 表達式速查表
Quartz Cron 表達式有 7 個欄位(秒 分 時 日 月 週 年),其中年是可選的:
秒 分 時 日 月 週 [年]
* * * * * ?
| 表達式 | 意思 |
|---|---|
0/30 * * * * ? | 每 30 秒 |
0 0/5 * * * ? | 每 5 分鐘 |
0 0 * * * ? | 每小時整點 |
0 0 8 * * ? | 每天早上 8:00 |
0 0 0 * * ? | 每天午夜 |
0 0 2 * * ? | 每天凌晨 2:00(適合備份) |
0 0 8 ? * MON | 每週一早上 8:00 |
0 0 8 ? * MON-FRI | 每週一到五早上 8:00 |
0 0 0 1 * ? | 每月 1 日午夜 |
0 0 0 L * ? | 每月最後一天午夜 |
特殊字元
*:所有值?:不指定(日和週互斥,其中一個用?)/:遞增(0/5= 從 0 開始每 5)L:最後(L在日欄位 = 月最後一天)MON-FRI:範圍
基本範例
// 1. 定義 Job
public class DatabaseBackupJob : IJob
{
private readonly ILogger<DatabaseBackupJob> _logger;
private readonly IBackupService _backupService;
public DatabaseBackupJob(
ILogger<DatabaseBackupJob> logger,
IBackupService backupService)
{
_logger = logger;
_backupService = backupService;
}
public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation("Database backup job started");
try
{
await _backupService.BackupAsync();
_logger.LogInformation("Database backup completed successfully");
}
catch (Exception ex)
{
_logger.LogError(ex, "Database backup failed");
// 拋出 JobExecutionException 讓 Quartz 知道執行失敗
throw new JobExecutionException(ex, refireImmediately: false);
}
}
}
// 2. 設定排程(Program.cs / Host Builder)
services.AddQuartz(q =>
{
var jobKey = new JobKey("DatabaseBackup", "Maintenance");
q.AddJob<DatabaseBackupJob>(opts => opts
.WithIdentity(jobKey)
.WithDescription("每日凌晨 2 點備份資料庫"));
q.AddTrigger(opts => opts
.ForJob(jobKey)
.WithIdentity("DatabaseBackup-Trigger")
.WithCronSchedule("0 0 2 * * ?") // 每天凌晨 2:00
.WithDescription("Daily at 2 AM"));
});
// 3. 啟動排程器為背景服務
services.AddQuartzHostedService(opts =>
{
opts.WaitForJobsToComplete = true; // 關機時等待執行中的 Job 完成
});
延伸閱讀
- 公司場景 Pattern — 定期備份、報表產出、校驗提醒、資料清理、持久化、Misfire 處理
- QuestPDF 文件產出 — 搭配定時報表
- ClosedXML Excel 讀寫 — 搭配定時匯出