1.前言
在使用framwork时构造对象都是直接new,本身框架没有提供DI的能力,当然也可以集成Autofac ,因为Autofac 提供了完善的适配器,可以无缝集成到各类项目中,可以看出微软选择不是"修补旧船",而是"建造新航母",随着 .netcore 的发展,依赖注入也是被作为.netcore 平台框架的基础核心功能之一。
带来这个有好处最大的亮点就是依赖倒置,容器帮你管理对象的生命周期,在搭建框架时配置下,你完全不需要操心,只需要在构造函数写入就行了。
cs
internal class SingletonService : ISingletonService
{
public ITransientService Transient { get; }
public Guid SingletonId { get; } = Guid.NewGuid();
public SingletonService(ITransientService transient)
{
Transient = transient;
}
}
相信有大部分小伙伴也是实际应用过的,只要按照项目内人家写的代码风格一样,往构造函数里放就完事了,其他的完全不用操心,框架也是尽可能这么做的,让你省心,但是您是否仔细思考一下您真的对他了解吗?因为新的技术意味着带来新的挑战,要学习新的东西,不然的话
1.假设哪天启动时突然构造函数不能注入了搞半天不知道怎么回事!
2.人家问你单例服务注入瞬时会不会报错,如果你没了解过,或者实际操作过的话,只能靠懵,我就遇到过,不过我不是靠懵,我是斩钉截铁的说,会!,因为慢一秒都代表心虚,底气来源就是,他的枪里没有子弹,(赌他可能也不知道)
接下来就聊聊一些需要注意的点,如果您知道,当看个热闹,如果不知道就当做扩展知识了
2. 三种服务生命周期的定义与行为
Transient(瞬时):每次从容器请求时都创建新实例,就跟每次直接new一样。
Scoped(作用域) :在同一个 IServiceScope 内是单例,不同作用域之间实例不同,一个 Scope 对应一个逻辑上下文(如一次 HTTP 请求、一次后台任务)
同一作用域内:Scoped :服务是单例。Transient :服务每次解析都是新实例,但可共享同一个 Scoped 实例,就是你有2个不同的Transient引用了一个scope的类,在同一作用域2次解析的scope类都会是同一对象 。Singleton:整个应用程序生命周期内只有一个实例,就例如你定义的静态类。
cs
var services = new ServiceCollection();
// 注入瞬时
services.AddTransient<ITransientService, TransientService>();
//注入单例
services.AddSingleton<ISingletonService, SingletonService>();
//注入作用域
services.AddSingleton<IScopedService, ScopedService>();
//构造服务对象
var serviceProvider = services.BuildServiceProvider();
3.生命周期依赖的规则
这里说的就是,你不同生命周期的类,如果要相互引用需要参照并且按照以下的规则,依赖方的生命周期不能长于其依赖的服务,就是说你使用的类如果依赖其他类,那么一定不能存在当前类还存在,依赖的服务已经死了的情况。
短生命周期 → 依赖 → 长生命周期(Transient 依赖 Singleton 就可以)
长生命周期 → 依赖 → 短生命周期(Singleton 依赖 Scoped 或 Transient(有状态时),单例是不能依赖作用域的,依赖瞬时不会报错,但是瞬时推荐要无状态的类)
| 消费者类型 \ 依赖类型 | Transient | Scoped | Singleton |
|---|---|---|---|
| Transient | 允许 | 必须要在作用域内 | 允许 |
| Scoped | 允许 | 允许 | 允许 |
| Singleton | 需要注意(依赖不能长于自身) | 不允许(依赖 Scoped) | 允许 |
我们注意看这个表格中的关系,后面依赖这个展开,当然具体就是围绕3个特殊的来分析,因为容易出错
1.瞬时周期类依赖作用域周期类
2.单例周期类依赖瞬时类
3.单例周期类依赖作用域周期类
4.瞬时周期类依赖作用域周期类
控制台或者后台服务应用
1.根容器(IServiceProvider)不能解析 Scoped 服务,provider根容器,来获取注册为作用域周期的类就直接报错了
2.不能直接注入注册为作用域周期的类,例如我在瞬时的对象中,注入一个scoped周期的类
C#
internal class TransientService2 : ITransientService
{
public Guid Id { get; } = Guid.NewGuid();
public IScopedService Scoped { get; }
public TransientService2(IScopedService scoped)
{
Scoped = scoped;
Console.WriteLine($"[Transient] 构造,ID: {Id},持有 Scoped: {scoped.Id}");
}
}
他就会出现如下错误

WebApi 应用
如果你善于观察,你会发现一个有趣的现象,这样的代码,如果放在api的控制器中注入

