EF Core updating related entities in disconnected scenario

This is a pattern to use for updating related entities that may have duplicate children. For example, a library has a collection of books. Each book has an author. Some authors have written more than one book, so there is a chance that some of the books in a collection that need updating have been written by the same author.

For simple updates, you can use the DbContext.Update method, which attaches the entire entity graph (all its children and their children etc) to the context as Modified and begins tracking each entity in the graph. Problem occurs when two books written by the same author are attached - the author is attached for the first book, but when they are attached for the second book, you get an exception because only one instance of an entity with a given key value can be tracked at a time.

The instance of entity type 'Author' cannot be tracked because another instance with the key value '{{Id}}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.

To work round this, you get the existing version of the entity being updated from the database - along with all its children, and then you make changes to that, based on the values in the incoming entity. You don't try attaching the incoming entity. The DbContext only ever tracks the database version of the entity.

The process involves the following steps:

  1. Retrieve the existing entity (which is automatically tracked)
  2. Transfer the values from the incoming entity to the existing version, which results in all the affected properties being marked as modified (CurrentValues.SetValues)
  3. Iterate the incoming children and see if they already exist in the database version. If not, add them to the tracked entity. If they do, use CurrentValues.SetValues to update their properties
  4. Remove any children that do not appear in the disconnected entity
  5. Save changes
public async Task InsertUpdateOrDeleteGraph(Blog disconnectedBlog)
{
    var trackedBlog = context.Blogs
        .Include(b => b.Posts)
        .FirstOrDefault(b => b.BlogId == disconnectedBlog.BlogId); // 1

    if (trackedBlog == null)
    {
        context.Add(blog);
    }
    else
    {
        context.Entry(trackedBlog).CurrentValues.SetValues(disconnectedBlog); // 2
        foreach (var post in disconnectedBlog.Posts)
        {
            var existingPost = trackedBlog.Posts.FirstOrDefault(p => p.PostId == post.PostId);

            if (existingPost == null)
            {
                trackedBlog.Posts.Add(post); // 3
            }
            else
            {
                context.Entry(existingPost).CurrentValues.SetValues(post);
            }
        }

        foreach (var post in trackedBlog.Posts)
        {
            if (!disconnectedBlog.Posts.Any(p => p.PostId == post.PostId)) // 4
            {
                context.Remove(post);
            }
        }
    }

    await context.SaveChangesAsync(); // 5
}

Further reading

Last updated: 10/11/2022 8:30:00 AM

Latest Updates

© 0 - 2025 - Mike Brind.
All rights reserved.
Contact me at Mike dot Brind at Outlook.com