3

Suppose I have a simple entity with children:

public class Entity
{
    public Guid ID { get; set; }
    public ICollection<Child> Children { get; set; }
}

public class Child
{
    public Guid ID { get; set; }
    public Guid ParentEntityID { get; set; }
}

The "Children" property is configured as a one to many relationship. There a few scenarios below that I need clarification on what will happen.

Scenario 1: Assume I load the entity but don't "Include" the Children property:

  • If I then initialize the collection to an empty list and save it... will that wipe out the children or do nothing? I've read that initializing collections is optional, which suggests that initializing it to an empty set is beneign. However, that contradicts the notion that the assigned set represents the related objects, in which case assigning an empty set would suggest you want to clear the related objects.

Scenario 2: Assume I load the entity and also "Include" the Children property:

  • If I clear the collection and save the entity, would that wipe out the children? Likewise, since it was included, if I assign a new empty set, will it remove the related objects? In other words, does assigning a new empty set and saving the entity function differently when I include the navigation property vs not include it?

Scenario 3: Assume I have thousands of Children and want to add or remove just a few of them:

  • Do I have to "Include" (i.e. preload) the entire collection in order to add or remove items? If so, how can I perform a partial update of the set without preloading all the children?
Triynko
  • 18,766
  • 21
  • 107
  • 173
  • 2
    Wow, ok. Just found out that explicitly setting a list of values for a navigation property collection does NOT replace the existing collection. That's insane. For example, if there are two children in the database and I assign a brand new collection with only one element to the navigation property and save it... it just *adds* the new entity to the existing set instead of replacing the set, even though I've replaced the set. So collection navigation properties are NOT an accurate representation of the associated set of entities at all. – Triynko Aug 08 '17 at 19:12
  • 2
    It gets stranger. I did experiments with calling "Include" before doing work. When you call "Include" it loads everything (no way to filter the set, which is misleading because the last experiment proved it's not always a complete representation of the set). By virtual of calling include, it's now tracking all entities in the set. At this point, you can replace the collection instance or reuse the existing one, and as long as you don't remove anything, you can add to the set without affecting existing entities. – Triynko Aug 08 '17 at 19:15
  • I was curious if I could 'hide' the loaded entities from the change tracker after including them, so I included them, added two new ones, remove the existing two, and then tried to set their change tracker state to "Unmodified". It immediately threw an error about the foreign key being null when "State" was set (the same error I'd normally get when saving changes after removing the entity from the collection). To get around that, I set the FK to a dummy value of Guid.Empty before setting its state to Unmodified and that worked. Two new entities were added and the old ones were left alone. – Triynko Aug 08 '17 at 19:18
  • That's really strange, like... if I'm telling EF the entity is unmodifed, why is it throwing an error about a field being null? If it were to take any action *at all*, wouldn't it be to reset the value back to it's original non-null state? Very strange, anyway... Another alternative is to just set the state of the two included/existing children to Detached before clearing the collection and adding two new ones to the collection.That also achieves the result of preserving the two existing children (in spite of clearing them from the collection) while simultaneously adding two new children. – Triynko Aug 08 '17 at 19:22
  • Further info: "By default, the Entity Framework performs Detect Changes automatically when the following methods are called: DbSet: (Find,Local,Remove,Add,Attach); DbContext: (SaveChanges, GetValidationErrors, Entry); DbChangeTracker: (Entries). – Triynko Aug 08 '17 at 19:42

1 Answers1

1

Scenario 1: Assume I load the entity but don't "Include" the Children property: If I then initialize the collection to an empty list and save it... will that wipe out the children or do nothing?

It will do nothing. No children are loaded, so none of their states are tracked. EF doesn't actually care about the collection itself, only the things inside it. You can even reassign a new collection instance, and as long as all the entities are added back into it, you're fine. You can even remove entities from the collection if you Detach them, and they won't be affected in the database. If you detach them all, you can set the collection to null or an empty collection, and it will still save changes without any error. It must perform some kind of closed-loop check on the children... seeing that their ParentID is X, it must then check Parent X to see if that child is still in it's Children collection and throw an error if it's not. It seem to analyze it all as a complete graph of the tracked entities and updates collections automatically when things are loaded. This allowed me to make a prediction that if I loaded a Parent and one of it's children separately and the Parent's Children collection is null, that should cause an error. Indeed, when I load parent by itself, the collection is initially null, and then when I load one or two children separtely, the parent's Children collection is automatically initialized/updated by EF to include the loaded children, such that clearing it would cause the foreign key error.

Scenario 2: Assume I load the entity and also "Include" the Children property: If I clear the collection and save the entity, would that wipe out the children?

Yes. The Include call would load all children into the tracker. It will ensure they're all in the Parent's Children collection, whether you initialized it earlier or not. The "Include" call doesn't really make it function any differnetly, it just causes all the children to enter the change tracker. Loading all the children separately after loading the parent ends up putting the entire system in the same state as if you had just called include up front.

Scenario 3: Assume I have thousands of Children and want to add or remove just a few of them: Do I have to "Include" (i.e. preload) the entire collection in order to add or remove items? If so, how can I perform a partial update of the set without preloading all the children?

Absolutely not. You can load just the ones you want to remove, then remove them from the DbSet. If you don't want to load them up front, you could issue a direct SQL delete statement on the IDs. If you don't call include and don't load any children, then you can just add a few new ones to the collection navigation property and only those few will be added; everything else that's not in the change tracker is left alone.

Triynko
  • 18,766
  • 21
  • 107
  • 173
  • What we often do in practice when removing specific children, is simply issue deletes for each child via their IDs, rather than using navigation lists. I generally only use navigation properties/list when retrieving data for reading. For example, if we need to change relationships we set FK's to update the child entity to associate it to a new parent. It seems to avoid bugs where devs make invalid assumptions about the behavior of these lists when updating data. – AaronLS Aug 08 '17 at 20:23
  • I don't have a lot of time right now, but I'm planning to update/clarify this answer soon. One thing I didn't mention is that this is all based on a one-to-many relationships where removing a child from a navigation property without removing it from the DbSet just sets the foreign key to null and throws an error (because it's non-nullable). To remove the child safely, you have to remove it from the DbSet (i.e. flag it for deletion). Having said this, this explains what's safe to remove or not remove from the navigation collection and explains how and when related entities are tracked). – Triynko Aug 08 '17 at 20:23
  • @AaronLS. Indeed, you can issue delete statements based on the IDs. The only danger there is when you issue direct SQL delete statements outside of the context. So if you have already loaded the children into the change tracker, then delete them out from under it, you could get errors when save changes is called if those entities were modified at all (because it will try to update them and discover they're no longer in the database). Deleting them outside of the context by id is a more efficient approach. – Triynko Aug 08 '17 at 20:25
  • "issue direct SQL delete statements" Note, there's several ways to delete entities via EF without issueing direct SQL queries: https://stackoverflow.com/a/17726414/84206 – AaronLS Aug 08 '17 at 20:37