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

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。请问:

  1. 在EF Core中,保存前手动更新RowVersion是否为错误方法?
  2. EF Core难道不应该在SaveChanges()时检测到RowVersion已更改并抛出并发异常吗?

解答

核心问题原因

EF Core对标记为IsRowVersion()的属性有特殊处理逻辑:这类属性会被认定为数据库自动管理的列,默认会忽略应用层对该属性的手动修改。生成SQL时,EF依然会使用从数据库加载实体时的原始RowVersion值做并发校验,保存成功后再自动将实体的RowVersion更新为数据库返回的新值。

你看到AutoMapper把实体的Rv改成了前端传来的旧值,但EF内部并未跟踪这个修改——IsRowVersion()的标记已经告诉EF:这个值只能由数据库维护,应用层的修改无效。

正确的并发校验实现

不要通过AutoMapper覆盖RowVersion属性,而是通过EF的实体条目(Entry)手动设置原始值:

  1. 从DTO中取出前端传来的旧RowVersion值
  2. 将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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 20:01:33