DDD + Clean Architecture 下 EF 多线程最佳实践
DDD + Clean Architecture 下 EF 多线程最佳实践
在复杂系统(尤其是 DDD + Clean Architecture 架构)中,多线程使用 EF Core 是一个非常容易踩坑的问题。
很多文章会直接推荐:
services.AddDbContextFactory<AppDbContext>();
然后在并行任务中:
using var db = _factory.CreateDbContext();
看起来合理。
但如果你正在构建:
- DDD 分层架构
- CQRS
- UnitOfWork
- DomainEvents
- 多租户系统
- 后台调度服务(Scheduler)
那么事情并没有这么简单。
一、问题的本质
EF Core 的 DbContext:
- ❌ 不是线程安全的
- ❌ 不能跨线程共享
- ❌ 不允许并发操作
官方建议:
每个工作单元使用独立 DbContext。
在 Web 请求中:
HTTP Request
↓
Scoped DbContext
↓
结束释放
完全没问题。
问题出在:
- BackgroundService
- Quartz Job
- Task.WhenAll
- Parallel.ForEachAsync
这些非 HTTP 生命周期场景。
二、两种方案对比
方案一:IDbContextFactory
注册:
services.AddDbContextFactory<AppDbContext>();
使用:
using var db = _factory.CreateDbContext();
优点
- 线程安全
- 简单
- 官方推荐
架构问题
在 DDD + Clean Architecture 下,它会带来几个隐患:
- 破坏 Scoped 生命周期语义
- Repository / UoW 难以统一管理
- Application 层可能被迫感知 Infrastructure
- DomainEvents 生命周期失控
如果你的 CommandHandler 直接创建 DbContext:
Application → Infrastructure(直接依赖)
分层就被打穿了。
方案二(推荐):为每个并行任务创建独立 Scope
注册方式仍然保持标准:
services.AddDbContext<AppDbContext>(options =>
{
options.UseNpgsql(configuration.GetConnectionString("postgres"));
});
在多线程场景中:
public class MyBackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public MyBackgroundService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public async Task RunAsync()
{
await Task.WhenAll(
Enumerable.Range(0, 5)
.Select(_ => ExecuteAsync())
);
}
private async Task ExecuteAsync()
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider
.GetRequiredService<AppDbContext>();
// 依然可以通过 Repository / UoW
await db.SaveChangesAsync();
}
}
三、为什么 Scope 才是架构级解法?
1️⃣ 生命周期一致
无论是:
- Web 请求
- 后台任务
- 调度服务
- 并行处理
模型都变成:
Scope
↓
DbContext
↓
Repository
↓
UnitOfWork
架构完全一致。
2️⃣ 保持 Clean Architecture 纯度
分层依然保持:
Domain
Application
Infrastructure
Web / Worker
Application 层:
- 不直接创建 DbContext
- 不依赖 Factory
- 只依赖抽象 Repository
边界干净。
3️⃣ CQRS 更自然
在命令处理中:
CommandHandler
↓
Repository
↓
DbContext
如果用 Factory:
- Handler 直接创建 DbContext
- UoW 无法统一
- 事务管理分散
如果用 Scope:
- 依赖注入自动完成
- 事务一致
- DomainEvents 正常派发
四、性能问题?
很多人担心:
CreateScope 会不会很重?
答案是:不会。
Scope 只是创建一个轻量级子容器。
真正耗时的是:
- SQL
- 网络 IO
- 磁盘
- 序列化
而不是 Scope。
五、什么时候可以用 IDbContextFactory?
适合场景:
- Blazor WASM
- 控制台工具
- 简单数据导入程序
- 无复杂分层约束
不适合:
- 严格 DDD
- 多模块 CQRS
- 多租户系统
- 复杂 UoW 管理系统
六、最终建议(架构总结)
在 DDD + Clean Architecture 中:
✅ Web 请求
由 ASP.NET 自动创建 Scope。
✅ 后台并行任务
手动创建 Scope。
❌ 不推荐
在 Application 层直接使用 DbContextFactory。
七、一句话总结
在复杂系统中:
多线程问题本质不是“如何创建 DbContext”, 而是“如何保持生命周期的一致性”。
DbContextFactory 解决线程安全。
Scope 解决架构一致性。
在架构型系统中:
生命周期 > 线程安全
如果你正在构建一个:
- 多租户
- DDD
- CQRS
- Scheduler 微服务
- AI 向量入库任务
请优先选择:
IServiceScopeFactory.CreateScope()
这才是可持续演进的解法。