实体类型实例无法追踪问题

问题描述 投票:0回答:1

我的应用程序正在使用具有存储库模式的实体框架。 有3个服务。 A、B、C。entity_r 有一个存储库 R。所有这些都被注册为

AsTransient
。代码看起来有点像这样(为了简洁而简化)-

public class A
{
  public async Task GenerateInvoice(int id)
  {
    using var scope = unitOfWork.Begin();
    await B.Invoice(id, scope);
    await C.ChangeStatus(id, scope);
  }
}

public class B
{
  public async Task Invoice(int id, IUnitOfWork scope)
  {
    var r = await R.GetByIdAsync(id);
    //do stuff
  }
}

public class C
{
  public async Task ChangeStatus(int id, IUnitOfWork scope)
  {
    var r = await R.GetByIdAsync(id);
    r.Status = 4; // new status
    await R.UpdatAsync(r);
    await scope.SaveChangesAsync();
  }
}

我收到以下错误。

无法跟踪实体类型“entity_r”的实例,因为 {entity_r_id} 具有相同键值的另一个实例是 已经被追踪了。

我尝试在 B.Invoice() 中使用

AsNoTracking()
获取实体_r。尝试将存储库 R 注册为
AsScoped
。但是,还是一样。 如果我将entity_r从A.GenerateInvoice()传递到B.Invoice()和C.ChangeStatus()而不更改其他任何内容,它就会起作用。 有没有办法解决这个问题,而不传递实体_r或将服务存储库范围更改为AsScoped?

entity-framework-core .net-8.0
1个回答
0
投票

问题很可能是您对工作单元的实现。工作单元和瞬态并不真正兼容。根本问题可能是,无论存储库和服务是瞬态的,我怀疑存储库都会进入范围来获取或访问

DbContext
,而
DbContext
实例也可能是
Transient
工作单元根据其目的的定义,它将在其生命周期内依赖于单个实例。如果不知道工作单元的实现,每个服务可能会通过存储库获取未跟踪的实体,但调用“更新”将开始跟踪该实例。唯一真正的解决方案是,由于每个步骤都不会将更改提交到数据库,因此您的存储库无法盲目地获取未跟踪的实体而不考虑跟踪的实体可能可用。

假设您的存储库实现的 GetByIdAsync 执行以下操作:

async Task<Invoice> IRepository.GetByIdAsync(int id)
{
    return await _scope.DbContext.Invoices
        .AsNoTracking()
        .FirstOrDefaultAsync(x => x.Id == id);
}

这需要修改为更像:

async Task<Invoice> IRepository.GetByIdAsync(int id)
{
    var invoice = _scope.DbContext.Invoices
        .Local()
        .FirstOrDefaultAsync(x => x.Id == id);
    if (invoice != null)
       return invoice;

    return await _scope.DbContext.Invoices
        .AsNoTracking()
        .FirstOrDefaultAsync(x => x.Id == id);
}

第一个调用检查跟踪缓存以找到任何可能的跟踪引用。如果找到,它们将被退回。如果没有,那么我们可以获取一个分离的实体。

但是,我并不真正推荐这种整体方法。对于只读操作,默认使用

.AsNoTracking()
查询是可以的,但是当涉及到写入操作时,您应该获取并使用跟踪实体查询来避免此类问题,并完全避免使用
.Update()
方法。 通过未跟踪引用进行更新的一个重要警告是,即使检查跟踪缓存以避免您看到的异常,您也只能获得被跟踪实体最初加载时的状态。例如,如果服务 B 仅需要发票,但服务 C 希望加载一些额外的参考导航属性,则在调用 B 时,在其“如果在跟踪缓存中未找到”实现中使用
.Include()
的服务 C 将不会收到这些导航属性首先,DbContext 正在跟踪没有它们的实例。通过让 EF 按其设计目的管理跟踪可以避免此问题。

此外,在这样的方法链中,负责启动工作单元的人应该负责提交或回滚它。即

public async Task GenerateInvoice(int id)
{
    using var scope = unitOfWork.Begin();
    await B.Invoice(id, scope);
    await C.ChangeStatus(id, scope);
    await scope.SaveChangesAsync();
}

理想情况下,“范围”的任何提交或回滚都不会在服务 B 或 C 中启动。您的实现的问题是执行顺序现在是相关的,如果您引入超出 c.ChangeStatus 的步骤,则

SaveChanges
必须被感动。如果您已经推出了自己的工作单元,那么传递给 UoW 使用者的“范围”的合约接口不应公开“SaveChanges”。这两者都应该保留在 UoW 的实施中:

public async Task GenerateInvoice(int id)
{
    using var scope = unitOfWork.Begin();
    await B.Invoice(id, scope);
    await C.ChangeStatus(id, scope);
    await unitOfWork.SaveChangesAsync(scope);
}

...或者范围实现两个接口,IUnitOfWork 和 IUnitOfWorkScope,其中 B 和 C 只接收“范围”作为 IUnitOfWorkScope,它不会公开

SaveChanges

public async Task GenerateInvoice(int id)
{
    using IUnitOfWork scope = unitOfWork.Begin();
    await B.Invoice(id, scope); // accepted as IUnitOfWorkScope so neither can "commit" it, only here after done by who initiated it.
    await C.ChangeStatus(id, scope);
    await scope.SaveChangesAsync();
}

public class B
{
  public async Task Invoice(int id, IUnitOfWorkScope scope)
  {
    //...
  }
}

public class C
{
  public async Task ChangeStatus(int id, IUnitOfWorkScope scope)
  {
      // ...
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.