'Does Entity Framework Core DbContext SaveChanges call DetectChanges implicitly when it throws DbUpdateConcurrencyException?

As a scenario, an entity has a xmin concurrency check which is equivalent of RowVersion column in PostgreSQL database. While updating this entity, there is a loop like below to force update operation as a last in wins strategy.

var entities = entityListContainsEntitiesModifiedByUser;
using var context = new ExampleDbContext();

while(true) 
{
    try
    {
        var entitiesWillBeUpdated = context.Set<TEntity>().Where(x => !entities.Contains(x)).ToList();
        context.UpdateRange(entitiesWillBeUpdated);
  
        // Some of entities have deleted since round before.
        // (This line will be mentioned later below)     
        context.ChangeTracker.DetectChanges();

        var revertEntities = context.ChangeTracker.Entries<TEntity>().Where(x => !entitiesWillBeUpdated.Contains(x.Entity)).ToList();
  
        foreach(var revert in reverEntities) 
        {
            revert.State == EntityState.Detach;
        }

        context.SaveChanges();
        break;
    }
    catch(DbUpdateConcurrencyException e) 
    {
        var entry = e.Entries.Single();
        var dbval = await entry.GetDatabaseValues(); 

        if (dbval == null) 
        {
            // Updating entry has been deleted so it cant be modified.  Ignore it.
            entry.State = EntityState.Detached;
            continue;
        }

        // Updating entry has been concurrently updated by someone else so change the old version to the new one
        var pxmin = entry.Property("xmin")!;
        pxmin.CurrentValue =  dbval.GetValue<uint>("xmin");
        pxmin.OriginalValue =  dbval.GetValue<uint>("xmin");
    }
}

If we go step by step, assume that user modified 1000 entities and we want to update these. In try block all 1000 entities are tracked and set as modified. There is 0 revertEntities at this point because this is first round so no tracked entities fetched from database before. Then, SaveChanges is called. While EF Core updating these entities, there is 1 entity modified inside that 1000 entities concurrently by other users.

So a DbUpdateConcurrencyException is caught. In the catch block new xmin version of this 1 entity is set. Before trying again to update all 1000 records, there is some time and assume in this time, 300 records of the 1000 are deleted. In next round, entitiesWillBeUpdated have 700 records and ExampleDbContect tracks 1000 entities which is loaded round before and 300 of them is deleted from database so revertEntities have 300 entities inside.

These 300 entities are set as detached and SaveChanges is called.

At this point 1 of 700 entities is modified again by another user. In next round, this 1 modified entity's xmin version is updated.

The question is:

At this point, will revertEntities have 300 detached entities again or are they cleared from ExampleDbContext and be untracked because of SaveChanges call made a round before so will it have 0 entities inside?

I know SaveChanges implicitly calls context.ChangeTracker.DetectChanges() but what if a DbUpdateConcurrencyException is thrown?

I know that when this exception is thrown tracked entity values in memory whose state are set modified is not changed but if there is no exception, this entity values have new values same with theirs record in database updated. Is same thing is true for detached entities?

Normally if no error is thrown all of them were cleared from ExampleDbContext and not be tracked. So in last situation, are detached entities cleared or they still exist locally? Do I need to call DetectChanges() manually like the comment line in the code?



Solution 1:[1]

I tested the case in another project and I see that, while detaching a tracked entry and AutoDetectChanges is enabled (by default it is enabled), entries are deleted from Entries list of ChangeTracker after Entry.State = EntryState.Detached line immediately. So, no need to wait for SaveChanges or DetectChanges call.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1