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)
@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:
context.ChangeTracker.TrackGraphAPI. Note, however, that it still won’t allow tracking two instances with the same ID.@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
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).