dbcontext本身不是unitofwork,直接将其当iunitofwork返回会导致90%事务失效;真正起效的是共享同一dbcontext实例与事务上下文,仓储应暴露语义化方法而非直接暴露set(),且iunitofwork必须与dbcontext严格同生命周期注册。

DbContext 本身不是 UnitOfWork,直接把它当 IUnitOfWork 接口返回,90% 的事务会失效。 真正起作用的不是“有没有调用 SaveChanges”,而是“所有操作是否共享同一个 DbContext 实例 + 同一个事务上下文”。
为什么 DbContext.Set() 不能当仓储入口用
很多初学者把 IUnitOfWork 设计成只暴露 Set<t>()</t> 方法,业务层直接写 _uow.Set<order>().Add(order)</order> —— 这等于把 EF 内部细节全捅给上层,后果很实在:
- 测试时无法 mock
Set<t>()</t>返回值,一写单元测试就卡住 - 仓储逻辑被绕过,
SaveChanges()前没法统一做审计、软删除标记、并发检查 - 多个仓储各自调用
Set<t>()</t>,但没共用变更追踪器(ChangeTracker),CompleteAsync()可能只提交了部分实体
正确做法是:仓储只暴露语义化方法,比如 CreateOrderAsync(Order order)、DecreaseStockAsync(int productId, int count),内部通过 _uow.Context.Set<t>()</t> 操作,且所有仓储构造函数必须接收 IUnitOfWork,而非 DbContext。
IUnitOfWork 生命周期必须和 DbContext 严格对齐
ASP.NET Core 中注册方式错误,是事务静默失败的头号原因。常见错误配置:
-
services.AddDbContext<appdbcontext>()</appdbcontext>默认是Scoped,但services.AddScoped<iunitofwork unitofwork>()</iunitofwork>却没确保它和 DbContext 在同一 Scope 实例中 - 手动 new ServiceScope() 后 Resolve 出的
IUnitOfWork和 Controller 注入的不是同一个实例 - 在
Task.Run或后台线程里调用CompleteAsync(),此时DbContext已被释放或处于不同 Scope
必须这样注册:
services.AddDbContext<appdbcontext>(options =>
options.UseSqlServer(connectionString));
services.AddScoped<iunitofwork unitofwork>();</iunitofwork></appdbcontext>
且 UnitOfWork 构造函数必须接收 AppDbContext,而不是自己 new 或从 IServiceProvider 解析 —— 否则 Scoped 生命周期就断了。
事务未回滚?先查 SaveChanges 是否真在异常路径上执行
现象:抛了异常,但数据库里数据已写入。这不是 UoW 写得不对,而是事务根本没生效。排查点:
- 是否在
try外提前调用了await _uow.CompleteAsync()?一旦成功提交,后续异常也救不回来 - 是否在
catch块里忘了调用_uow.RollbackAsync()?EF Core 不自动回滚,必须显式调用context.Database.CurrentTransaction?.RollbackAsync() - 是否用了
TransactionScope但没加TransactionScopeOption.Required?默认是Required,但如果嵌套使用且没传参,可能降级为无事务上下文 - 是否在仓储方法里偷偷调了
context.SaveChanges()?这会提前提交,UoW 的CompleteAsync()就成了空操作
推荐事务封装方式:
public async Task<bool> CompleteAsync()
{
try
{
await _context.SaveChangesAsync();
return true;
}
catch
{
await _context.Database.CurrentTransaction?.RollbackAsync();
throw;
}
}</bool>
并发更新冲突必须由 UnitOfWork 层统一拦截
扣库存超卖、订单重复提交这类问题,不是靠“加锁”或“前端防重”能根治的。EF Core 的并发令牌([ConcurrencyCheck] 或 IsRowVersion())只有在 SaveChanges 时才触发,所以必须让所有写操作都经过同一个 CompleteAsync() 入口。
典型错误是:库存校验走查询(IReadOnlyRepository),扣减走另一个仓储,中间没加锁也没版本比对 —— 查询到库存充足,但另一请求已扣完,本请求仍会成功写入。
正确做法:
- 所有涉及状态变更的操作,必须在同一 UoW 下完成(即同一次 HTTP 请求内)
- 在
CompleteAsync()前,统一做乐观并发检查,例如:if (context.ChangeTracker.Entries().Any(e => e.State == EntityState.Modified && e.Property("Version").IsModified)) - 必要时,在仓储方法里用
ExecuteSqlRaw加数据库行锁(如SELECT ... FOR UPDATE),但仅限高竞争场景,别滥用
最易被忽略的一点:UoW 不是银弹。单表 CRUD、只读报表、异步日志写入这些场景,硬套 UoW 反而增加事务开销和死锁风险。该用 IReadOnlyRepository 就用,该用独立 DbContext 就独立 —— 边界感比模式本身更重要。











