
Covarel 虽然作为一个轻量级调度框架已经满足需求,但是对于Corn 表达式的支持相对较弱,作者实现对秒级的支持实际是通过特殊属性标记字段进行处理,也不支持秒级表达式* * * * * 这种6位写法,只支持到5位。
对于需要秒级cron的开发者来说,需要自定扩展,笔者采用的是使用NCrontab 实现对cron 秒级的解析。官方传送门,如果对NCrontab 不熟悉,可以自行查阅。以下是官方描述:
It does not provide any scheduler or is not a scheduling facility like cron from Unix platforms. What it provides is parsing, formatting and an algorithm to produce occurrences of time based on a give schedule expressed in the crontab format.
翻译过来就是,他并不是提供了任何调度能力,也不像是Unix 平台那样的cron功能。它提供的是解析、格式化以及根据用 crontab 格式表示的给定计划生成时间的算法。简单说就是,它能解析Cron表达式并按照算法转换为对应的时间。以官方描述为例。
cs
using System;
using NCrontab;
var s = CrontabSchedule.Parse("0 12 * */2 Mon"); // Monday of every other month
var start = new DateTime(2000, 1, 1); // Starting January 1, 2000
var end = start.AddYears(1); // Through the year 2000
var occurrences = s.GetNextOccurrences(start, end);
Console.WriteLine(string.Join(Environment.NewLine,
from t in occurrences
select $"{t:ddd, dd MMM yyyy HH:mm}"));
输出结果。
bash
周一, 03 1月 2000 12:00
周一, 10 1月 2000 12:00
周一, 17 1月 2000 12:00
周一, 24 1月 2000 12:00
主要实现思路如下,案例的主体内容依托于笔者的上一篇文章Coravel 自动加载Invocable调度任务,通过外观者模式以及一个秒级cron任务调度引擎类,通过判定调度任务类[ScheduledJob(cron)]中对应的cron 表达式位数进行逻辑选择,控制该类是分配给Covarel还是自定调度。