我这里分别注入依赖了作用域的瞬时,直接注入作用域,注入根容器,通过根容器获取作用域服务,他竟然全都不会报错。
结论
1.在控制台或者后台服务报错的原因,是因为作用域服务不能在根容器解析,这个如何理解呢?
因为作用域服务的生命周期是绑定在逻辑作用域(如一个 HTTP 请求)上的,每个逻辑操作单元一个实例,实例应该在该作用域内创建、共享、并在作用域结束时被释放。
而根容器(Root ServiceProvider)是应用程序生命周期级别的,它没有作用域的逻辑概念,如果直接从根容器解析作用域服务的话,就会:
1.作用域服务变成"伪单例"(在整个应用程序生命周期内复用,而不是每次请求一个新实例或作用域内共享)如果是DbContext ,那么同一个 DbContext 实例被多个 HTTP 请求、多个线程共享,虽然数据库连接本身可能被复用,但 DbContext 持有连接状态,长时间不释放 → 连接无法归还池 → 新请求无法获取连接 → 超时
2.因为没有真正的作用域上下文,破坏作用域的生命周期管理,不能正确触发作用域服务的释放和 Dispose(),可能造成内存泄漏或数据混乱。
那如何解决这个问题呢?答案就是我们需要通过作用域解析 Scoped 服务
C#
var services = new ServiceCollection();
services.AddScoped<IScopedService, ScopedService>();
services.AddTransient<ITransientService, TransientService2>();
var provider = services.BuildServiceProvider(validateScopes: true);
// 通过作用域解析 Scoped 服务和依赖了 Scoped 的 Transient 服务
using (var scopeed = provider.CreateScope())
{
var scopedProvider = scopeed.ServiceProvider;
var scopedService = scopedProvider.GetRequiredService<ITransientService>();
}
可以发现scopedProvider 和 provider 都是 IServiceProvider,但它们代表的是两个不同层级的 DI 容器实例,生命周期和解析行为完全不同。根IServiceProvider 是整个应用程序级别的。而scopedProvider 是当前using块范围内
区分他们很简单,IServiceProvider 有一个IsScope属性,如果细心调试下会发现,通过scopeed 创建的IServiceProvider 的IsScope为true,根容器IServiceProvider 的IsScope为false

所以得出结论如果在后台服务线程或者控制台中要解析作用域周期对象,或者解析依赖了作用域周期的对象必须使用scopeed 来创建一个根容器来获取。
如果是在web中就不用管,因为微软已经在web框架中的RequestServicesFeature帮我们实现了将IServiceProvider 设为请求级别的 Scoped 根容器,在注入时可以发现它的IsScope为true

可以看到每一次请求创建一个新的作用域
c#
_scope = _scopeFactory.CreateScope();
_requestServices = _scope.ServiceProvider; // 这是请求级别的容器

请求的生命周期执行过程
1.在管道的最开始(或通过 HttpContextFactory),会为这个请求创建一个 HttpContext对象
2.在这个过程中创建一个 请求级别的 IServiceProvider,然后将这个 IServiceProvider赋值给 HttpContext.RequestServices
3.后续不管是中间件、Controller、Page ,可以通过 HttpContext.RequestServices获取到当前请求的 Scoped 服务
注意点
如果是在控制器或者他引用的类中开启了后台线程,尽可能在主线程先获取子线程需要的数据传入,如果不可避免在后台中解析服务,依然还是要用根容器 来创建一个**Scoped**的根容器来获取
C#
[HttpGet("run-task")]
public IActionResult RunBackgroundTask()
{
// 只读取你需要的数据,而不是拿服务对象var data = "一些业务数据";
Task.Run(() =>
{
// 这里不使用任何注入的服务
Console.WriteLine($"Running in background with data: {data}");
});
// 方法2
using (var scope = _serviceProvider.CreateScope())
{
var scopedService = scope.ServiceProvider.GetRequiredService<ScopedService>();
scopedService.DoSomething();
}
return Ok("OK");
}
| 场景 | 根容器 (IServiceProvider) |
如何获取 Scoped 服务 |
|---|---|---|
| Web API | 本身就是 Scoped (IsScope=true) | 直接注入即可 |
| 后台服务/控制台 | 不是 Scoped (IsScope=false) | 必须 provider.CreateScope() |
最后再聊聊开头那个面问题。为啥 Singleton 依赖 Scoped 会报错?说白了,就是因为根容器(Root)本身就是个 Singleton 级别的"大管家",它根本没法直接给你变出一个有"时效性"的 Scoped 对象来。微软搞这套机制,初衷其实很简单:就是让咱们用的时候拿,不用了自动扔。在 Web 里,这个用的时候就是一次请求;在后台服务里,就是咱们手动 using 包裹的那一下。
所以,别管是写 Web 还是写后台,只要死记硬背这一条------根容器不能直接生 Scoped,你就彻底稳了。