Search code examples
c#.netentity-framework-coreef-core-6.0c#-10.0

Another instance with the key value '' is already being tracked in 1:n relationship


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;
}

Solution

  • 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 TenderOffers 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.