I'm trying to update an entity Tender
, this entity has a List of TenderOffer
and this TenderOffer
has a Company
entity. When I try to update the Tender I'm getting the following error
Message=The instance of entity type 'Company' cannot be tracked because another instance with the key value '{ID: 4}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
Source=Microsoft.EntityFrameworkCore StackTrace: at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.ThrowIdentityConflict(InternalEntityEntryentry) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKeykey, InternalEntityEntry entry, Boolean updateDuplicate) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKeykey, InternalEntityEntry entry) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(InternalEntityEntryentry) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntryentry) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityStateoldState, EntityState newState, Boolean acceptChanges, BooleanmodifyProperties) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityStateentityState, Boolean acceptChanges, Boolean modifyProperties,Nullable`1 forceStateWhenUnknownKey) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.PaintAction(EntityEntryGraphNode`1node) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1node, Func`2 handleNode) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1node, Func`2 handleNode) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1node, Func`2 handleNode) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1node, Func`2 handleNode) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.AttachGraph(InternalEntityEntryrootEntry, EntityState targetState, EntityStatestoreGeneratedWithKeySetTargetState, Boolean forceStateWhenUnknownKey) at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.SetEntityState(InternalEntityEntryentry, EntityState entityState) at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.Update(TEntity entity) at DAL.Repositories.TendersRepository.<UpdateTender>d__10.MoveNext() in C:\Progetti\DAL\Repositories\TendersRepository.cs:line 613 at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw() at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult() at UI.Controllers.TenderController.<SelectTenderWinner>d__13.MoveNext() in C:\Progetti\UI\Controllers\TenderController.cs:line 808 This exception was originally thrown at this call stack: [External Code] DAL.Repositories.TendersRepository.UpdateTender(ML.Models.Tender) in TendersRepository.cs [External Code] UI.Controllers.TenderController.SelectTenderWinner(int,int, bool) in TenderController.cs
I don't understand why I'm getting this tracking error. I have disabled all tracking behavior of the constructor of the DBContext
base.ChangeTracker.AutoDetectChangesEnabled = false;
base.ChangeTracker.LazyLoadingEnabled = false;
base.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
I'm also using AsNoTracking() in all my queries. I can't figure out why EF core is tracking anyway.
The entity_one is "Tender", the entity_two is "TenderOffer"
This is the code in the controller:
try
{
//id and winnerOfferID are parameters of the request
Tender tender = await this._tenderRepository.GetTender(id, false, this.CurrentCompanyID);
TenderOffer offer = tender.TenderOffers.FirstOrDefault(o => o.ID == winnerOfferID)
tender.IsClosed = true;
tender.ClosureDate = DateTime.UtcNow;
tender.ShowWinner = showWinner;
tender.WinnerOfferID = winnerOfferID;
tender.TenderOffers.ForEach(o =>
{
if (o.ID == winnerOfferID)
{
o.Status = TenderOfferStatus.Winner;
}
else
{
o.Status = TenderOfferStatus.Rejected;
o.Company = null;
}
});
await this._tenderRepository.UpdateTender(tender);
return Ok();
}
catch (Exception ex)
{
return StatusCode(500);
}
This is the Update function:
public async Task<Tender> UpdateTender(Tender tender)
{
if (tender == null || tender.ID <= 0)
throw new ArgumentNullException("Cannot update, tender is null");
this._context.Tenders.Update(tender);
await this.SaveChangesAsync();
return tender;
}
This is the Get function:
public async Task<Tender> GetTender(int id, bool skipCompanyCheck = false, int? companyId = null)
{
IQueryable<Tender> query = this._context.Tenders.AsNoTracking()
.Include(t => t.Company).AsNoTracking()
.Include(t => t.Company).ThenInclude(c => c.Logo).AsNoTracking();
//Other include based on parameters, ALSO TENDER OFFERS
tender = await query.Where(tender => tender.ID == id).FirstOrDefaultAsync();
return tender;
}
You retrieve tenders without tracking, but the ChangeTracker
options don't mean that EF is not capable of change tracking anymore. In fact, you count on it being able to track changes, seeing the later modifications and the SaveChanges
call.
The statement this._context.Tenders.Update(tender);
marks all objects in the object graph (i.e. tender
and adhering child objects) as Modified
. The problem is that the graph contains multiple instances of the same Company
, because multiple TenderOffer
s refer to the same company.
This causes the exception "another instance with the key value '{ID: 4}' is already being tracked".
It seems fair to assume that EF should know it's the same object, but it doesn't. That has to do with AsNoTracking
behavior. Since EFC 3.0, AsNoTracking
is without identity resolution, meaning that when an entity occurs multiple times in a query result, multiple instances of it are materialized. To change this behavior you should use AsNoTrackingWithIdentityResolution()
. (Only one call in one LINQ statement is sufficient.)
BTW, if you don't include Company
and add a nullable CompanyId
property to TenderOffer
you could simply set that property to null
. The current Update
statement also marks Company
as Modified
, which isn't necessary. That could be prevented by first attaching Company
instances to the context, but setting a CompanyId
is easier and more light-weight.
Another alternative would be to fetch the data with tracking, modify their properties, and save changes without calling Update
.