使用UseInMemoryDatabase时ConcurrencyCheck并发检查失效求助
这确实是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的生成和并发校验。
配置步骤:
- 先安装Sqlite的EF Core包:
Install-Package Microsoft.EntityFrameworkCore.Sqlite
- 在单元测试中配置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

