ASP.NET Core 定时任务怎么做?FreeScheduler 可视化调度实战
后台里总有一些每天凌晨跑一次每月 1 号清理数据的需求。EasyAdminBlazor 2.3 的做法是底层用 FreeScheduler 做调度与持久化上层提供统一接口和可视化页面。一、接入builder.AddEasyAdminBlazor(newEasyAdminBlazorOptions{...}).AddEasyAdminBlazorScheduler();publicstaticWebApplicationBuilderAddEasyAdminBlazorScheduler(thisWebApplicationBuilderbuilder){builder.Services.AddSingletonEasyAdminBlazor.ISchedulerService(spnewDefaultSchedulerService(sp));// 注册 FreeScheduler.Scheduler 供直接使用了 FreeScheduler 类型的页面如 TaskScheduler.razor注入builder.Services.AddSingleton(sp((DefaultSchedulerService)sp.GetRequiredServiceEasyAdminBlazor.ISchedulerService()).GetInternalScheduler());returnbuilder;}核心包默认注册的是空的NullSchedulerService// AdminExtensions.csbuilder.Services.TryAddSingletonISchedulerService,NullSchedulerService();它的IsAvailable为 false框架里依赖调度的页面比如任务管理页会据此隐藏或重定向varschedulerServiceProvider.GetRequiredServiceISchedulerService();if(!scheduler.IsAvailablepath.Contains(taskscheduler)){admin.Redirect(/Admin/);return;}所以定时任务是可选扩展不装它系统照常运行只是没有调度能力。二、调度器初始化时做了什么publicDefaultSchedulerService(IServiceProviderserviceProvider){varoptionsserviceProvider.GetRequiredServiceIOptionsEasyAdminBlazor.EasyAdminBlazorOptions().Value;// 调度器是 Singleton 后台服务使用主库 ORM不能解析 Scoped 的 IFreeSqlvarfsqlserviceProvider.GetRequiredServiceMainOrmHandle().Orm;EnsureSchedulerTableMapping(fsql);BuildSchedulerAttributeTriggers(options,fsql);varscheduleTimeZoneResolveScheduleTimeZone(serviceProvider);_schedulernewFreeSchedulerBuilder().OnExecuting(task{Console.WriteLine($[{DateTime.Now:HH:mm:ss.fff}]{task.Topic}被执行);if(_schedulerAttributeTriggers.TryGetValue(task.Topic,outvartrigger)){trigger(serviceProvider);return;}options.SchedulerExecuting?.Invoke(serviceProvider,MapToData(task));}).UseTimeZone(scheduleTimeZone).UseStorage(fsql).UseCustomInterval(task{try{varnowDateTime.UtcNow;varnextTimeCrontabSchedule.Parse(task.IntervalArgument,newCrontabSchedule.ParseOptions{IncludingSecondstrue}).GetNextOccurrence(now);if(nextTimenow)returnTimeSpan.FromSeconds(5);returnnextTime.Subtract(now);}catch{// 非法 Cron 表达式避免调度器崩溃退化为 5 秒后重试returnTimeSpan.FromSeconds(5);}}).Build();}四个关键点必须用主库 ORMMainOrmHandle.Orm。调度器是单例后台服务直接解析 Scoped 的IFreeSql会踩生命周期问题源码注释明确写了这一点。UseStorage(fsql)任务和日志持久化到数据库重启不丢。OnExecuting优先匹配特性注册的任务[Scheduler]其次走options.SchedulerExecuting回调。UseCustomInterval用 NCrontab 计算下次执行时间支持含秒的 6 段 Cron。三、时区三级解析/// summary/// 解析调度器时区。////// 优先级Scheduler:TimeZoneIdIANA/Windows 时区名/// → Scheduler:UtcOffsetHours小时偏移/// → 默认 8保持既有中国时区行为避免破坏现有项目的既有计划任务。/// /summaryprivatestaticTimeSpanResolveScheduleTimeZone(IServiceProviderserviceProvider){varconfigurationserviceProvider.GetServiceMicrosoft.Extensions.Configuration.IConfiguration();// 1) 优先按 IANA/Windows 时区名解析可正确处理夏令时vartimeZoneIdconfiguration?[Scheduler:TimeZoneId];if(!string.IsNullOrWhiteSpace(timeZoneId)){try{returnTimeZoneInfo.FindSystemTimeZoneById(timeZoneId).BaseUtcOffset;}catch(Exceptionex)when(exisTimeZoneNotFoundExceptionorInvalidTimeZoneException){Console.WriteLine($[EasyAdminBlazor] Scheduler:TimeZoneId 无效{timeZoneId}回退到偏移配置);}}// 2) 其次按小时偏移配置varoffsetTextconfiguration?[Scheduler:UtcOffsetHours];if(!string.IsNullOrWhiteSpace(offsetText)double.TryParse(offsetText,System.Globalization.NumberStyles.Float,System.Globalization.CultureInfo.InvariantCulture,outvarhours)hoursis-14and14){returnTimeSpan.FromHours(hours);}// 3) 默认保持现有的中国时区行为returnTimeSpan.FromHours(8);}{Scheduler:{TimeZoneId:Asia/Shanghai,UtcOffsetHours:null}}为什么默认是 8 而不是 UTC源码注释给了答案保持既有行为避免升级后老项目的计划任务时间全部偏移。如果你的服务器在别的时区显式配置TimeZoneId即可。四、Cron 表达式与校验添加任务时会先校验 CronpublicstringAddTask(stringtopic,stringbody,intround,SchedulerIntervalinterval,stringargument){if(intervalSchedulerInterval.Custom!IsValidCronExpression(argument)){thrownewArgumentException(Cron 表达式无效请检查格式秒 分 时 日 月 周,nameof(argument));}returnFreeScheduler.Datafeed.AddTask(_scheduler,topic,body,round,(FreeScheduler.TaskInterval)(int)interval,argument);}privatestaticboolIsValidCronExpression(string?expression){if(string.IsNullOrWhiteSpace(expression))returnfalse;try{CrontabSchedule.Parse(expression,newCrontabSchedule.ParseOptions{IncludingSecondstrue});returntrue;}catch{returnfalse;}}格式是 6 段、含秒秒 分 时 日 月 周。0 0 0 1 * * → 每月 1 号 00:00:00 0 0/5 * * * * → 每 5 分钟 0 30 8 * * 1-5 → 工作日 08:30调度间隔的类型publicenumSchedulerInterval{/// summary按秒触发/summarySeconds1,/// summary每天固定时间触发如 15:55:59/summaryRunOnDay11,/// summary每星期几固定时间触发如 2:15:55:59/summaryRunOnWeek12,/// summary每月第几天固定时间触发如 5:15:55:59/summaryRunOnMonth13,/// summary自定义 Cron 表达式/summaryCustom21,}枚举值与 FreeScheduler 的TaskInterval数值对齐所以转换就是一次强转(FreeScheduler.TaskInterval)(int)interval。五、用代码注册任务[Scheduler]特性仓库里有一个真实用例/// summary/// 定时清理过期错误日志/// /summaryinternalstaticclassErrorLogCleanupJob{/// summary/// 每月 1 号凌晨清理一个月前的错误日志/// /summary[Scheduler(清理错误日志,0 0 0 1 * *)]internalstaticvoidClearErrorLogs(IServiceProviderservice){System.Console.WriteLine(清理错误日志 被触发...);varscopeFactoryservice.GetRequiredServiceIServiceScopeFactory();usingvarscopescopeFactory.CreateScope();varreposcope.ServiceProvider.GetServiceIBaseRepositorySysLog();if(repo!null){repo.Delete(xx.CreatedTimeDateTime.Now.AddMonths(-1));}}}特性接受两种构造方式[AttributeUsage(AttributeTargets.Method)]publicclassSchedulerAttribute:Attribute{publicstringName{get;set;}string.Empty;publicSchedulerIntervalInterval{get;set;}publicstringArgument{get;set;}string.Empty;publicintRound{get;set;}-1;publicSchedulerTaskStatusStatus{get;set;}publicSchedulerAttribute(stringname){this.Namename;}publicSchedulerAttribute(stringname,stringcron){this.Namename;this.IntervalSchedulerInterval.Custom;this.Argumentcron;}}启动时会扫描EasyAdminBlazorOptions.Assemblies里的静态方法并同步到任务表vartaskInfonewSchedTaskInfo{Topic$[SchedulerAttribute]{attr.Name},Interval(FreeScheduler.TaskInterval)(int)attr.Interval,IntervalArgumentattr.Argument,Roundattr.Round,Status(FreeScheduler.TaskStatus)(int)attr.Status,Bodystring.Empty,CreateTimeDateTime.Now,CurrentRound0,ErrorTimes0,LastRunTimenewDateTime(1970,1,1),};同步策略是保留运行状态、只更新定义varexistingfsql.SelectSchedTaskInfo().Where(aa.Topic.StartsWith([SchedulerAttribute])).ToList();foreach(varentryinallSchedulerMethods){varfindexisting.Find(aa.Topicentry.TaskInfo.Topic);if(find!null){entry.TaskInfo.Idfind.Id;entry.TaskInfo.Bodyfind.Body;entry.TaskInfo.CreateTimefind.CreateTime;entry.TaskInfo.CurrentRoundfind.CurrentRound;entry.TaskInfo.ErrorTimesfind.ErrorTimes;entry.TaskInfo.LastRunTimefind.LastRunTime;}else{entry.TaskInfo.Id${DateTime.Now:yyyyMMdd}.{YitIdHelper.NextId()};}}varrepofsql.GetRepositorySchedTaskInfo();repo.BeginEdit(existing);repo.EndEdit(allSchedulerMethods.Select(aa.TaskInfo).ToList());已存在的任务复用原 Id、保留执行次数与最后运行时间新任务生成新 Id。这样重启不会把任务的运行历史清零。特性任务的 Topic 带[SchedulerAttribute]前缀用来和页面手动创建的任务区分开——所以管理页里能看到哪些是代码定义的、哪些是后台加的。六、可视化管理页面/Admin/TaskScheduler用的是 BootstrapBlazor 的Table不是AdminTable因为任务数据来自调度器而不是业务表Table reftable TItemSchedulerTaskData EditDialogSizeSize.Large IsPaginationtrue IsStripedtrue IsBorderedtrue IsMultipleSelecttrue ShowToolbartrue ShowExtendButtonstrue ShowEditButtonfalse ShowExtendEditButtonfalse OnQueryAsyncOnQueryAsync OnSaveAsyncOnSaveAsync OnDeleteAsyncOnDeleteAsync页面提供的能力操作实现分页查询Scheduler.GetTasks(pageIndex, pageItems)新建任务OnSaveAsync→Scheduler.AddTask(topic, body, round, interval, argument)删除任务OnDeleteAsync→Scheduler.RemoveTask(id)暂停 / 恢复PauseTask/ResumeTask带[OperationLog(暂停了任务)]等审计标注立即触发RunNowTask查看日志弹窗内嵌TableSchedulerTaskLog调GetTaskLogs其中几个方法都标了操作日志特性[OperationLog(恢复了任务)]asyncTaskResumeTask(SchedulerTaskDatatask){Scheduler.ResumeTask(task.Id);awaittable.QueryAsync(1);}管理操作会被记进操作日志这在生产排查时很有用“谁在什么时候暂停了清理任务”。七、日志与失败信息任务执行结果由调度器记录映射成统一的SchedulerTaskLogprivatestaticSchedulerTaskLogMapToLog(FreeScheduler.TaskLoglog){returnnewSchedulerTaskLog{TaskIdlog.TaskId,Roundlog.Round,ElapsedMillisecondslog.ElapsedMilliseconds,Successlog.Success,Exceptionlog.Exception,Remarklog.Remark,CreateTimelog.CreateTime};}字段用途Round第几轮执行ElapsedMilliseconds耗时Success是否成功Exception异常信息StringLength -1不截断Remark备注任务本身还带ErrorTimes累计失败次数和Status运行中 / 已暂停 / 已结束列表页直接展示便于快速发现一直在失败的任务。八、多实例部署要注意什么任务定义和执行状态都在数据库里FreeScheduler_task/FreeScheduler_tasklog所以多实例共享同一份任务清单。同一时刻只有一个实例真正执行任务这一点由 FreeScheduler 的存储与抢占机制保证如果对执行频率有强要求建议在任务体内部再做一次幂等保护例如用分布式锁见第 19 篇。任务体要自己创建 Scope。看ErrorLogCleanupJob的写法varscopeFactoryservice.GetRequiredServiceIServiceScopeFactory();usingvarscopescopeFactory.CreateScope();varreposcope.ServiceProvider.GetServiceIBaseRepositorySysLog();任务由单例调度器触发直接解析 Scoped 服务会出问题。时区要统一配置。多实例部署在不同时区时Scheduler:TimeZoneId必须一致否则同一个 Cron 会在不同实例上解释成不同时间。九、常见问题现象原因处理菜单里没有任务计划未安装 Scheduler 扩展调用AddEasyAdminBlazorScheduler()页面被重定向回首页ISchedulerService.IsAvailable false同上添加任务报Cron 表达式无效段数不对或语法错误使用 6 段格式秒 分 时 日 月 周任务时间比预期差 8 小时时区配置设置Scheduler:TimeZoneId重启后任务丢失未使用UseStorage被改动过确认DefaultSchedulerService未被修改代码里加了[Scheduler]但没出现所在程序集不在EasyAdminBlazorOptions.Assemblies里把程序集加进去任务报错但没有日志任务体内部吞了异常让异常抛出调度器会记录到Scheduler_tasklog十、小结EasyAdminBlazor 的定时任务可以理解成三层层内容抽象层ISchedulerServiceNullSchedulerService不装扩展也能编译运行引擎层FreeScheduler 数据库持久化 时区 Cron 校验管理层/Admin/TaskScheduler可视化页面 操作日志 执行日志再加一个实用的代码注册方式[Scheduler(名称, cron)]就覆盖了后台定时任务的两类来源代码里写死的周期性任务和运营人员在页面上临时加的任务。如果你正在用 .NET 10 Blazor 做后台需要每天/每月跑一次的调度能力可以看看 EasyAdminBlazor 的 Scheduler 扩展任务持久化、可视化启停、执行日志、时区配置都开箱可用。文档https://easyadmin.wang-zhan.com.cn/doc源码https://gitee.com/gudufy/EasyAdminBlazor

相关新闻

拉孚 AIoT 节能实践:以“物联基座 + 场景智能体“击穿存量空间的能源黑洞

拉孚 AIoT 节能实践:以“物联基座 + 场景智能体“击穿存量空间的能源黑洞

关键词:拉孚、DeepBasic Folar、AIoT、节能、暖通节能智能体、冷源机组节能、空间认知智能体、存量建筑节能、AI Agent、建筑智能化、双碳建筑运行能耗约占中国全社会能源消费的 20% 以上,其中暖通空调(HVAC)系统又占据建筑能耗的…

2026/10/12 9:42:47 阅读更多 →
华为S5700交换机VLAN配置全流程:从接口类型到VLAN间通信与避坑指南

华为S5700交换机VLAN配置全流程:从接口类型到VLAN间通信与避坑指南

简介:这份PDF资料面向网络运维初学者与中小型企业网络管理员,聚焦华为S5700三层交换机的VLAN划分与配置实践,帮助读者解决多网段逻辑隔离、交换机与路由器协同组网等常见问题。资源为单一PDF文档,压缩包约1.43MB,内容以…

2026/10/13 1:08:29 阅读更多 →
WebSocket企业级实时通信实践:sgcWebSockets部署、心跳与高并发避坑指南

WebSocket企业级实时通信实践:sgcWebSockets部署、心跳与高并发避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:11:52 阅读更多 →

最新新闻

金字塔原理:职场沟通结构化表达的四个原则与实战技巧

金字塔原理:职场沟通结构化表达的四个原则与实战技巧

废话不多说,这可能是你在职场沟通中最值得补的一课。我见过太多人,明明业务能力很强,可一开口就败下阵来:汇报讲了十分钟,领导眉头越皱越紧;邮件写了几百字,对方回复“所以你想表达什么”&#…

2026/10/13 17:10:09 阅读更多 →
实时数据采集架构设计:从需求量化到全链路调优

实时数据采集架构设计:从需求量化到全链路调优

直接开讲。实时数据采集架构设计这件事,我前前后后做了三年多,从最早只会写脚本定时拉数据,到后来能独立设计一套覆盖采集、缓冲、计算、落库全链路的实时方案,中间踩过不少坑。很多人一听到“实时数据采集”这几个字,…

2026/10/13 17:10:09 阅读更多 →
专科生AI论文写作平台实测:8个靠谱工具全解析

专科生AI论文写作平台实测:8个靠谱工具全解析

实话实说,专科生写毕业论文,难度不比本科生低,甚至更难。难在哪?时间紧、任务重,还有相当一部分同学是第一次接触完整的学术写作流程。我自己当年在专科阶段写毕业设计报告时,光是“怎么开头”“怎么把一段…

2026/10/13 17:10:09 阅读更多 →
Docker部署Ceph实现RBD镜像创建与挂载全流程

Docker部署Ceph实现RBD镜像创建与挂载全流程

1. 为什么非得用 Docker 部署 Ceph?——从“三台物理机起步”到“单机快速验证”的真实动因很多人看到“Docker 部署 Ceph”第一反应是皱眉:“Ceph 不是分布式存储系统吗?跑在容器里靠谱?”这问题我当年在某高校实验室带学生做存储…

2026/10/13 17:10:09 阅读更多 →
Python机器学习入侵检测系统实战:从NSL-KDD到Flask部署

Python机器学习入侵检测系统实战:从NSL-KDD到Flask部署

简介:这是一份基于Python机器学习的入侵检测系统项目源码,面向希望入门或进阶网络安全与机器学习方向的学习者,可作为毕业设计、课程设计、大作业或工程实训的参考方案。项目围绕网络流量嗅探与异常检测展开,包含数据预处理、特征…

2026/10/13 17:10:09 阅读更多 →
荷兰耕地遥感语义分割:从数据标注到模型训练全流程实战

荷兰耕地遥感语义分割:从数据标注到模型训练全流程实战

简介:这份荷兰耕地语义分割遥感影像数据集面向遥感图像处理、计算机视觉与农业监测方向的研究者和开发者,用于训练和验证耕地识别与像素级分割模型。资源包共2000个文件,以xml标注文件为主要类型,对应每张遥感影像的像素级类别标签…

2026/10/13 17:09:08 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →