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

ASP.NET Core 5中C# 9 records配合EF Core的追踪冲突解决

问题根因

这个异常和C# record类型本身没有兼容问题,完全是EF Core实体追踪机制的逻辑导致的:

  • 你调用FindAsync获取实体时,EF Core默认开启变更追踪,已经把返回的Product实例纳入内部追踪缓存,缓存以实体主键为键,不允许同一个DbContext实例下存在两个同主键的被追踪实体。
  • 你使用with表达式修改record时,会生成一个全新的内存实例,原来被追踪的旧实例依然留在缓存中,没有被释放。这时候你调用db.Update(新实例),EF Core会发现缓存中已经存在同主键的追踪实体,直接抛出冲突异常。
可落地的解决方案

根据你对record不可变性的设计要求,可以选下面任意一种方案,不需要强制改回普通类作为实体:

方案1:直接利用现有追踪逻辑,减少不必要的实例创建

绝大多数常规更新场景,你根本不需要生成新的record实例,也不需要手动调用db.Update()——EF Core会自动识别被追踪实体的属性变更,在SaveChanges时生成对应的更新SQL。
如果你的record定义允许修改属性,直接调整写法即可:

[HttpGet("test/{id}")]
public async Task<Product> ExampleControllerAction(int id, CancellationToken cancellationToken)
{
    string newName = "test new name!!";
    // 拿到的实体是被追踪状态
    Product product = await db.Products.FindAsync(new object[] { id }, cancellationToken);
    // 直接修改属性,不需要手动调用Update
    product.Name = newName;
    await db.SaveChangesAsync(cancellationToken);
    return product;
}

*注意:位置语法定义的record默认属性是init只读的,如果要支持直接修改属性,可以把record的属性改为可写,调整record的定义方式:

// 调整record定义,开放set访问器,不破坏record的值相等语义
public record Product
{
    public int ID { get; set; }
    public string Name { get; set; }
}

方案2:保持record不可变设计,使用Entry API同步值

如果你要严格保持record的不可变性(属性只留init访问器,不开放直接修改),不需要生成新实例再去走追踪逻辑,可以把新生成的record实例作为值的载体,同步到已经被追踪的旧实例上:

[HttpGet("test/{id}")]
public async Task<Product> ExampleControllerAction(int id, CancellationToken cancellationToken)
{
    string newName = "test new name!!";
    // 拿到被追踪的原始实体
    Product trackedProduct = await db.Products.FindAsync(new object[] { id }, cancellationToken);
    // 生成全新的不可变实例作为更新后的值载体
    Product updatedProduct = trackedProduct with { Name = newName };
    // 把新实例的属性值同步到被追踪的实体上,不触发追踪冲突
    db.Entry(trackedProduct).CurrentValues.SetValues(updatedProduct);
    await db.SaveChangesAsync(cancellationToken);
    // 直接返回新的不可变实例,符合record的使用习惯
    return updatedProduct;
}

这种方式既完全保留了record的不可变语义,也不会因为新实例和旧实例同主键导致追踪冲突,不需要手动调用Update方法。

方案3:查询时关闭追踪,适配手动Update的场景

如果你确实需要拿到无追踪的实体实例,再通过with生成新实例走手动更新逻辑,可以在查询时明确关闭实体追踪,让上下文不会缓存原始的实体实例:

[HttpGet("test/{id}")]
public async Task<Product> ExampleControllerAction(int id, CancellationToken cancellationToken)
{
    string newName = "test new name!!";
    // 查询时关闭追踪,上下文不会缓存返回的实体实例
    Product product = await db.Products.AsNoTracking().FirstAsync(p => p.ID == id, cancellationToken);
    // 生成新实例不会有追踪冲突,因为上下文里没有同主键的追踪项
    product = product with { Name = newName };
    // 这时候手动调用Update是完全安全的
    db.Update(product);
    await db.SaveChangesAsync(cancellationToken);
    return product;
}
常见误区澄清
  • 不存在“record类型不能当EF Core实体”的说法:EF Core 6开始已经原生支持record类型作为实体,EF Core 5虽然没有内置的record构造函数绑定优化,但只要处理好追踪逻辑,完全可以正常使用。
  • 不要在同一个DbContext生命周期内,把两个同主键的实体实例同时塞进追踪缓存,这和实体是普通类还是record没有关系——哪怕你用普通类,只要搞两个同主键的实例一个被追踪、一个手动Update,一样会抛一模一样的异常。

内容的提问来源于stack exchange,提问作者Phil K

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.25 05:36:01