简介
NCrontab 是 .NET 平台下功能完备的 Cron 表达式解析与调度计算库,用于处理类似 Unix Cron 的时间调度逻辑。它不依赖外部系统服务,纯托管实现,是构建定时任务系统的核心组件。
解决的关键问题
Cron表达式解析:将字符串表达式转换为可计算的时间模型- 时间序列生成:计算下次执行时间或生成时间序列
- 跨平台支持:纯
.NET实现,无操作系统依赖 - 轻量高效:无外部依赖,内存占用低(<100KB)
相比于自己手写解析器或引入重量级调度框架(如 Quartz.NET),NCrontab 专注于表达式分析和下一次运行时间计算,体积轻巧、依赖少、性能高。
Cron表达式格式详解
- 标准格式(5段式)
1* * * * * 2┬ ┬ ┬ ┬ ┬ 3│ │ │ │ │ 4│ │ │ │ └── 星期几 (0-6, 0=周日) 5│ │ │ └─────── 月份 (1-12) 6│ │ └──────────── 日 (1-31) 7│ └───────────────── 小时 (0-23) 8└────────────────────── 分钟 (0-59) 9
- 扩展格式(6段式,支持秒级)
1* * * * * * 2┬ ┬ ┬ ┬ ┬ ┬ 3│ │ │ │ │ │ 4│ │ │ │ │ └── 星期几 (0-6) 5│ │ │ │ └─────── 月份 (1-12) 6│ │ │ └──────────── 日 (1-31) 7│ │ └───────────────── 小时 (0-23) 8│ └────────────────────── 分钟 (0-59) 9└─────────────────────────── 秒 (0-59) 10
- 特殊字符说明
| 字符 | 含义 | 示例 | 说明 |
|---|---|---|---|
| * | 任意值 | * * * * * | 每分钟执行 |
| , | 值列表 | 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 | 每月第二个周一执行 |
安装与配置
1Install-Package NCrontab 2
NCrontab 兼容 .NET Framework 4.6.1+、.NET Standard 2.0+,以及所有 .NET Core/.NET 5+ 版本。
只需在代码文件顶部添加引用:
1using NCrontab; 2
核心功能
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 分钟执行
1// 解析表达式 "15 * * * *":每小时的第 15 分钟 2var schedule = CrontabSchedule.Parse("15 * * * *"); 3 4// 获取下一次执行时间(相对于当前时间) 5var next = schedule.GetNextOccurrence(DateTime.Now); 6Console.WriteLine($"下一次执行时间:{next}"); 7 8// 枚举未来 24 小时内的所有执行时间 9var now = DateTime.Now; 10var list = schedule.GetNextOccurrences(now, now.AddHours(24)); 11foreach (var dt in list) 12{ 13 Console.WriteLine(dt); 14} 15
- 支持年字段:每年 1 月 1 日凌晨 0 点
1// 6 段表达式:"0 0 1 1 * *"(秒 分 时 日 月 周 年) 2var opts = new CrontabSchedule.ParseOptions { IncludingSeconds = false, // NCrontab 默认不支持秒 3 // NCrontab 默认不支持年字段,需要扩展包或自定义支持 4}; 5var yearly = CrontabSchedule.Parse("0 0 1 1 *", new CrontabSchedule.ParseOptions()); 6 7// 获取未来 5 次执行 8var occs = yearly.GetNextOccurrences(DateTime.Now, DateTime.Now.AddYears(10)).Take(5); 9foreach (var dt in occs) Console.WriteLine(dt); 10
高级功能详解
时区处理
1// 创建带时区的调度器 2var cron = CrontabSchedule.Parse("0 12 * * *", new CrontabSchedule.ParseOptions 3{ 4 IncludingSeconds = false // 使用5段式 5}); 6 7// 转换到特定时区 8var tz = TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time"); 9DateTime utcNow = DateTime.UtcNow; 10 11// 计算东京时区的下次中午12点 12DateTime next = cron.GetNextOccurrence(utcNow); 13DateTime nextInTokyo = TimeZoneInfo.ConvertTimeFromUtc(next, tz); 14
复杂表达式解析
1// 每月最后一个工作日上午10:15 2var cron = CrontabSchedule.Parse("15 10 LW * *"); 3 4// 每月第三个周五下午3点 5var cron = CrontabSchedule.Parse("0 15 * * 5#3"); 6 7// 工作日上午9点到下午6点,每10分钟 8var cron = CrontabSchedule.Parse("*/10 9-18 * * Mon-Fri"); 9
构建简单调度器
1public class CronScheduler 2{ 3 private readonly CrontabSchedule _schedule; 4 private DateTime _nextRun; 5 6 public CronScheduler(string cronExpression) 7 { 8 _schedule = CrontabSchedule.Parse(cronExpression); 9 _nextRun = _schedule.GetNextOccurrence(DateTime.Now); 10 } 11 12 public async Task StartAsync(CancellationToken ct) 13 { 14 while (!ct.IsCancellationRequested) 15 { 16 var now = DateTime.Now; 17 if (now >= _nextRun) 18 { 19 await ExecuteJobAsync(); 20 _nextRun = _schedule.GetNextOccurrence(now); 21 } 22 await Task.Delay(TimeSpan.FromSeconds(30), ct); // 每30秒检查 23 } 24 } 25 26 private Task ExecuteJobAsync() 27 { 28 // 任务执行逻辑 29 Console.WriteLine($"任务于 {DateTime.Now} 执行"); 30 return Task.CompletedTask; 31 } 32} 33
在 ASP.NET Core 中使用
1// Program.cs 2builder.Services.AddHostedService<CronBackgroundService>(); 3 4// 后台服务实现 5public class CronBackgroundService : BackgroundService 6{ 7 private readonly CrontabSchedule _cron; 8 private DateTime _nextRun; 9 10 public CronBackgroundService() 11 { 12 _cron = CrontabSchedule.Parse("0 */2 * * *"); // 每2小时 13 _nextRun = _cron.GetNextOccurrence(DateTime.Now); 14 } 15 16 protected override async Task ExecuteAsync(CancellationToken stoppingToken) 17 { 18 while (!stoppingToken.IsCancellationRequested) 19 { 20 var now = DateTime.Now; 21 if (now > _nextRun) 22 { 23 await DoHourlyTaskAsync(); 24 _nextRun = _cron.GetNextOccurrence(now); 25 } 26 await Task.Delay(5000, stoppingToken); // 每5秒检查 27 } 28 } 29} 30
错误处理策略
1try 2{ 3 var schedule = CrontabSchedule.Parse(userInput); 4} 5catch (CrontabException ex) 6{ 7 // 捕获特定解析错误 8 logger.LogError($"无效的cron表达式: {userInput}, 错误: {ex.Message}"); 9 // 提供默认表达式 10 schedule = CrontabSchedule.Parse("0 0 * * *"); 11} 12
性能优化技巧
1// 缓存高频使用的调度器 2private static readonly ConcurrentDictionary<string, CrontabSchedule> _scheduleCache = new(); 3 4public CrontabSchedule GetCachedSchedule(string cron) 5{ 6 return _scheduleCache.GetOrAdd(cron, CrontabSchedule.Parse); 7} 8 9// 批量计算优化 10DateTime[] GetNextOccurrencesBatch(CrontabSchedule schedule, int count) 11{ 12 var results = new DateTime[count]; 13 DateTime current = DateTime.Now; 14 15 for (int i = 0; i < count; i++) 16 { 17 current = schedule.GetNextOccurrence(current); 18 results[i] = current; 19 } 20 21 return results; 22} 23
结合 Quartz.NET
NCrontab 可与 Quartz.NET 集成,用于更复杂的调度:
1using Quartz; 2using Quartz.Impl; 3using System; 4using System.Threading.Tasks; 5 6public class MyJob : IJob 7{ 8 public Task Execute(IJobExecutionContext context) 9 { 10 Console.WriteLine($"Job executed at: {DateTime.Now}"); 11 return Task.CompletedTask; 12 } 13} 14 15class Program 16{ 17 static async Task Main() 18 { 19 var factory = new StdSchedulerFactory(); 20 var scheduler = await factory.GetScheduler(); 21 await scheduler.Start(); 22 23 var job = JobBuilder.Create<MyJob>() 24 .WithIdentity("myJob", "group1") 25 .Build(); 26 27 var trigger = TriggerBuilder.Create() 28 .WithIdentity("myTrigger", "group1") 29 .WithCronSchedule("0 0 8 * * ?") // 每天 8:00 30 .Build(); 31 32 await scheduler.ScheduleJob(job, trigger); 33 } 34} 35
使用 NCrontab.Scheduler
NCrontab.Scheduler 是基于 NCrontab 的轻量级调度器,支持动态添加任务:
1using NCrontab.Scheduler; 2 3class Program 4{ 5 static void Main() 6 { 7 var scheduler = new Scheduler(); 8 scheduler.AddTask(CrontabSchedule.Parse("*/1 * * * *"), ct => 9 { 10 Console.WriteLine($"Task runs every minute: {DateTime.Now:O}"); 11 }); 12 scheduler.Start(); 13 Console.ReadLine(); // 保持运行 14 } 15} 16
简单定时任务示例
1public class CronJob 2{ 3 private readonly CrontabSchedule _schedule; 4 private DateTime _nextRun; 5 6 public CronJob(string cronExpression) 7 { 8 _schedule = CrontabSchedule.Parse(cronExpression); 9 _nextRun = _schedule.GetNextOccurrence(DateTime.Now); 10 } 11 12 public void CheckAndRun(Action action) 13 { 14 DateTime now = DateTime.Now; 15 16 if (now >= _nextRun) 17 { 18 action.Invoke(); 19 _nextRun = _schedule.GetNextOccurrence(now); 20 } 21 } 22} 23 24// 使用示例:每小时执行一次 25var hourlyJob = new CronJob("0 * * * *"); 26while (true) 27{ 28 hourlyJob.CheckAndRun(() => { 29 Console.WriteLine($"执行于: {DateTime.Now}"); 30 }); 31 Thread.Sleep(60_000); // 每分钟检查一次 32} 33
封装为可配置服务
1public class CronService : BackgroundService 2{ 3 private readonly List<CronJob> _jobs = new(); 4 5 public void AddJob(string cron, Action action) 6 { 7 _jobs.Add(new CronJob(cron, action)); 8 } 9 10 protected override async Task ExecuteAsync(CancellationToken stoppingToken) 11 { 12 while (!stoppingToken.IsCancellationRequested) 13 { 14 foreach (var job in _jobs) 15 { 16 job.CheckAndRun(); 17 } 18 await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken); 19 } 20 } 21} 22 23// 注册服务 24services.AddHostedService<CronService>(); 25
常见使用场景
适用场景
- 后台服务定时任务
在 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包:www.nuget.org/packages/NC…GitHub仓库:github.com/atifaziz/NC…NCrontab表达式测试工具:ncrontab.swimburger.net