应使用 AddHostedService() 注册 BackgroundService,因其确保 StartAsync/StopAsync 被主机调用;AddSingleton() 无效。ExecuteAsync 必须响应 stoppingToken,所有异步操作需传递该令牌,并在内部作用域中获取 Scoped 服务如 DbContext。

直接用 BackgroundService,别手写 IHostedService —— 除非你明确需要绕过基类封装的生命周期控制(比如要自定义启动失败重试、或集成非标准取消模型)。
为什么不能用 AddSingleton<myservice>()</myservice> 替代 AddHostedService<myservice>()</myservice>
因为 IHostedService 实例必须由主机在启动/停止阶段统一调度;AddSingleton 注册的服务不会被主机识别为“托管服务”,StartAsync 和 StopAsync 根本不会被调用。即使类实现了接口,也等于没写。
- 注册错误示例:
services.AddSingleton<mybackgroundservice>()</mybackgroundservice>→ 启动时静默忽略 - 正确注册方式:
services.AddHostedService<mybackgroundservice>()</mybackgroundservice> - 如果依赖注入失败(如构造函数请求了
Scoped服务),日志里会出现Failed to instantiate hosted service,但主机仍会继续启动
BackgroundService 的 ExecuteAsync 必须响应取消信号
这是最容易出问题的地方:不检查 stoppingToken 或漏传它,会导致服务无法优雅关闭,进程卡在 StopAsync 阶段超时退出。
- 错误写法:
while (true) { await DoWork(); await Task.Delay(1000); }→ 完全无视取消 - 正确写法:
while (!stoppingToken.IsCancellationRequested) { await DoWorkAsync(stoppingToken); await Task.Delay(1000, stoppingToken); } - 所有异步调用(如
HttpClient.SendAsync、DbContext.SaveChangesAsync)都必须把stoppingToken传进去 - 避免在
ExecuteAsync外层try/catch吞异常——BackgroundService基类已捕获并记录未处理异常;吞掉它只会让服务“假死”
如何在后台服务里安全使用 DbContext 等 Scoped 服务
IHostedService 实例是 Singleton 生命周期,不能直接注入 Scoped 服务,否则运行时报 Cannot resolve scoped service from root provider。
- 必须在
ExecuteAsync内部按需创建作用域:using var scope = _scopeFactory.CreateScope(); - 从
scope.ServiceProvider获取DbContext或其他 Scoped 服务 - 每次数据库操作都应新建 scope,不要缓存
DbContext实例,也不要跨 scope 复用 - .NET 6+ 可直接注入
IServiceProvider,但CreateScope()开销仍存在,高频场景需权衡
真正麻烦的不是写代码,而是取消信号的传播深度——从 ExecuteAsync 到每一层 async 方法,再到第三方 SDK 的异步调用,只要断一环,关机就超时。别假设“Delay 有取消就够了”,得逐层确认。


















