簡介
NCrontab 是 .NET 平台下功能完備的 Cron 表達式解析與調度計算庫,用於處理類似 Unix Cron 的時間調度邏輯。它不依賴外部系統服務,純託管實現,是構建定時任務系統的核心組件。
解決的關鍵問題
Cron表達式解析:將字符串表達式轉換為可計算的時間模型- 時間序列生成:計算下次執行時間或生成時間序列
- 跨平台支持:純
.NET實現,無操作系統依賴 - 輕量高效:無外部依賴,內存佔用低(<100KB)
相比於自己手寫解析器或引入重量級調度框架(如 Quartz.NET),NCrontab 專注於表達式分析和下一次運行時間計算,體積輕巧、依賴少、性能高。
Cron表達式格式詳解
- 標準格式(5段式)
* * * * *
┬ ┬ ┬ ┬ ┬
│ │ │ │ │
│ │ │ │ └── 星期幾 (0-6, 0=週日)
│ │ │ └─────── 月份 (1-12)
│ │ └──────────── 日 (1-31)
│ └───────────────── 小時 (0-23)
└────────────────────── 分鐘 (0-59)
- 擴展格式(6段式,支持秒級)
* * * * * *
┬ ┬ ┬ ┬ ┬ ┬
│ │ │ │ │ │
│ │ │ │ │ └── 星期幾 (0-6)
│ │ │ │ └─────── 月份 (1-12)
│ │ │ └──────────── 日 (1-31)
│ │ └───────────────── 小時 (0-23)
│ └────────────────────── 分鐘 (0-59)
└─────────────────────────── 秒 (0-59)
- 特殊字符説明
| 字符 | 含義 | 示例 | 説明 |
|---|---|---|---|
* |
任意值 | * * * * * |
每分鐘執行 |
, |
值列表 | 0,15,30 * * * * |
每小時的0,15,30分執行 |
- |
範圍 | 9-17 * * * * |
9點到17點每小時執行 |
/ |
步長 | */5 * * * * |
每5分鐘執行 |
? |
不指定(僅用於日和星期) | 0 0 ? * 1 |
每週一午夜 |
L |
最後 (Last) | 0 0 L * * |
每月最後一天午夜執行 |
W |
最近工作日(Weekday) | 0 0 15W * * |
每月15日最近的工作日執行 |
# |
第N個星期X | 0 0 * * 1#2 |
每月第二個週一執行 |
安裝與配置
Install-Package NCrontab
NCrontab 兼容 .NET Framework 4.6.1+、.NET Standard 2.0+,以及所有 .NET Core/.NET 5+ 版本。
只需在代碼文件頂部添加引用:
using NCrontab;
核心功能
Cron表達式解析
支持標準 5 段(分、時、日、月、周)格式,以及可選的第 6 段“年”字段擴展。
-
下次執行時間計算
CrontabSchedule.GetNextOccurrence(DateTime baseTime):獲取從baseTime開始的下一條匹配時間。CrontabSchedule.GetNextOccurrences(DateTime start, DateTime end):枚舉指定時間範圍內的所有匹配時間。
-
可配置解析選項
CrontabSchedule.Parse(string expression, CrontabSchedule.ParseOptions options):控制是否支持年字段或秒級字段。CrontabSchedule.ParseOptions.IncludeSeconds(僅在擴展包NCrontab.Scheduler中支持)。
-
線程安全
CrontabSchedule實例在多線程間可安全共享,建議對同一表達式只調用一次Parse並緩存結果。
API 用法
| 方法 / 屬性 | 説明 |
|---|---|
CrontabSchedule.Parse(string expression) |
解析 5 段標準 Cron 表達式,返回調度對象 |
CrontabSchedule.Parse(string expression, ParseOptions opt) |
按指定選項解析 Cron 表達式 |
DateTime GetNextOccurrence(DateTime baseTime) |
獲取從 baseTime 之後的第一條匹配時間 |
IEnumerable<DateTime> GetNextOccurrences(DateTime start, DateTime end) |
獲取指定時間區間內的所有匹配時間 |
string ToString() |
返回原始表達式文本 |
ParseOptions.IncludeSeconds |
true 時支持解析第 0 段(秒)字段;默認只支持分級別。 |
使用示例
- 基本示例:每小時第 15 分鐘執行
// 解析表達式 "15 * * * *":每小時的第 15 分鐘
var schedule = CrontabSchedule.Parse("15 * * * *");
// 獲取下一次執行時間(相對於當前時間)
var next = schedule.GetNextOccurrence(DateTime.Now);
Console.WriteLine($"下一次執行時間:{next}");
// 枚舉未來 24 小時內的所有執行時間
var now = DateTime.Now;
var list = schedule.GetNextOccurrences(now, now.AddHours(24));
foreach (var dt in list)
{
Console.WriteLine(dt);
}
- 支持年字段:每年 1 月 1 日凌晨 0 點
// 6 段表達式:"0 0 1 1 * *"(秒 分 時 日 月 周 年)
var opts = new CrontabSchedule.ParseOptions { IncludingSeconds = false, // NCrontab 默認不支持秒
// NCrontab 默認不支持年字段,需要擴展包或自定義支持
};
var yearly = CrontabSchedule.Parse("0 0 1 1 *", new CrontabSchedule.ParseOptions());
// 獲取未來 5 次執行
var occs = yearly.GetNextOccurrences(DateTime.Now, DateTime.Now.AddYears(10)).Take(5);
foreach (var dt in occs) Console.WriteLine(dt);
高級功能詳解
時區處理
// 創建帶時區的調度器
var cron = CrontabSchedule.Parse("0 12 * * *", new CrontabSchedule.ParseOptions
{
IncludingSeconds = false // 使用5段式
});
// 轉換到特定時區
var tz = TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time");
DateTime utcNow = DateTime.UtcNow;
// 計算東京時區的下次中午12點
DateTime next = cron.GetNextOccurrence(utcNow);
DateTime nextInTokyo = TimeZoneInfo.ConvertTimeFromUtc(next, tz);
複雜表達式解析
// 每月最後一個工作日上午10:15
var cron = CrontabSchedule.Parse("15 10 LW * *");
// 每月第三個週五下午3點
var cron = CrontabSchedule.Parse("0 15 * * 5#3");
// 工作日上午9點到下午6點,每10分鐘
var cron = CrontabSchedule.Parse("*/10 9-18 * * Mon-Fri");
構建簡單調度器
public class CronScheduler
{
private readonly CrontabSchedule _schedule;
private DateTime _nextRun;
public CronScheduler(string cronExpression)
{
_schedule = CrontabSchedule.Parse(cronExpression);
_nextRun = _schedule.GetNextOccurrence(DateTime.Now);
}
public async Task StartAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
var now = DateTime.Now;
if (now >= _nextRun)
{
await ExecuteJobAsync();
_nextRun = _schedule.GetNextOccurrence(now);
}
await Task.Delay(TimeSpan.FromSeconds(30), ct); // 每30秒檢查
}
}
private Task ExecuteJobAsync()
{
// 任務執行邏輯
Console.WriteLine($"任務於 {DateTime.Now} 執行");
return Task.CompletedTask;
}
}
在 ASP.NET Core 中使用
// Program.cs
builder.Services.AddHostedService<CronBackgroundService>();
// 後台服務實現
public class CronBackgroundService : BackgroundService
{
private readonly CrontabSchedule _cron;
private DateTime _nextRun;
public CronBackgroundService()
{
_cron = CrontabSchedule.Parse("0 */2 * * *"); // 每2小時
_nextRun = _cron.GetNextOccurrence(DateTime.Now);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var now = DateTime.Now;
if (now > _nextRun)
{
await DoHourlyTaskAsync();
_nextRun = _cron.GetNextOccurrence(now);
}
await Task.Delay(5000, stoppingToken); // 每5秒檢查
}
}
}
錯誤處理策略
try
{
var schedule = CrontabSchedule.Parse(userInput);
}
catch (CrontabException ex)
{
// 捕獲特定解析錯誤
logger.LogError($"無效的cron表達式: {userInput}, 錯誤: {ex.Message}");
// 提供默認表達式
schedule = CrontabSchedule.Parse("0 0 * * *");
}
性能優化技巧
// 緩存高頻使用的調度器
private static readonly ConcurrentDictionary<string, CrontabSchedule> _scheduleCache = new();
public CrontabSchedule GetCachedSchedule(string cron)
{
return _scheduleCache.GetOrAdd(cron, CrontabSchedule.Parse);
}
// 批量計算優化
DateTime[] GetNextOccurrencesBatch(CrontabSchedule schedule, int count)
{
var results = new DateTime[count];
DateTime current = DateTime.Now;
for (int i = 0; i < count; i++)
{
current = schedule.GetNextOccurrence(current);
results[i] = current;
}
return results;
}
結合 Quartz.NET
NCrontab 可與 Quartz.NET 集成,用於更復雜的調度:
using Quartz;
using Quartz.Impl;
using System;
using System.Threading.Tasks;
public class MyJob : IJob
{
public Task Execute(IJobExecutionContext context)
{
Console.WriteLine($"Job executed at: {DateTime.Now}");
return Task.CompletedTask;
}
}
class Program
{
static async Task Main()
{
var factory = new StdSchedulerFactory();
var scheduler = await factory.GetScheduler();
await scheduler.Start();
var job = JobBuilder.Create<MyJob>()
.WithIdentity("myJob", "group1")
.Build();
var trigger = TriggerBuilder.Create()
.WithIdentity("myTrigger", "group1")
.WithCronSchedule("0 0 8 * * ?") // 每天 8:00
.Build();
await scheduler.ScheduleJob(job, trigger);
}
}
使用 NCrontab.Scheduler
NCrontab.Scheduler 是基於 NCrontab 的輕量級調度器,支持動態添加任務:
using NCrontab.Scheduler;
class Program
{
static void Main()
{
var scheduler = new Scheduler();
scheduler.AddTask(CrontabSchedule.Parse("*/1 * * * *"), ct =>
{
Console.WriteLine($"Task runs every minute: {DateTime.Now:O}");
});
scheduler.Start();
Console.ReadLine(); // 保持運行
}
}
簡單定時任務示例
public class CronJob
{
private readonly CrontabSchedule _schedule;
private DateTime _nextRun;
public CronJob(string cronExpression)
{
_schedule = CrontabSchedule.Parse(cronExpression);
_nextRun = _schedule.GetNextOccurrence(DateTime.Now);
}
public void CheckAndRun(Action action)
{
DateTime now = DateTime.Now;
if (now >= _nextRun)
{
action.Invoke();
_nextRun = _schedule.GetNextOccurrence(now);
}
}
}
// 使用示例:每小時執行一次
var hourlyJob = new CronJob("0 * * * *");
while (true)
{
hourlyJob.CheckAndRun(() => {
Console.WriteLine($"執行於: {DateTime.Now}");
});
Thread.Sleep(60_000); // 每分鐘檢查一次
}
封裝為可配置服務
public class CronService : BackgroundService
{
private readonly List<CronJob> _jobs = new();
public void AddJob(string cron, Action action)
{
_jobs.Add(new CronJob(cron, action));
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
foreach (var job in _jobs)
{
job.CheckAndRun();
}
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
}
}
}
// 註冊服務
services.AddHostedService<CronService>();
常見使用場景
適用場景
- 後台服務定時任務
在 ASP.NET Core、Windows Service 或 Worker Service 中,用來調度郵件發送、報表生成、緩存清理等週期性任務。
- 動態配置調度
從數據庫或配置中心讀取 Cron 表達式,並動態生成 CrontabSchedule 實例,允許業務人員無需重啓即可調整調度策略。
- 微服務消息投遞
結合消息隊列(RabbitMQ、Kafka)實現延遲隊列或定時重試功能。
不適用場景
- 高精度定時(<1秒級精度)
- 分佈式協調任務(需用分佈式調度器)
- 動態實時調整(表達式變更需重啓)
- 長週期任務(超過5年的調度計算)
何時選擇其他方案:
- 需要分佈式任務調度 →
Quartz.NET - 需要任務持久化和重試 →
Hangfire - 需要複雜工作流管理 → Elsa Workflows
性能與注意事項
-
性能
- 解析開銷:
Parse方法對錶達式做詞法和語法分析,建議對同一表達式只執行一次,並緩存CrontabSchedule實例。 - 計算開銷:
GetNextOccurrence算法為線性掃描,遇到複雜範圍(如“每月的最後一個工作日”)時性能略有下降,但對常見表達式足夠快速。
- 解析開銷:
-
線程安全
CrontabSchedule的GetNext*方法可在多線程併發調用,無需額外同步。
-
時區問題
- 輸入的
DateTime:NCrontab不涉及時區轉換,所有計算均在DateTime自身的Kind上執行。 UTC vs Local:如果系統跨時區或夏令時環境,建議統一使用DateTime.UtcNow並將調度時間也轉換為UTC。
- 輸入的
-
表達式合法性
- 對於不合法的表達式,
Parse會拋出CrontabException。
- 對於不合法的表達式,
-
擴展限制
- 正式包不支持秒級(第 0 段)或年級(第 6 段)字段;社區擴展或自定義修改後可按需添加。
資源和文檔
NuGet包:https://www.nuget.org/packages/NCrontabGitHub倉庫:https://github.com/atifaziz/NCrontabNCrontab表達式測試工具:https://ncrontab.swimburger.net