C#怎么处理EFCore并发更新_C# RowVersion时间戳冲突解决【进阶】
先直接说一个核心观点:DbUpdateConcurrencyException 这个异常,与其说它是“报错”,不如把它理解为 EF Core 跟你打了个招呼,提醒你“有人动过你的数据了,你自己决定接下来怎么办”。如果你不捕获它、不配置并发令牌,那所谓的“乐观锁”策略其实根本没有生效。 可以把这个异常
先直接说一个核心观点:DbUpdateConcurrencyException 这个异常,与其说它是“报错”,不如把它理解为 EF Core 跟你打了个招呼,提醒你“有人动过你的数据了,你自己决定接下来怎么办”。如果你不捕获它、不配置并发令牌,那所谓的“乐观锁”策略其实根本没有生效。

可以把这个异常看作一次“介于成功与失败之间的汇报”。它只会在两个条件同时满足时才会被抛出:数据库中那一行的 RowVersion 值,与你加载时获取的值对不上号;并且,这个字段在 EF Core 里被正确标记成了并发令牌。很多开发者遇到这个异常就慌了神,但往往问题出在以下几个细节上:
- 实体类上虽然打了
[Timestamp]属性,但字段类型写错了——必须是byte[],写成string或int都没用。 - 用 Fluent API 配置时,只写了
.HasDefaultValueSql("..."),却漏掉了最关键的.IsRowVersion()。 - 数据库迁移没跑通,导致表里的
RowVersion字段是NULL,SQL Server 根本不会自动生成值。 - 手动给
RowVersion赋了值(比如entity.RowVersion = new byte[8]),等于自己破坏了数据库的自动维护机制。
怎么从异常里拿到真正有用的值
ex.Entries 是唯一的入口,每个 EntityEntry 会暴露三套状态,这三套数据加起来,就是你做决策的全部依据:
- OriginalValues:你调用
FindAsync或FirstOrDefault时从数据库读出来的旧数据,包含了旧的RowVersion。 - CurrentValues:你修改完、准备提交的新值,这里不含新的
RowVersion—— 那是 EF Core 自动生成的活儿。 - DatabaseValues:此刻数据库里真实的最新状态。注意,这个值需要异步获取:
await entry.GetDatabaseValuesAsync()。
这里必须提醒一个很容易忽略的点:DatabaseValues 可能返回 null —— 说明这条记录已经被删了。所以别急着调 .ToObject(),先判空。另外,千万别图省事在同步方法里用 .GetDatabaseValues(),那个重载已经被标记为过时,而且会卡死。
重试合并时最容易踩的三个坑
经典的处理策略是“拉最新数据 → 合并修改 → 重试提交”,但在实际代码中,经常看到开发者掉进这些坑里:
- 没重设
OriginalValues:只调了SetValues(databaseValues)还远远不够,必须显式调用entry.OriginalValues.SetValues(databaseValues),否则下一次提交时,EF Core 拿的还是一个过期的RowVersion去比对,结果可想而知。 - 盲目覆盖所有字段:如果用户只改了
Name,但DatabaseValues里的Price在过程中被别人动过,你用SetValues一股脑全设回去,等于把别人的修改给抹平了。正确的做法是只合并用户本次操作的字段。 - 忽略了主从表的联动:比如一个
Product关联多个OrderItem。主表冲突后,子表的OriginalValues没跟着同步刷新,二次提交仍然失败。这才是真正的“连环坑”。
RowVersion 字段在 SQL Server 之外的问题
需要明确一个事实:[Timestamp] 是 SQL Server 特有的语义。换到 PostgreSQL,你得用 bytea 类型搭配 .IsRowVersion();而到了 SQLite,它根本不支持自动 rowversion,只能退而求其次,改用 [ConcurrencyCheck] 手动维护一个 int 版本号。如果你的项目要跨多数据库,那最好在 OnModelCreating 里统一用 Fluent API 来配置,别依赖特性标注——否则 [Timestamp] 在 PostgreSQL 的迁移过程中会静默失效,而你还浑然不觉。


































