dbupdateconcurrencyexception是ef core的乐观并发保护机制,仅在rowversion不匹配且正确配置并发令牌时抛出;需通过ex.entries获取originalvalues、currentvalues和databasevalues来实现安全重试合并。

直接抛 DbUpdateConcurrencyException 不是失败,而是 EF Core 在告诉你:“这行数据已变,你得决定怎么处理”。不捕获它、不配置 [Timestamp] 或 .IsRowVersion(),所谓的“乐观锁”就等于没开。
为什么 SaveChanges() 会突然抛 DbUpdateConcurrencyException
这个异常只在两个条件同时满足时才会出现:数据库里那行的 RowVersion 值和你加载时拿到的不一致,且该字段被 EF Core 正确标记为并发令牌。常见漏点:
- 实体类加了
[Timestamp],但字段类型不是byte[](比如写成string或int) - 用了 Fluent API 配置,却漏掉
.IsRowVersion(),只写了.HasDefaultValueSql("...") - 数据库迁移没生效——
RowVersion字段在表里实际是NULL或缺失,SQL Server 不会自动生成值 - 手动给
RowVersion赋值(比如entity.RowVersion = new byte[8]),破坏了数据库自动维护机制
怎么从 DbUpdateConcurrencyException 里拿到真正有用的值
ex.Entries 是唯一入口,每个 EntityEntry 提供三套状态:
-
OriginalValues:你调用FindAsync或FirstOrDefault时从 DB 读出来的旧值(含旧RowVersion) -
CurrentValues:你改完、准备提交的新值(不含新RowVersion,EF Core 会自动填) -
DatabaseValues:此刻 DB 里真实最新的整行数据(需异步查:await entry.GetDatabaseValuesAsync())
注意:DatabaseValues 可能为 null(记录已被删),别直接 .ToObject(),先判空;也别在同步方法里用 .GetDatabaseValues()(已弃用,会卡死)。
重试合并时最容易错的三件事
典型策略是“拉最新数据 + 合并修改 + 重试提交”,但实操中常踩坑:
- 没重设
OriginalValues:只调SetValues(databaseValues)不够,必须显式执行entry.OriginalValues.SetValues(databaseValues),否则下次提交还是拿旧RowVersion去比 - 盲目覆盖所有字段:如果用户只改了
Name,但DatabaseValues里Price也被别人动过,直接SetValues就等于把别人的Price改回去了——应只合并用户本次操作的字段 - 忽略并发链式更新:一个
Product关联多个OrderItem,主表冲突后,子表的OriginalValues没同步刷新,二次提交仍失败
RowVersion 字段在 SQL Server 外的注意事项
[Timestamp] 是 SQL Server 特有语义,其他数据库不认。PostgreSQL 要用 bytea 类型 + .IsRowVersion();SQLite 根本不支持自动 rowversion,得退化为 [ConcurrencyCheck] 手动维护 int 版本号。跨数据库项目务必在 OnModelCreating 里统一用 Fluent API 配置,别依赖特性——[Timestamp] 在 PostgreSQL 迁移时会静默失效。