新增项目CovarelCronLab,主要用于保留CronLab 扩展代码,对应依赖如下:
xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="10.0.0" />
<PackageReference Include="Coravel" Version="6.0.2" />
<PackageReference Include="NCrontab" Version="3.4.0" />
</ItemGroup>
</Project>
对应类功能用途如下:
bash
NCrontab/
# 实际调度选择类
HybridScheduleInterval.cs
# 混合调度外包类(外观模式实现类)
HybridScheduler.cs
# 混合调度扩展类(用于平替Covarel.UseScheduler)
HybridSchedulerExtensions.cs
# 混合调度外包接口(外部调用入口)
IHybridScheduler.cs
# 秒级调度接口(仿Covarel)
ISecCronScheduler.cs
# 本地定义空ScheduledEventConfiguration
NullScheduledEventConfiguration.cs
# 秒级调度实现类+后台任务实现
SecCrontabSchedulerEngine.cs
代码改造
对原有代码进行改造,其中包括任务类、服务注册以及调度配置部分。
任务调度类
设置DateTimeJob 调度类作为演示案例,修改原有5位分钟级cron 为秒级6位,暂且定义每20s 执行一次。
cs
[ScheduledJob("*/20 * * * * *")]
public class DateTimeJob : IInvocable
{
private readonly ILogger<DateTimeJob> _logger;
public DateTimeJob(ILogger<DateTimeJob> logger)
{
_logger = logger;
}
public Task Invoke()
{
_logger.LogInformation("datetimejob run at {date}", DateTime.Now);
return Task.CompletedTask;
}
}
服务注册与调度配置
使用HybridSchedulerExtensions 中预先创建的扩展,实现服务注册和调度配置。
cs
public static class HybridSchedulerExtensions
{
/// <summary>
/// 添加混合调度器服务(秒级)
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddHybridScheduler(this IServiceCollection services)
{
//注册秒级调度器 as IHostedService
services.AddHostedService<SecCrontabSchedulerEngine>();
//注册秒级调度器实现
services.AddSingleton<ISecCronScheduler, SecCrontabSchedulerEngine>();
// 注册混合调度器(供业务使用)
services.AddSingleton<IHybridScheduler>(sp =>
new HybridScheduler(
coravel: sp.GetRequiredService<IScheduler>(),
sec: sp.GetRequiredService<ISecCronScheduler>(),
sp: sp));
services.AddHostedService<SecCrontabSchedulerEngine>();
return services;
}
/// <summary>
/// 配置混合调度器
/// </summary>
/// <param name="provider"></param>
/// <param name="configure"></param>
/// <exception cref="ArgumentNullException"></exception>
public static void UseHybridScheduler(this IServiceProvider provider, Action<IHybridScheduler> configure)
{
if (provider == null) throw new ArgumentNullException(nameof(provider));
if (configure == null) throw new ArgumentNullException(nameof(configure));
// 获取混合调度器实例
var scheduler = provider.GetRequiredService<IHybridScheduler>();
configure(scheduler);
}
}
主类修改
修改Program.cs,将扩展应用于控制台程序中。
cs
internal class Program
{
static void Main(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
ILogger<Program> logger = builder.Services.BuildServiceProvider().GetRequiredService<ILogger<Program>>();
// 注册调度服务
builder.Services.AddScheduler();
// 新增扩展->注册混合调度服务
builder.Services.AddHybridScheduler();
var app = builder.Build();
// 修改调度->UseScheduler 替换为 UseHybridScheduler
// 去除.LogScheduledTaskProgress()
app.Services.UseHybridScheduler(scheduler => {
if (jobTypes != null)
{
// 遍历类型将其添加到调度类型中
foreach (var jobType in jobTypes)
{
var attribute = jobType.GetCustomAttribute<ScheduledJobAttribute>();
if (attribute != null)
{
scheduler.ScheduleInvocableType(jobType).Cron(attribute.CronExpression);
}
}
}
});
// 注册应用生命周期结束时调度任务
var appLifetime = app.Services.GetService<IHostApplicationLifetime>();
appLifetime?.ApplicationStopping.Register(() =>
{
// 结束调度
});
app.Run();
}
}
核心代码解析
类调用依赖图
CrontabSchedule HybridScheduleInterval HybridScheduler HybridSchedulerExtensions IInvocable IScheduleInterval IScheduler ISecCronScheduler NullScheduledEventConfiguration SecCrontabSchedulerEngine
IHybridScheduler
调度外包入口接口IHybridScheduler.cs,主要用于上层外包,保留几个Coravel 调度同名函数,用于封装避免函数差异。
cs
/// <summary>
/// 混合调度器接口,支持秒级和分钟级调度
/// </summary>
public interface IHybridScheduler
{
IScheduleInterval Schedule(Action action);
IScheduleInterval ScheduleAsync(Func<Task> asyncTask);
IScheduleInterval Schedule<T>() where T : IInvocable;
IScheduleInterval ScheduleWithParams<T>(params object[] parameters) where T : IInvocable;
IScheduleInterval ScheduleWithParams(Type invocableType, params object[] parameters);
IScheduleInterval ScheduleInvocableType(Type invocableType);
}
HybridScheduler
实现类HybridScheduler.cs作为IHybridScheduler实现类,内部包含对应的实际调度转换逻辑类HybridScheduleInterval。
cs
public class HybridScheduler : IHybridScheduler
{
private readonly IScheduler _coravel;
private readonly ISecCronScheduler _sec;
private readonly IServiceProvider _sp;
// 依赖注入时,将`Covarel`调度类于秒级调度类进行实例传入
public HybridScheduler(IScheduler coravel, ISecCronScheduler sec, IServiceProvider sp)
{
_coravel = coravel;
_sec = sec;
_sp = sp;
}
public IScheduleInterval Schedule(Action action) =>
new HybridScheduleInterval(_coravel, _sec, _sp, action);
public IScheduleInterval ScheduleAsync(Func<Task> asyncTask) =>
new HybridScheduleInterval(_coravel, _sec, _sp, asyncTask, isAsync: true);
public IScheduleInterval Schedule<T>() where T : IInvocable =>
new HybridScheduleInterval(_coravel, _sec, _sp, typeof(T), isInvocable: true);
public IScheduleInterval ScheduleWithParams<T>(params object[] parameters) where T : IInvocable =>
new HybridScheduleInterval(_coravel, _sec, _sp, typeof(T), parameters, isInvocable: true, hasParams: true);
public IScheduleInterval ScheduleWithParams(Type type, params object[] parameters) =>
new HybridScheduleInterval(_coravel, _sec, _sp, type, parameters, isInvocable: true, hasParams: true);
public IScheduleInterval ScheduleInvocableType(Type type) =>
new HybridScheduleInterval(_coravel, _sec, _sp, type, isInvocable: true);
}
ISecCronScheduler
自定义秒级调度接口ISecCronScheduler.cs,与Covarel 中的部分函数同名便于切换。
cs
public interface ISecCronScheduler
{
void ScheduleSync(Action action, string cronExpression);
void ScheduleAsync(Func<Task> task, string cronExpression);
void ScheduleInvocableByType(IServiceProvider sp, Type type, object[] parameters, string cronExpression);
}
SecCrontabSchedulerEngine
秒级调度实现引擎类SecCrontabSchedulerEngine.cs,既包含了后台任务实现,也包含了调度实现,其中CreateTimer(Func<Task> callback, string cronExpression) 为核心调度任务生成类,主要通过委托方式和NCronLab生成调度逻辑,按照Cron表达式生成调度触发时刻,交由Timer 进行执行,命中条件即执行委托函数。
cs
public class SecCrontabSchedulerEngine : ISecCronScheduler, IHostedService, IDisposable
{
private readonly IServiceProvider _serviceProvider;
private readonly ConcurrentDictionary<Guid, Timer> _timers = new();
private readonly ILogger<SecCrontabSchedulerEngine> _logger;
private readonly ConcurrentBag<Task> _runningTasks = new();
private bool _disposed = false;
private readonly object _lock = new();
public SecCrontabSchedulerEngine(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_logger = serviceProvider.GetService<ILogger<SecCrontabSchedulerEngine>>();
}
// 核心函数
private Timer CreateTimer(Func<Task> callback, string cronExpression)
{
if (_disposed) throw new ObjectDisposedException(nameof(SecCrontabSchedulerEngine));
var schedule = CrontabSchedule.Parse(cronExpression, new CrontabSchedule.ParseOptions { IncludingSeconds = true });
Guid id = Guid.NewGuid();
async void OnTick(object? _)
{
Task? task = null;
try
{
task = callback();
if (task != null)
{
_runningTasks.Add(task);
await task;
}
}
catch (Exception ex)
{
// 可替换为 ILogger
_logger.LogInformation($"[SecCron] Error: {ex}");
}
finally
{
if (task != null) _runningTasks.TryTake(out Task _);
}
if (_disposed) return;
var now = DateTime.Now;
var next = schedule.GetNextOccurrence(now);
var delayMs = Math.Max(0, (long)(next - now).TotalMilliseconds);
if (_timers.TryGetValue(id, out var timer))
{
try { timer.Change(delayMs, Timeout.Infinite); }
catch (ObjectDisposedException) { }
}
}
var first = schedule.GetNextOccurrence(DateTime.Now);
var firstDelayMs = Math.Max(0, (long)(first - DateTime.Now).TotalMilliseconds);
var timer = new Timer(OnTick, null, firstDelayMs, Timeout.Infinite);
lock (_lock)
{
if (!_disposed)
_timers[id] = timer;
else
timer.Dispose();
}
return timer;
}
public void ScheduleSync(Action action, string cron) =>
CreateTimer(() => { action(); return Task.CompletedTask; }, cron);
public void ScheduleAsync(Func<Task> task, string cron) =>
CreateTimer(task, cron);
public void ScheduleInvocableByType(IServiceProvider sp, Type type, object[] parameters, string cronExpression)
{
if (!typeof(IInvocable).IsAssignableFrom(type))
throw new ArgumentException($"{type} must implement IInvocable");
CreateTimer(async () =>
{
using var scope = sp.CreateScope();
var instance = (IInvocable)ActivatorUtilities.CreateInstance(scope.ServiceProvider, type, parameters);
await instance.Invoke();
}, cronExpression);
}
public Task StartAsync(CancellationToken ct) => Task.CompletedTask;
public async Task StopAsync(CancellationToken ct)
{
_disposed = true;
foreach (var (_, timer) in _timers) timer?.Dispose();
_timers.Clear();
// 等待所有任务完成(最多 30 秒)
var timeout = TimeSpan.FromSeconds(30);
var startTime = DateTime.UtcNow;
while (_runningTasks.Count > 0 && DateTime.UtcNow - startTime < timeout)
{
await Task.Delay(50, ct);
}
}
public void Dispose()
{
_disposed = true;
foreach (var (_, timer) in _timers) timer?.Dispose();
_timers.Clear();
}
}
HybridScheduleInterval
混合秒和分钟调度类HybridScheduleInterval.cs 实现Coravel中的IScheduleInterval接口,用于同等构建调度逻辑,将分钟配置都代理到Coravel调度实例,秒级由自定义秒级调度处理类进行处理。核心代码类主要有两个,一个是Cron(string cronExpression) 用于处理cron表达式,一个ProxyToCoravel(Func<IScheduleInterval, IScheduledEventConfiguration> f) 用于将固有分钟级别调度逻辑均代理到Coravel调度实例。
cs
public class HybridScheduleInterval : IScheduleInterval
{
private readonly IScheduler _coravel;
private readonly ISecCronScheduler _sec;
private readonly IServiceProvider _sp;
private readonly object _task;
private readonly object[] _params;
private readonly bool _isAsync;
private readonly bool _isInvocable;
private readonly bool _hasParams;
public HybridScheduleInterval(
IScheduler coravel,
ISecCronScheduler sec,
IServiceProvider sp,
object task,
object[]? parameters = null,
bool isAsync = false,
bool isInvocable = false,
bool hasParams = false)
{
_coravel = coravel;
_sec = sec;
_sp = sp;
_task = task;
_params = parameters ?? Array.Empty<object>();
_isAsync = isAsync;
_isInvocable = isInvocable;
_hasParams = hasParams;
}
private bool IsSixField(string cron)
{
var parts = cron.Trim().Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);
return parts.Length == 6;
}
// 核心类 用于区分当前cron是5位分钟还是6位秒级
public IScheduledEventConfiguration Cron(string cronExpression)
{
if (string.IsNullOrWhiteSpace(cronExpression))
throw new ArgumentException("Cron expression is required.", nameof(cronExpression));
bool isSixField = IsSixField(cronExpression);
if (isSixField)
{
if (_isInvocable && _task is Type invocableType)
{
_sec.ScheduleInvocableByType(_sp, invocableType, _params, cronExpression);
}
else if (_isAsync && _task is Func<Task> asyncFunc)
{
_sec.ScheduleAsync(asyncFunc, cronExpression);
}
else if (_task is Action action)
{
_sec.ScheduleSync(action, cronExpression);
}
else
{
throw new InvalidOperationException("Unsupported task type for second-precision scheduling.");
}
return new NullScheduledEventConfiguration();
}
else
{
// Delegate to Coravel for 5-field cron
if (_task is Action a)
return _coravel.Schedule(a).Cron(cronExpression);
if (_task is Func<Task> f)
return _coravel.ScheduleAsync(f).Cron(cronExpression);
if (_isInvocable && _task is Type t)
{
if (_hasParams)
return _coravel.ScheduleWithParams(t, _params).Cron(cronExpression);
else
return _coravel.ScheduleInvocableType(t).Cron(cronExpression);
}
}
return new NullScheduledEventConfiguration();
}
// --- 频率方法:秒级走自定义,分钟级走 Coravel ---
public IScheduledEventConfiguration EverySecond() => Cron("*/1 * * * * *");
public IScheduledEventConfiguration EveryFiveSeconds() => Cron("*/5 * * * * *");
public IScheduledEventConfiguration EveryTenSeconds() => Cron("*/10 * * * * *");
public IScheduledEventConfiguration EveryFifteenSeconds() => Cron("*/15 * * * * *");
public IScheduledEventConfiguration EveryThirtySeconds() => Cron("*/30 * * * * *");
public IScheduledEventConfiguration EverySeconds(int s) => Cron($"*/{s} * * * * *");
public IScheduledEventConfiguration EveryMinute() => ProxyToCoravel(x => x.EveryMinute());
public IScheduledEventConfiguration EveryFiveMinutes() => ProxyToCoravel(x => x.EveryFiveMinutes());
public IScheduledEventConfiguration EveryTenMinutes() => ProxyToCoravel(x => x.EveryTenMinutes());
public IScheduledEventConfiguration EveryFifteenMinutes() => ProxyToCoravel(x => x.EveryFifteenMinutes());
public IScheduledEventConfiguration EveryThirtyMinutes() => ProxyToCoravel(x => x.EveryThirtyMinutes());
public IScheduledEventConfiguration Hourly() => ProxyToCoravel(x => x.Hourly());
public IScheduledEventConfiguration HourlyAt(int m) => ProxyToCoravel(x => x.HourlyAt(m));
public IScheduledEventConfiguration Daily() => ProxyToCoravel(x => x.Daily());
public IScheduledEventConfiguration DailyAtHour(int h) => ProxyToCoravel(x => x.DailyAtHour(h));
public IScheduledEventConfiguration DailyAt(int h, int m) => ProxyToCoravel(x => x.DailyAt(h, m));
public IScheduledEventConfiguration Weekly() => ProxyToCoravel(x => x.Weekly());
public IScheduledEventConfiguration Monthly() => ProxyToCoravel(x => x.Monthly());
private IScheduledEventConfiguration ProxyToCoravel(Func<IScheduleInterval, IScheduledEventConfiguration> f)
{
if (_task is Action a) return f(_coravel.Schedule(a));
if (_task is Func<Task> func) return f(_coravel.ScheduleAsync(func));
if (_isInvocable && _task is Type t)
{
if (_hasParams)
return f(_coravel.ScheduleWithParams(t, _params));
else
return f(_coravel.ScheduleInvocableType(t));
}
return new NullScheduledEventConfiguration();
}
}
运行效果如下:
bash
info: ConsoleCorave.Jobs.DateTimeJob[0]
datetimejob run at 12/05/2025 09:44:00
info: ConsoleCorave.Jobs.HelloJob[0]
hellojob run at 12/05/2025 09:44:00
info: ConsoleCorave.Jobs.DateTimeJob[0]
datetimejob run at 12/05/2025 09:44:20
info: ConsoleCorave.Jobs.DateTimeJob[0]
datetimejob run at 12/05/2025 09:44:40
info: ConsoleCorave.Jobs.DateTimeJob[0]
datetimejob run at 12/05/2025 09:45:00
info: ConsoleCorave.Jobs.HelloJob[0]
hellojob run at 12/05/2025 09:45:00
info: ConsoleCorave.Jobs.DateTimeJob[0]
datetimejob run at 12/05/2025 09:45:20
案例总结
通过以上方式对Covarel 也有了更加深入的理解,如果不需要Covarel 那些高级功能,完全可以按照上方案例实现一个简版的自定义秒级框架,而不是依托Coravel。