DbContext 是由 Root Provider 创建的,为什么不能解析 Scoped Provider?
在 ASP.NET Core + EF Core 的项目中,经常会遇到这样一个看似“反直觉”的结论:
DbContext 的“创建规则”由 Root Provider 持有, 但 DbContext 的“实例”必须始终由 Scope Provider 创建和管理。
很多问题——
包括 Domain Event 分发、后台任务、并发异常、生命周期错乱——
本质都源于对这句话理解不够深入。
这篇文章把这个问题一次讲清楚。
一、先说结论(非常重要)
DbContext 的生命周期规则只有一条:
DbContext 必须始终运行在一个明确的 Scope 中,而不能依赖 Root Provider 的隐式 Scope。
如果你在 Root Provider 上直接解析 DbContext,或者让 DbContext 反向依赖 Scoped 服务,一定会出问题。
二、Root Provider 和 Scope Provider 到底是什么
1️⃣ Root Provider(根容器)
- 应用启动时创建
- 生命周期贯穿整个进程
- 只能安全解析:
- Singleton
- Transient(但 Transient 如果依赖 Scoped 也会炸)
Application Start
└── Root ServiceProvider
2️⃣ Scope Provider(作用域容器)
-
通常由以下场景创建:
- HTTP 请求
- BackgroundService
- 手动 CreateScope()
-
用来管理 Scoped 生命周期
HTTP Request
└── IServiceScope
└── Scoped ServiceProvider
三、DbContext 的真实创建方式
虽然我们通常这样注册:
services.AddDbContext<AppDbContext>();
但 DbContext 并不是“随便就能 new 出来” 的。
实际行为是:
- DbContext 是 Scoped
- 只有在 Scope Provider 中才能被安全解析
- Root Provider 没有资格管理 DbContext 的生命周期
所以这段代码是危险的:
var db = rootProvider.GetRequiredService<AppDbContext>(); // ❌
原因不是语法,而是 生命周期语义错误。
四、为什么 DbContext 不能反向依赖 Scoped Provider
很多人在 DDD / CQRS 场景下会写出类似代码:
public class SomeService
{
public SomeService(AppDbContext db, IOtherScopedService service)
{
}
}
如果这个 SomeService 是:
- Singleton
- 或者由 Root Provider 创建
那么问题就来了。
本质原因只有一句话:
Root Provider 无法感知 Scoped 生命周期的开始与结束。
一旦 Root Provider 解析了 Scoped 对象,就会导致:
- DbContext 被“提升”为全局共享
- 并发请求共用同一个 DbContext
- 随机异常、脏数据、线程安全问题
五、这也是为什么 DomainEventDispatcher 要 CreateScope
你前面遇到的那个经典问题,其实正好印证了这一点:
为什么 DomainEventDispatcher 里必须重新 CreateScope?
答案很简单:
-
Domain Event 可能在:
- 命令执行后
- 事务提交后
- 非 HTTP 上下文中触发
-
此时 没有现成的 Scope
所以正确写法一定是:
using var scope = _serviceScopeFactory.CreateScope();
var handlers = scope.ServiceProvider.GetServices<IDomainEventHandler<T>>();
这是在显式声明生命周期边界。
六、一个非常重要的工程原则
可以把规则总结成一句工程铁律:
谁创建 Scope,谁负责 Scope 内的一切依赖。
因此:
| 场景 | 正确做法 |
|---|---|
| HTTP 请求 | 框架自动创建 Scope |
| Command / Query | 在 Application 层执行 |
| Domain Event | Dispatcher 内部 CreateScope |
| Background Job | 手动 CreateScope |
| DbContext | 永远只活在 Scope 内 |
七、这和 DDD / CQRS 的关系
这也是为什么在干净的架构里:
-
Domain 层 不注入 DbContext
-
Domain 层 不感知 DI
-
Application / Infrastructure 才负责:
- Scope
- DbContext
- 事务
- 事件分发
生命周期不是细节,而是架构的一部分。
八、总结一句话
DbContext 不是不能用, 是 不能被 Root Provider 用错方式用。
一旦你开始用 Scope 明确边界 的方式设计系统, 很多“莫名其妙的问题”都会自动消失。