You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET 8 EF Core NUnit单测无多线程/外部数据库访问仍抛出DbUpdateConcurrencyException

.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.16 06:54:36