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
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;
});