在AspNetCore中理解依赖注入生命周期冲突与解决方案

在AspNetCore中理解依赖注入生命周期冲突与解决方案
L X Y在AspNetCore中理解依赖注入生命周期冲突
在 ASP.NET Core 的日常开发中,依赖注入(Dependency Injection, DI)几乎是贯穿所有功能的核心技术。而在学习 DI 时,
一个非常重要但又容易被忽略的问题就是 服务生命周期(Service Lifetime)之间的冲突。
这篇文章通过一个简单的 后台托管服务(BackgroundService) 示例,来展示不同生命周期的服务在注入时会出现什么问题,以及如何正确解决这种冲突。
生命周期的三种类型
在 ASP.NET Core 中,服务的生命周期有三种类型:
- Transient:每次请求都会创建一个新的实例
- Scoped:每次请求都会创建一个新的实例,但同一个请求中的不同控制器会共享同一个实例
- Singleton:整个应用程序中只有一个实例,所有请求都会共享这个实例
而后台托管服务(BackgroundService)是 ASP.NET Core 中用于在后台运行异步任务的抽象基类。
本文中选择它为案例是因为后台托管任务的生命周期是singleton。
依赖注入的常用方式
在 ASP.NET Core 中,依赖注入的常用方式有三种:
构造函数注入(Constructor Injection)
1
2
3
4
5
6
7
8
9
10
11public class TestHostedService : BackgroundService
{
private readonly ILogger<TestHostedService> _logger;
private readonly IIdGenService _idGenService;
public TestHostedService(ILogger<TestHostedService> logger, IIdGenService idGenService)
{
_logger = logger;
_idGenService = idGenService;
}
}属性注入(Property Injection)
在注册服务时,需要使用 ActivatorUtilities 或 ConfigureServices 配置 才能给它写入属性1
2
3
4
5
6
7
8
9public class MyService
{
public ILogger<MyService>? Logger { get; set; }
public void DoWork()
{
Logger?.LogInformation("Working...");
}
}方法注入(Method Injection)
1
2
3
4
5
6
7
8
9public class MyController : Controller
{
public IActionResult Index([FromServices] IIdGenService idGenService)
{
string id = idGenService.NewGuid();
return Ok(id);
}
}
其中构造函数注入和属性注入是比较常见的,而方法注入则比较少见。建议使用构造函数注入,因为它更方便,更安全。
方法和属性注入在这里就是补充说明一下,因为它们的使用场景比较少,所以这里就不再深入讲解了。
示例代码:一个依赖 IdGenService 的后台定时任务
1 | using Microsoft.EntityFrameworkCore; |
在Program.cs中注册服务
1 | builder.Services.AddHostedService<TestHostedService>(); |
在这段代码中,我们注册了后台托管服务 TestHostedService,并使用不同的生命周期类型注册了 IdGenService。
重点是 IIdGenService 的生命周期不同会带来不同效果。
我们先分别解除注释,看看使用情况:
解除 Scoped 的注释
出现了报错
运行截图:
核心原因是:ASP.NET Core 的 后台托管服务(HostedService)默认是 Singleton(单例)DI 不允许 生命周期长的服务依赖生命周期短的服务
因为这会产生所谓的:捕获(capture)一个比自身短生命周期的依赖,可能导致依赖被提前销毁或状态错误。
例如在后台托管服务中直接注入DbContext,就会出现这种情况。因为DbContext的生命周期默认是Scoped,而导致后台托管服务在使用完这一个DbContext后,却还一直持有,
占用数据库连接,导致数据库连接池耗尽。
Singleton 服务 在整个程序生命周期都活着,它不能依赖随时都可能被释放的 Transient 或 Scoped 服务。
然后我们把Scoped注释掉,再解除Singleton的注释
发现正常运行:
运行截图:
因为这时IdGenService是与托管服务TestHostedService是同一个生命周期,所以没有问题。
上面说了,长的生命周期不允许依赖短的生命周期,那我们猜一下解除 Transient 的注释会发生什么?
运行截图:
(⊙o⊙)?不对,怎么是正常的?
我去查了一下:原来在ASP.NET Core 的 DI 系统中,Transient 是不受生命周期“捕获”规则限制的。
微软官方定义如下(简化):
Transient 服务每次请求都会重新创建,容器允许它被任何生命周期的服务注入。
也就是说:
. Transient 本身没有“作用域”
. 它也不依赖容器来管理生命周期
. 所以不会产生“被提前释放”的风险
因此:
单例依赖 Transient 是安全的(没有生命周期冲突)
那为什么大家都以为 Transient 会报错?
因为很多教程、中文文章、视频讲解都把 Scoped 和 Transient 混在一起,以为这两者都「生命周期短 → 都不能给单例用」。
但实际情况是:
| 生命周期 | 是否允许被 Singleton 依赖? | 原因 |
|---|---|---|
| Singleton | ✔ | 同生命周期 / 更长 |
| Transient | ✔ | 不依赖 Scope,由容器直接 new |
| Scoped | ❌ | 必须在 Scope 内创建(Http Request) |
那如果我们非要在 Singleton 中注入 Scoped 呢?
解决方案
使用 using 和 IServiceScopeFactory 来创建一个临时的 Scoped 作用域
1 | using Microsoft.EntityFrameworkCore; |
这样就解决了,运行截图:









