.NET 8 EF Core NUnit单测无多线程/外部数据库访问仍抛出DbUpdateConcurrencyException
这种问题真的挺磨人的——明明没开多线程、也没其他程序碰数据库,单测里却时不时蹦出DbUpdateConcurrencyException,还没规律,完全摸不着头脑!先把你的问题理清楚:
你在NUnit单测里执行删除操作的步骤是:
- 用
DbSet.Where查询指定名称的记录 - 调用
DbSet.RemoveRange批量删除 - 执行
DbContext.SaveChanges()时抛出异常,错误信息如下:
Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException : The database operation was expected to affect 1 row(s), but actually affected 0 row(s); data may have been modified or deleted since entities were loaded. See 相关文档 for information on understanding and handling optimistic concurrency exceptions.
而且你的环境是.NET 8 EF Core + SQL Server,没有其他外部访问,单测也没异步代码,按说不该出现并发问题才对。结合我之前踩过的坑,给你几个排查方向和解决办法:
1. 先解决延迟执行的坑(最常见!)
EF Core的Where查询默认是延迟执行的——你写Where的时候并没有真的去数据库查数据,直到你遍历结果或者调用ToList()/ToArray()这类方法才会执行。如果你的代码是这样的:
var records = _dbContext.MyEntities.Where(e => e.Name == "TestName"); _dbContext.MyEntities.RemoveRange(records); _dbContext.SaveChanges();
那实际查询数据库是在SaveChanges()的时候才发生的,如果这时候数据库里的记录已经被同一个测试里的其他步骤删掉了(比如之前的清理操作没彻底?),就会出现“预期影响1行实际0行”的错误。
解决办法:强制立即执行查询,把结果拿到内存里再删除:
// 加个ToList(),确保先把数据从数据库查出来 var records = _dbContext.MyEntities.Where(e => e.Name == "TestName").ToList(); _dbContext.MyEntities.RemoveRange(records); _dbContext.SaveChanges();
2. 检查DbContext的生命周期
如果你的单测里复用了同一个DbContext实例,比如在SetUp里创建一次,所有测试都用它,那上下文的缓存可能会搞事情——比如之前的测试操作过实体,上下文里缓存的实体状态和数据库实际状态不一致,导致删除的时候找不到对应记录。
解决办法:每个测试方法都创建全新的DbContext实例,测试结束后及时释放,避免缓存干扰。比如在NUnit的SetUp里创建,TearDown里Dispose:
private MyDbContext _dbContext; [SetUp] public void Setup() { var options = new DbContextOptionsBuilder<MyDbContext>() .UseSqlServer("你的连接字符串") .Options; _dbContext = new MyDbContext(options); } [TearDown] public void TearDown() { _dbContext.Dispose(); }
3. 换个更直接的删除方式(跳过实体加载)
如果加载实体再删除的方式总是出问题,不如直接用EF Core的ExecuteDelete方法——它会直接生成DELETE SQL语句执行,不需要先把实体加载到内存里,既高效又能避免加载和删除之间的间隙问题。
代码示例:
// 直接删除符合条件的记录,不需要加载实体 _dbContext.MyEntities.Where(e => e.Name == "TestName").ExecuteDelete();
这个方法会直接操作数据库,不存在“加载后数据被修改”的情况,大概率能解决你的并发异常。
4. 检查并发令牌配置
如果你的实体配置了并发令牌(比如用[Timestamp]属性,或者在Fluent API里用IsConcurrencyToken()),那EF Core会在更新/删除时检查令牌是否匹配。如果查询到的实体令牌和数据库里的不一致(比如之前的操作修改了令牌但上下文没更新),就会抛出这个异常。
解决办法:
- 如果不需要并发控制,可以去掉并发令牌的配置
- 如果需要,那在删除前调用
_dbContext.Entry(record).Reload()重新加载实体的最新状态,确保令牌一致
快速排查小技巧
- 在删除前加一行
Console.WriteLine($"查到要删除的记录数:{records.Count}"),确认真的查到了数据 - 开启EF Core的SQL日志,看生成的DELETE语句是什么,直接去数据库里执行看看能不能删除成功
备注:内容来源于stack exchange,提问作者user3478180

