My app is using Entity Framework with repository pattern.
There are 3 services. A, B, C. There’s a Repository R for entity_r. All of these are registered as AsTransient
. The code looks somewhat like this (simplified for brevity) –
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();
}
}
I am getting the following error.
The instance of entity type ‘entity_r’ cannot be tracked because
another instance with the same key value for {entity_r_id} is
already being tracked.
I have tried getting entity_r with AsNoTracking()
in B.Invoice(). Tried registering repository R as AsScoped
. But, still getting the same.
If I pass entity_r from A.GenerateInvoice() to B.Invoice() and C.ChangeStatus() without changing anything else, it works.
Is there a way to get around this without passing entity_r or changing the service-repository scopes to AsScoped?
The problem is most likely your implementation of a unit of work. Unit of Work and Transient are not really compatible. The underlying issue is likely that regardless of the Repository and services being Transient, I suspect the Repository is going to the Scope to fetch or access the DbContext
, and while that DbContext
instance may also be Transient
the Unit of Work by definition of its purpose will be relying on a single instance over its lifetime. Without knowing the implementation of the Unit of Work it is likely that untracked entities are being fetched by each service through the repository, but calling “Update” will begin tracking that instance. The only real solution, since changes are not committed to the database with each step is that your repositories cannot blindly fetch untracked entities without considering that a tracked entity might be available.
Assuming your Repository implementation’s GetByIdAsync does something like:
async Task<Invoice> IRepository.GetByIdAsync(int id)
{
return await _scope.DbContext.Invoices
.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == id);
}
This needs to be revised to something more like:
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);
}
The first call checks the tracking cache to locate any possible tracked references. If found, they are returned. If not then we can fetch a detached entity.
However, I don’t really recommend this overall approach. For read-only operations it is fine to default to .AsNoTracking()
queries, but when it comes to write operations you should fetch, and use tracked entity queries to avoid issues like this, and avoid the .Update()
method entirely. The big caveat of updating via untracked references is that even with checking the tracking cache to avoid the exception you are seeing, you will only get state of the tracked entity as it was originally loaded. For instance if service B only needed the invoice, but service C wanted to eager load some additional reference navigation properties, service C using .Include()
in its “if not found in tracking cache” implementation would not receive those navigation property when B was called first and the DbContext is tracking an instance without them. This issue is avoided by letting EF manage the tracking as it is designed to do.
Additionally, in a method chain like that, whatever is responsible for initiating the Unit of Work should be the one responsible for committing, or rolling it back. I.e.
public async Task GenerateInvoice(int id)
{
using var scope = unitOfWork.Begin();
await B.Invoice(id, scope);
await C.ChangeStatus(id, scope);
await scope.SaveChangesAsync();
}
Ideally any commit or rollback of “scope” would not be initiated within into Services B or C. The issue with your implementation is that the execution order is now dependent and if you introduce a step beyond c.ChangeStatus then the SaveChanges
must be moved. If you have rolled your own Unit of Work, then the contract interface for the “scope” that is passed to the consumers of the UoW should not expose “SaveChanges”. That either should remain in the UoW implementation:
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);
}
… or the scope implement two interfaces, the IUnitOfWork and a IUnitOfWorkScope where B & C only receive the “scope” as IUnitOfWorkScope which does not expose 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)
{
// ...
}
}