.NET 7 EF Core单元测试:如何触发DbUpdateConcurrencyException?
如何在.NET 7 EF Core中触发DbUpdateConcurrencyException的单元测试
问题背景
在.NET 7 EF Core中编写单元测试,目标是触发DbUpdateConcurrencyException(异常信息:"数据库操作预期影响1行,但实际影响0行;实体加载后数据可能已被修改或删除。"),从而测试并发问题的处理逻辑。但尝试用不同DbContext实例修改同一实体时,第一个上下文保存修改并未抛出异常,无法达到测试目的。
失败原因
- InMemory数据库默认不支持并发检测:InMemory Provider本身不会自动处理乐观并发冲突,必须显式配置并发令牌让EF Core在保存时校验数据版本。
- 实体缺少并发令牌配置:原
MyObject类没有用于并发检测的属性,EF Core无法判断数据是否被其他上下文修改。 - 自定义SaveChangesAsync捕获异常未抛出:重写的
SaveChangesAsync方法捕获了DbUpdateConcurrencyException但未重新抛出,导致测试无法捕获到异常。 - 测试步骤冗余混乱:原测试中多次重复修改保存,逻辑分散,没有聚焦核心并发场景。
修正方案
1. 给实体添加并发令牌
通过特性或Fluent API给实体配置并发令牌,EF Core会在每次保存时自动更新令牌值,并在后续保存时对比版本:
public class MyObject { public int Id { get; set; } public string Name { get; set; } // 用Timestamp特性标记为并发令牌,自动生成版本号 [Timestamp] public byte[] RowVersion { get; set; } }
或者用Fluent API在DbContext中配置:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<MyObject>(entity => { entity.ToTable("MyObject"); // 标记RowVersion为并发令牌 entity.Property(e => e.RowVersion).IsRowVersion(); }); }
2. 修正DbContext的SaveChangesAsync方法
确保捕获异常后要么处理逻辑后重新抛出,要么移除捕获逻辑(测试阶段优先保证异常能传递到测试代码):
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) { try { return await base.SaveChangesAsync(cancellationToken); } catch (DbUpdateConcurrencyException ex) { // 这里添加你的并发冲突处理逻辑 // 测试时必须重新抛出异常,让测试断言能捕获 throw; } }
3. 简化并修正测试逻辑
聚焦核心并发场景:添加实体→另一个上下文修改保存→原上下文修改后保存,触发异常:
public class ExceptionTest { [Fact] public async Task DbUpdateConcurrencyExceptionTest() { // Arrange var options = new DbContextOptionsBuilder<MyContext>() .UseInMemoryDatabase(databaseName: "ConcurrencyTest") .Options; // 第一个上下文添加实体并保存 await using var context1 = new MyContext(options); var myObject = new MyObject { Id = 1, Name = "初始名称" }; context1.MyObjects.Add(myObject); await context1.SaveChangesAsync(); // 第二个上下文加载实体并修改保存(模拟其他客户端操作) await using var context2 = new MyContext(options); var myObject2 = await context2.MyObjects.FindAsync(myObject.Id); myObject2.Name = "第二个上下文修改的名称"; await context2.SaveChangesAsync(); // Act & Assert:原上下文修改后保存,触发并发异常 myObject.Name = "第一个上下文修改的名称"; await Assert.ThrowsAsync<DbUpdateConcurrencyException>(() => context1.SaveChangesAsync()); } }
原理说明
配置并发令牌后,EF Core在保存实体时会将上下文跟踪的RowVersion值与数据库中的值对比:如果不一致,说明实体已被其他上下文修改,就会抛出DbUpdateConcurrencyException,InMemory数据库会模拟这一校验逻辑,满足单元测试需求。
内容的提问来源于stack exchange,提问作者falowil
相关产品推荐
相关产品推荐

