Search code examples
c#entity-frameworknavigation-properties

Preserving navigation properties in Entity Framework when updating without tracking


I am developing a program following the Repository pattern which involves a database access with Entity Framework and I have come across a problem. When I try to manipulate the same variable twice in the same context, I am greeted with this exception:

System.InvalidOperationException: Attaching an entity of type
'Syn.Commons.Data.Tests.EntityFramework.Helper.DbModel.DBBox' failed because 
another entity of the same type already has the same primary key value.
This can happen when using the 'Attach' method or [...]

I know it comes from the fact that the entity is being used more than once in the same context. However, when I try to use the Detach() method, it loses all of its navigation properties (i.e., all of them become null edit: are reset to the last known values), as stated here (section Detaching Objects):

 In an independent association, the relationship information is not maintained for a detached object.

If I were making queries, I would use AsNoTracking(), but it is not possible since I am not using them. Here is the Update() method:

public bool Update(T entity)
{
    // Some code to get dbEnt

    _context.Set<TDbType>().Attach(dbEnt);

    // The following gives an error when manipulating the entity for the second time
    //_context.Entry<TDbType>(dbEnt).State = EntityState.Modified;

    _context.SaveChanges();

    // The following makes the entity "lose" all of its navigation properties
    //DetachEntry(_context.Entry(dbEnt));

    return true;
}

It seems that both solutions conflict with each other. Is there any way not to track an entity (without explicit queries) while conserving its navigation properties?


Edit: people in the comments were right. Although it did no harm, I did not need Attach(). Therefore, the method now looks like this:

public bool Update(T entity)
{
    // Some code to get dbEnt
    _context.Entry<TDbType>(dbEnt).State = EntityState.Modified;
    _context.SaveChanges();
    return true;
}

Solution

  • I found the answer to my question.

    Entity Framework does not manage relationships when updating from detached entities (see the answers from here and this for more details), and so it must be done by hand, which is a little sad since relationships are managed by default when adding.

    I found a library that was supposed to automatically link the updated relationships on detached entities (which was exactly what I wanted), but I could not seem to make it work. It is called GraphDiff, in case you want to check it out (there are more over there, you can search for them as well).

    Thanks to everyone in the comments.


    Edit: after working on it, I have managed to solve my problem by hand. Here is the pseudocode for one-to-many relationships. It is assuming that you have access to your current entity (which I will call entity), and that there is a list in such entity that holds the "many" part:

    var previous = Find_Previous_Entity();
    
    // Find list differences from the database entity (you may need a comparer)
    var added = entity.OneToManyList.Except( previous.OneToManyList );  // ToList() optional but very recommended
    var deleted = previous.OneToManyList.Except( entity.OneToManyList );  // Same as above
    
    // Make the appropriate changes
    foreach( var item in deleted )
    {
        item.TheForeignKey = null;  // Foreign key related with the entity
        myContext.Entry(item).State = EntityState.Modified;
    }
    foreach( var item in added )
    {
        item.TheForeignKey = entity.ThePrimaryKey;
        myContext.Entry(item).State = (item.Id == 0) ? EntityState.Added : EntityState.Modified;
    }
    

    This should be it. Now, regarding many-to-many relationships it gets a little trickier. What I did was restore the list of the new entity to their original value (the one in the database), Attach() the entity to the context so that Entity Framework could track the changes made to the relationships, and then change them:

    var previous = Find_Previous_Entity();
    
    // Find list differences from the database entity (you may need a comparer)
    var added = entity.ManyToManyList.Except( previous.ManyToManyList );  // ToList() optional but very recommended
    var deleted = previous.ManyToManyList.Except( entity.ManyToManyList );  // Same as above
    
    // Restore the current list to the value held in the database
    entity.ManyToManyList.Clear();
    foreach( var item in previous.ManyToManyList )
    {
        // You may want to insert the line below to avoid attaching both the previous entity and the current one to the context, it gets messy then
        item.OtherManyToManyList.Remove(previous);
        entity.ManyToManyList.Add(item);
    }
    // The new entity now has a copy of the previous list
    
    //Attach the entity with the previous list
    myContext.Set<ClassOfEntity>().Attach(entity);
    // Entity Framework will now acknowledge the changes made in the list and automatically create the appropriate relationships
    
    foreach( var item in deleted )
    {
        entity.ManyToManyList.Remove(item);
        myContext.Entry(item).State = EntityState.Modified;
    }
    foreach( var item in added )
    {
        entity.ManyToManyList.Add(item);
        myContext.Entry(item).State = (item.Id == 0) ? EntityState.Added : EntityState.Modified;
    }
    

    The many-to-many part is not beautiful, but it works. As you may have noted, if you have many many-to-many (hehe) relationships, you will need to update the current entity with all of the lists of previous, then call Attach() and finally work with the lists.

    If you have any other way to do it without "attaching the entity to the context with the previous list", please tell me in the comments, it would be very appreciated. Regardless, I hope it is useful for any of you.