EF Core手动修改实体RowVersion为何无法触发并发异常?
并发问题排查:EF Core未检测到RowVersion并发冲突
场景概述
当两名用户先后尝试保存同一条记录时,EF Core未抛出并发异常,允许两次操作都成功。技术栈:前端Angular,后端EF Core 7数据库优先模式。
数据行包含名为Rv的RowVersion属性,后端存储为byte[],前端存储为ArrayBuffer。
代码片段
后端C#实体类
public partial class User { public int Id { get; set; } public byte[] Rv { get; set; } // 其他属性... }
前端Angular接口
export interface User { id: number; Rv: ArrayBuffer; // 其他属性... }
用户A的保存流程
Chrome开发者工具显示请求中RV的值为"AAAAAAAAEBE="
public async Task<ActionResult> Save(User dto) { var entity = await context.Users.FindAsync(id); // 此时实体的RowVersion = byte[8] = 0,0,0,0,0,0,16,17 mapper.Map(dto, entity); // 使用AutoMapper更新实体所有属性 // 现在实体的RowVersion = byte[8] = 0,0,0,0,0,0,16,17 await context.SaveChangesAsync(); // 生成的SQL: UPDATE [Users] SET LastName = 'ABC' WHERE [Id] = 22 AND [RV] = '0x0000000000001011' // 现在实体的RowVersion = byte[8] = 0,0,0,0,0,0,16,18 }
用户B的保存流程
Chrome开发者工具显示请求中RV的值为"AAAAAAAAEBE="
public async Task<ActionResult> Save(User dto) { var entity = await context.Users.FindAsync(id); // 此时实体的RowVersion = byte[8] = 0,0,0,0,0,0,16,18 mapper.Map(dto, entity); // 使用AutoMapper更新实体所有属性 // 现在实体的RowVersion = byte[8] = 0,0,0,0,0,0,16,17 await context.SaveChangesAsync(); // 生成的SQL: UPDATE [Users] SET LastName = 'XYZ' WHERE [Id] = 22 AND [RV] = '0x0000000000001012' // 现在实体的RowVersion = byte[8] = 0,0,0,0,0,0,16,19 }
DbContext配置(反向工程生成)
entity.Property(e => e.Rv) .IsRequired() .IsRowVersion() .IsConcurrencyToken() .HasColumnName("RV");
疑问
AutoMapper已将实体的Rv属性正确映射为请求DTO中的值,但生成的SQL并未使用这个更新后的值,而是用了从数据库查询出来的最新RowVersion。请问:
- 在EF Core中,保存前手动更新
RowVersion是否为错误方法? - EF Core难道不应该在
SaveChanges()时检测到RowVersion已更改并抛出并发异常吗?
解答
核心问题原因
EF Core对标记为IsRowVersion()的属性有特殊处理逻辑:这类属性会被认定为数据库自动管理的列,默认会忽略应用层对该属性的手动修改。生成SQL时,EF依然会使用从数据库加载实体时的原始RowVersion值做并发校验,保存成功后再自动将实体的RowVersion更新为数据库返回的新值。
你看到AutoMapper把实体的Rv改成了前端传来的旧值,但EF内部并未跟踪这个修改——IsRowVersion()的标记已经告诉EF:这个值只能由数据库维护,应用层的修改无效。
正确的并发校验实现
不要通过AutoMapper覆盖RowVersion属性,而是通过EF的实体条目(Entry)手动设置原始值:
- 从DTO中取出前端传来的旧RowVersion值
- 将EF跟踪的实体原始RowVersion值设置为这个旧值,确保生成UPDATE语句时用该旧值作为WHERE条件的一部分
修改后的Save方法示例:
public async Task<ActionResult> Save(User dto) { var entity = await context.Users.FindAsync(id); // 关键操作:手动设置EF跟踪的原始RowVersion值为前端传来的旧版本 context.Entry(entity).Property(e => e.Rv).OriginalValue = dto.Rv; // 映射时排除RowVersion属性,避免AutoMapper覆盖 mapper.Map(dto, entity, opt => opt.Exclude(e => e.Rv)); try { await context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { // 处理并发冲突:返回提示给前端 return Conflict("该记录已被其他用户修改,请刷新后重试"); } return Ok(entity); }
补充说明
IsRowVersion()是IsConcurrencyToken()的特例:标记为RowVersion的属性自动成为并发令牌,且数据库会自动更新其值(如SQL Server的timestamp/rowversion类型)。- 必须排除AutoMapper对RowVersion的映射,避免干扰EF的并发校验逻辑。
内容的提问来源于stack exchange,提问作者Pavel
相关产品推荐
相关产品推荐

