Attaching an entity with a mix of existing and new entities in its graph (Entity Framework Core 1.1.0)

后端 未结 1 2135
野性不改
野性不改 2021-02-20 06:26

I have encountered an issue when attaching entities holding reference properties to existing entities (I call existing entity an entity that already exists in the database, and

相关标签:
1条回答
  • 2021-02-20 06:41

    After some researches, reading the comments, blog posts, and above all, the answer by an EF team member to an issue I submitted in the GitHub repo, it appears that the behaviour I noticed in my question is not a bug, but a feature of EF Core 1.0.0 and 1.1.0.

    [...] in 1.1 whenever we determine that an entity should be Added because it does not have a key set, then all entities discovered as children of that entity will also be marked as Added.

    (Arthur Vickers -> https://github.com/aspnet/EntityFramework/issues/7334)

    So what I called 'workaround' is actually a recommended practice, as Ivan Stoev stated in his comment.

    Dealing with entity states according to their primary key state

    The DbContect.ChangeTracker.TrackGraph(object rootEntity, Action<EntityEntryGraphNodes> callback) method takes the root entity (the one that is posted, or added, updated, attached, whatever), and then iterates over all the discovered entities in the relationship graph of the root, and executes the callback Action.

    This can be called prior to the _context.Add() or _context.Update() methods.

    _context.ChangeTracker.TrackGraph(rootEntity, node => 
    { 
        node.Entry.State = n.Entry.IsKeySet ? 
            EntityState.Modified : 
            EntityState.Added; 
    });
    

    But (nothing said before 'but' actually matters!) there's something I had been missing for too long and that caused me HeadAcheExceptions:

    If an entity is discovered that is already tracked by the context, that entity is not processed (and it's navigation properties are not traversed).

    (source: intellisense of that method !)

    So maybe it might be safe to ensure the context is free of anything before posting a disconnected entity:

    public virtual void DetachAll()
    {
        foreach (var entityEntry in _context.ChangeTracker.Entries().ToArray())
        {
            if (entityEntry.Entity != null)
            {
                entityEntry.State = EntityState.Detached;
            }
        }
    }
    

    Client-side state mapping

    Another approach is to deal with the state on client side, post entities (therefore disconnected by design), and set their state according to a client-side state.

    First, define an enum that maps client states to entity states (only the detached state is missing, because is doen't make sense):

    public enum ObjectState
    {
        Unchanged = 1,
        Deleted = 2,
        Modified = 3,
        Added = 4
    }
    

    Then, use the DbContect.ChangeTracker.TrackGraph(object rootEntity, Action<EntityEntryGraphNodes> callback) method to set Entity states according to client state:

    _context.ChangeTracker.TrackGraph(entity, node =>
    {
        var entry = node.Entry;
        // I don't like switch case blocks !
        if (childEntity.ClientState == ObjectState.Deleted) entry.State = EntityState.Deleted;
        else if (childEntity.ClientState == ObjectState.Unchanged) entry.State = EntityState.Unchanged;
        else if (childEntity.ClientState == ObjectState.Modified) entry.State = EntityState.Modified;
        else if (childEntity.ClientState == ObjectState.Added) entry.State = EntityState.Added;
    });
    

    With this approach, I use a BaseEntity abstract class, which shares the Id (PK) of my entities, and also the ClientState (of type ObjectState) (and a IsNew accessor, based on PK value)

    public abstract class BaseEntity
    {
        public int Id {get;set;}
        [NotMapped]
        public ObjectState ClientState { get;set; } = ObjectState.Unchanged;
        [NotMapped]
        public bool IsNew => Id <= 0;
    }
    

    Optimistic / heuristic approach

    This is what I actually implemented. I have of mix of the old approach (meaning that if en entity has it's PK undefined, it must be added, and if the root has a PK, it must me updated), and the client state approach:

    _context.ChangeTracker.TrackGraph(entity, node =>
    {
        var entry = node.Entry;
        // cast to my own BaseEntity
        var childEntity = (BaseEntity)node.Entry.Entity;
        // If entity is new, it must be added whatever the client state
        if (childEntity.IsNew) entry.State = EntityState.Added;
        // then client state is mapped
        else if (childEntity.ClientState == ObjectState.Deleted) entry.State = EntityState.Deleted;
        else if (childEntity.ClientState == ObjectState.Unchanged) entry.State = EntityState.Unchanged;
        else if (childEntity.ClientState == ObjectState.Modified) entry.State = EntityState.Modified;
        else if (childEntity.ClientState == ObjectState.Added) entry.State = EntityState.Added;
    });
    
    0 讨论(0)
提交回复
热议问题