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

.NET 7 EF Core单元测试:如何触发DbUpdateConcurrencyException?

如何在.NET 7 EF Core中触发DbUpdateConcurrencyException的单元测试

问题背景

在.NET 7 EF Core中编写单元测试,目标是触发DbUpdateConcurrencyException(异常信息:"数据库操作预期影响1行,但实际影响0行;实体加载后数据可能已被修改或删除。"),从而测试并发问题的处理逻辑。但尝试用不同DbContext实例修改同一实体时,第一个上下文保存修改并未抛出异常,无法达到测试目的。

失败原因

  1. InMemory数据库默认不支持并发检测:InMemory Provider本身不会自动处理乐观并发冲突,必须显式配置并发令牌让EF Core在保存时校验数据版本。
  2. 实体缺少并发令牌配置:原MyObject类没有用于并发检测的属性,EF Core无法判断数据是否被其他上下文修改。
  3. 自定义SaveChangesAsync捕获异常未抛出:重写的SaveChangesAsync方法捕获了DbUpdateConcurrencyException但未重新抛出,导致测试无法捕获到异常。
  4. 测试步骤冗余混乱:原测试中多次重复修改保存,逻辑分散,没有聚焦核心并发场景。

修正方案

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 18:35:28