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

.NET 8+EF+SQL Server中rowversion并发处理的最优方案咨询

EF + SQL Server RowVersion 并发控制最优实现方案

首先要明确:EF 本身不会自动用你传入的旧 RowVersion 值做并发校验,必须手动配置原始值(OriginalValue),但可以通过更简洁的方式实现,无需依赖拦截器或让映射器耦合上下文。

第一步:正确配置 RowVersion 属性

确保实体的 Version 字段被 EF 识别为 SQL Server 的 rowversion(即 timestamp 类型),这是并发校验的基础:

方式1:DataAnnotations

public class Product
{
    public string Id { get; set; }
    public string DescrLg { get; set; }
    
    [Timestamp]
    public byte[] Version { get; set; }
}

方式2:Fluent API

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Product>()
        .Property(p => p.Version)
        .IsRowVersion(); // 等价于配置为 DatabaseGeneratedOption.Computed + 并发令牌
}

第二步:最优更新实现方案

根据你的场景(从前端接收 ID、新描述、旧 RowVersion),推荐以下几种简洁且解耦的实现:

方案1:查询实体 + 手动设置原始版本值

这是最直观的方式,适合需要先读取实体其他字段的场景:

public async Task<Product> UpdateAsync(string id, byte[] oldVersion, string newDescription, CancellationToken cancellationToken)
{
    // 查询跟踪状态的实体
    var product = await context.Products.FirstAsync(e => e.Id == id, cancellationToken);
    
    // 更新业务字段
    product.DescrLg = newDescription;
    
    // 关键:将传入的旧版本设置为属性的原始值,EF 会用这个值和数据库做对比
    context.Entry(product).Property(p => p.Version).OriginalValue = oldVersion;

    try
    {
        await context.SaveChangesAsync(cancellationToken);
        // SaveChanges 后,EF 会自动把数据库生成的最新 Version 赋值给实体
        return new Product(product.Id, product.DescrLg, product.Version);
    }
    catch (DbUpdateConcurrencyException)
    {
        // 处理并发冲突:比如检查实体是否存在,或返回自定义提示
        if (!await context.Products.AnyAsync(p => p.Id == id, cancellationToken))
        {
            throw new KeyNotFoundException("产品不存在");
        }
        throw new InvalidOperationException("产品已被其他用户修改,请刷新后重试");
    }
}

方案2:无跟踪查询 + 手动附加(减少上下文跟踪开销)

如果不需要上下文跟踪查询到的实体,用无跟踪查询后手动附加,适合高并发场景:

public async Task<Product> UpdateAsync(string id, byte[] oldVersion, string newDescription, CancellationToken cancellationToken)
{
    // 无跟踪查询,避免上下文缓存旧版本
    var product = await context.Products.AsNoTracking().FirstAsync(e => e.Id == id, cancellationToken);
    product.DescrLg = newDescription;

    // 附加到上下文并标记需要更新的字段
    context.Attach(product);
    context.Entry(product).Property(p => p.DescrLg).IsModified = true;
    
    // 设置原始版本值用于并发校验
    context.Entry(product).Property(p => p.Version).OriginalValue = oldVersion;

    try
    {
        await context.SaveChangesAsync(cancellationToken);
        return new Product(product.Id, product.DescrLg, product.Version);
    }
    catch (DbUpdateConcurrencyException)
    {
        // 冲突处理逻辑同上
        throw;
    }
}

方案3:直接构造实体(无需查询数据库,性能最优)

如果不需要读取实体其他字段,直接构造实体完成更新,减少一次数据库查询:

public async Task<Product> UpdateAsync(string id, byte[] oldVersion, string newDescription, CancellationToken cancellationToken)
{
    var product = new Product
    {
        Id = id,
        DescrLg = newDescription,
        Version = oldVersion // 这里只是临时赋值,关键是下面的 OriginalValue 设置
    };

    context.Attach(product);
    // 仅标记业务字段为已修改
    context.Entry(product).Property(p => p.DescrLg).IsModified = true;
    // 设置原始版本值
    context.Entry(product).Property(p => p.Version).OriginalValue = oldVersion;

    try
    {
        await context.SaveChangesAsync(cancellationToken);
        // SaveChanges 后 EF 会自动更新实体的 Version 为最新值
        return new Product(product.Id, product.DescrLg, product.Version);
    }
    catch (DbUpdateConcurrencyException)
    {
        // 冲突处理逻辑同上
        throw;
    }
}

关键说明

  1. 为什么不能直接设置 product.Version = oldVersion?
    当上下文跟踪实体时,会自动记录查询到的 Version 作为原始值,如果你直接修改当前值,EF 生成 Update 语句时仍会用原始值和数据库对比,无法触发并发校验。必须手动覆盖 OriginalValue。
  2. 拦截器方案的问题:
    拦截器虽然能实现,但增加了复杂度,且不如直接在服务层设置 OriginalValue 直观、易维护。
  3. 解耦映射器:
    上述方案中,映射器只需要负责构造实体或传递字段值,无需依赖 EF 上下文,完全解耦。

内容的提问来源于stack exchange,提问作者Roelant M

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 15:47:12