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

使用UseInMemoryDatabase时ConcurrencyCheck并发检查失效求助

解决InMemoryDatabase不触发RowVersion并发异常的问题

这确实是InMemory数据库的一个常见局限——它并没有完全模拟SQL Server对[ConcurrencyCheck]和[DatabaseGenerated(Computed)]属性的处理逻辑,所以修改RowVersion后调用SaveChanges不会抛出预期的并发异常。不用切换到真实SQL Server,有两个靠谱的方案可以解决:

方案1:手动在DbContext中模拟并发校验与RowVersion更新

因为InMemory不会自动生成计算列的RowVersion值,也不会对比版本号触发并发异常,我们可以重写DbContext的SaveChanges方法,手动实现这个逻辑:

首先定义一个接口来统一标记带有RowVersion的实体:

public interface IRowVersionedEntity
{
    byte[] RowVersion { get; set; }
}

然后让你的实体类实现这个接口(比如你的实体已经有RowVersion属性,只需要加接口实现即可):

public class YourEntity : IRowVersionedEntity
{
    [Required]
    [ConcurrencyCheck]
    [DatabaseGenerated(DatabaseGeneratedOption.Computed)]
    public byte[] RowVersion { get; set; }
    // 其他属性...
}

接下来重写DbContext的SaveChanges和SaveChangesAsync方法:

public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
    HandleRowVersions();
    return base.SaveChanges(acceptAllChangesOnSuccess);
}

public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
    HandleRowVersions();
    return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}

private void HandleRowVersions()
{
    var entries = ChangeTracker.Entries<IRowVersionedEntity>()
        .Where(e => e.State is EntityState.Added or EntityState.Modified);

    foreach (var entry in entries)
    {
        if (entry.State == EntityState.Added)
        {
            // 为新增实体生成初始RowVersion值
            entry.Entity.RowVersion = Guid.NewGuid().ToByteArray();
        }
        else
        {
            // 模拟并发校验:对比当前实体的RowVersion与数据库中的版本
            var databaseValues = entry.GetDatabaseValues();
            if (databaseValues == null) continue;

            var dbRowVersion = (byte[])databaseValues[nameof(IRowVersionedEntity.RowVersion)];
            if (!entry.Entity.RowVersion.SequenceEqual(dbRowVersion))
            {
                throw new DbUpdateConcurrencyException("并发冲突:该实体已被其他操作修改,请刷新后重试。");
            }

            // 更新RowVersion为新值,模拟数据库自动生成的行为
            entry.Entity.RowVersion = Guid.NewGuid().ToByteArray();
        }
    }
}

这样修改后,当你手动修改实体的RowVersion再调用SaveChanges时,就会触发我们手动抛出的DbUpdateConcurrencyException,和SQL Server的行为一致。

方案2:改用Sqlite内存数据库替代InMemory

如果不想手动写逻辑,Sqlite的内存模式是更好的选择——它比InMemory更贴近真实数据库的行为,支持自动处理RowVersion的生成和并发校验。

配置步骤:

  1. 先安装Sqlite的EF Core包:
Install-Package Microsoft.EntityFrameworkCore.Sqlite
  1. 在单元测试中配置DbContext使用Sqlite内存数据库:
// 创建Sqlite内存连接,必须保持连接打开否则数据会丢失
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();

var dbOptions = new DbContextOptionsBuilder<YourDbContext>()
    .UseSqlite(connection)
    .Options;

// 使用配置好的选项创建DbContext
using var context = new YourDbContext(dbOptions);
// 初始化数据库结构
context.Database.EnsureCreated();

之后你的单元测试逻辑和之前用InMemory时几乎一样,但Sqlite会自动处理RowVersion的生成和并发校验——当你修改RowVersion后调用SaveChanges,会像SQL Server一样抛出DbUpdateConcurrencyException。

为什么选Sqlite内存模式?

  • 它支持更多数据库级别的特性,比InMemory更适合测试真实的EF Core行为
  • 速度足够快,不会影响单元测试的执行效率
  • 不需要额外的数据库实例,完全在内存中运行

总结

  • 如果想继续使用InMemory数据库,手动重写SaveChanges模拟并发逻辑是可行的
  • 如果希望更贴近真实数据库的行为,减少手动代码,Sqlite内存模式是更省心的选择

内容的提问来源于stack exchange,提问作者T. Czubaszek

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.22 09:43:10