efcore: Adding an Entity Graph with sub-n-level entities with the same Id results in error - Breaking Change

EFCore 3.1.1 - breaking change Adding a disconnected entity graph to EF Context, if there is an entity of the same type with the same Id this results in an error.

In the previous version 2.2 and in the EF6 this was working fine.

Steps to reproduce

    public class SampleContext : DbContext
    {
        public DbSet<Book> Books { get; set; }
        public DbSet<Author> Authors { get; set; }
        public DbSet<Shelf> Shelfs { get; set; }
        public DbSet<ShelfBooks> ShelfBooks { get; set; }

        public SampleContext()
            :base()
        {
            //  Detaching an entity results in related entities being deleted
            //  https://github.com/dotnet/efcore/issues/18982
            base.ChangeTracker.CascadeDeleteTiming = CascadeTiming.OnSaveChanges;
            base.ChangeTracker.DeleteOrphansTiming = CascadeTiming.OnSaveChanges;
        }
    }
    public class Author
    {
        public int AuthorId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public ICollection<Book> Books { get; set; }
    }
    public class Book
    {
        public int BookId { get; set; }
        public string Title { get; set; }
        public Author Author { get; set; }
        public int AuthorId { get; set; }
    }
    public class Shelf
    {
        public int ShelfId { get; set; }
        public string Description { get; set; }
        public ICollection<ShelfBooks> ShelfBooks { get; set; }

    }
    public class ShelfBooks
    {
        public int ShelfBookId { get; set; }
        public int ShelfId { get; set; }
        public Book Shelf { get; set; }
        public int BookId { get; set; }
        public Book Book { get; set; }
    }
    
    public class Test
    {
        public void AddToEFC()
        {
            var shelf = new Shelf();
            shelf.ShelfId = 0;
            shelf.Description = "New Shelf";

            shelf.ShelfBooks = new List<ShelfBooks>();
            var sb1 = new ShelfBooks
            {
                ShelfId = 0,
                Book = new Book
                {
                    BookId = 100,
                    Author = new Author
                    {
                        AuthorId = 2,
                        FirstName = "Existing Author",
                        LastName = "2"
                    },
                    Title = "Existing Book1"
                }
            };

            shelf.ShelfBooks.Add(sb1);

            var sb2 = new ShelfBooks
            {
                ShelfId = 0,
                Book = new Book
                {
                    BookId = 200,
                    Author = new Author
                    {
                        AuthorId = 2,
                        FirstName = "Existing Author",
                        LastName = "2"
                    },
                    Title = "Existing Book2"
                }
            };

            shelf.ShelfBooks.Add(sb2);

            var ctx = new SampleContext();
            ctx.Shelfs.Add(shelf);      // throws error
        }
    }

Adding the shelf entity to the EFCore context results in the error: Message: The instance of entity type ‘Author’ cannot be tracked because another instance with the key value ‘{AuthorId: 2}’ is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.

StackTrace:

at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.ThrowIdentityConflict(InternalEntityEntry entry)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry, Boolean updateDuplicate)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, Boolean acceptChanges, Boolean modifyProperties)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.PaintAction(EntityEntryGraphNode`1 node)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1 node, Func`2 handleNode)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1 node, Func`2 handleNode)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1 node, Func`2 handleNode)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1 node, Func`2 handleNode)
   at Microsoft.EntityFrameworkCore.DbContext.SetEntityState(InternalEntityEntry entry, EntityState entityState)
   at Microsoft.EntityFrameworkCore.DbContext.SetEntityState[TEntity](TEntity entity, EntityState entityState)

Further technical details

EF Core version: 3.1.1 Database provider: Microsoft.EntityFrameworkCore.SqlServer Target framework: .NET 4.6.1 Operating system: Win 10 Pro 1909 IDE: Visual Studio 2019 16.4.5

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Comments: 16 (6 by maintainers)

Most upvoted comments

@jprsilva I went back and re-investigated from the beginning. It turns out that in 2.2 your code was making use of this bug: #18007. So EF was, incorrectly, silently discarding the duplicate instances of the Author entity. Unfortunately, this behavior, though a bug, is what you were using to get the behavior you wanted.

EF Core does not, in general, allow multiple instances with the same key to be tracked, regardless of the state of the entity. (Deleted entities can have special behavior, but that’s not relevant here.) The approach of going through an invalid graph state in order to then fix it up later is not recommended, even though the limitations of EF6 made it a common approach there. (Note that EF6 still required identity resolution to a single instance.)

To fix this:

  • Do entity resolution before attaching the graph–you need to decide which of the multiple instances EF should use. See #20124 and https://github.com/dotnet/efcore/issues/20116#issuecomment-593499871
  • If the mechanism of using the key value to determine new/existing changes is not sufficient, then consider using the context.ChangeTracker.TrackGraph API. Note, however, that it still won’t allow tracking two instances with the same ID.

@jprsilva Then don’t set BookId. Leave it set to the CLR default–that is, zero.

@ajcvickers if the book is an existing one I need to set it’s Id (did you saw the title “Existing Book”?). But do you understand that the BookId is not the problem? Working with a disconnected graph, if the book is an existing one, I need to set is Id - that’s the case in the example. But the entity root (Shelf) is new - so I am not setting is Id, and calling the Add to set the full graph as added state. After that I have a helper class (my own rules) that will correct the states of the entities of the graph.

@ajcvickers Thanks for your feedback. I made another test project with EFCore 2.2.6 and as you can see it works fine without any error with the method Add. https://github.com/jprsilva/EFCore311_Issue19984/tree/master/EFCore226

Code with EFCore 2.2.6

using (var ctx = new EFC.BooksDbContext())
{
	ctx.Shelfs.Add(shelf);      // Does not throws any error - EFCore 3.1.1. throws error

	var addedEntities = ctx.ChangeTracker.Entries()
		.Where(x => x.State == Microsoft.EntityFrameworkCore.EntityState.Added)
		.Count();
	var updatedEntities = ctx.ChangeTracker.Entries()
		.Where(x => x.State == Microsoft.EntityFrameworkCore.EntityState.Modified)
		.Count();
	var unchagedEntities = ctx.ChangeTracker.Entries()
		.Where(x => x.State == Microsoft.EntityFrameworkCore.EntityState.Unchanged)
		.Count();
	var detachedEntities = ctx.ChangeTracker.Entries()
		.Where(x => x.State == Microsoft.EntityFrameworkCore.EntityState.Detached)
		.Count();

	Console.WriteLine($"Added entities: {addedEntities}");
	Console.WriteLine($"Updated entities: {updatedEntities}");
	Console.WriteLine($"Unchaged entities: {unchagedEntities}");
	Console.WriteLine($"Detached entities: {detachedEntities}");
}

Result

Yes, I believe this is related to that breaking change. So my question is, how can I disable that? (And return to the old behavior - so I can track this kind of things myself).