private void DetectNavigationChange(InternalEntityEntry entry, INavigation navigation, Sidecar snapshot) { var snapshotValue = snapshot[navigation]; var currentValue = entry[navigation]; var stateManager = entry.StateManager; var added = new HashSet <object>(ReferenceEqualityComparer.Instance); if (navigation.IsCollection()) { var snapshotCollection = (IEnumerable)snapshotValue; var currentCollection = (IEnumerable)currentValue; var removed = new HashSet <object>(ReferenceEqualityComparer.Instance); if (snapshotCollection != null) { foreach (var entity in snapshotCollection) { removed.Add(entity); } } if (currentCollection != null) { foreach (var entity in currentCollection) { if (!removed.Remove(entity)) { added.Add(entity); } } } if (added.Any() || removed.Any()) { stateManager.Notify.NavigationCollectionChanged(entry, navigation, added, removed); snapshot.TakeSnapshot(navigation); } } else if (!ReferenceEquals(currentValue, snapshotValue)) { stateManager.Notify.NavigationReferenceChanged(entry, navigation, snapshotValue, currentValue); if (currentValue != null) { added.Add(currentValue); } snapshot.TakeSnapshot(navigation); } foreach (var addedEntity in added) { var addedEntry = stateManager.GetOrCreateEntry(addedEntity); if (addedEntry.EntityState == EntityState.Detached) { _attacher.AttachGraph(addedEntry, EntityState.Added); } } }
/// <summary> /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> public virtual void NavigationReferenceChanged(InternalEntityEntry entry, INavigation navigation, object oldValue, object newValue) { if (_inFixup) { return; } var foreignKey = navigation.ForeignKey; var stateManager = entry.StateManager; var inverse = navigation.FindInverse(); var oldTargetEntry = oldValue == null ? null : stateManager.TryGetEntry(oldValue); if (oldTargetEntry?.EntityState == EntityState.Detached) { oldTargetEntry = null; } var newTargetEntry = newValue == null ? null : stateManager.TryGetEntry(newValue); if (newTargetEntry?.EntityState == EntityState.Detached) { newTargetEntry = null; } try { _inFixup = true; if (navigation.IsDependentToPrincipal()) { if (newValue != null) { if (newTargetEntry != null) { if (foreignKey.IsUnique) { // Navigation points to principal. Find the dependent that previously pointed to that principal and // null out its FKs and navigation property. A.k.a. reference stealing. // However, if the FK has already been changed or the reference is already set to point // to something else, then don't change it. var victimDependentEntry = stateManager.GetDependents(newTargetEntry, foreignKey).FirstOrDefault(); if (victimDependentEntry != null && victimDependentEntry != entry) { ConditionallyNullForeignKeyProperties(victimDependentEntry, newTargetEntry, foreignKey); if (ReferenceEquals(victimDependentEntry[navigation], newTargetEntry.Entity)) { SetNavigation(victimDependentEntry, navigation, null); } } } // Set the FK properties to reflect the change to the navigation. SetForeignKeyProperties(entry, newTargetEntry, foreignKey, setModified: true); } } else { // Null the FK properties to reflect that the navigation has been nulled out. ConditionallyNullForeignKeyProperties(entry, oldTargetEntry, foreignKey); } if (inverse != null) { var collectionAccessor = inverse.IsCollection() ? inverse.GetCollectionAccessor() : null; // Set the inverse reference or add the entity to the inverse collection if (newTargetEntry != null) { SetReferenceOrAddToCollection(newTargetEntry, inverse, collectionAccessor, entry.Entity); } // Remove the entity from the old collection, or null the old inverse unless it was already // changed to point to something else if (oldTargetEntry != null) { if (collectionAccessor != null) { RemoveFromCollection(oldTargetEntry, inverse, collectionAccessor, entry.Entity); } else if (ReferenceEquals(oldTargetEntry[inverse], entry.Entity)) { SetNavigation(oldTargetEntry, inverse, null); } } } } else { Debug.Assert(foreignKey.IsUnique); if (newTargetEntry != null) { // Navigation points to dependent and is 1:1. Find the principal that previously pointed to that // dependent and null out its navigation property. A.k.a. reference stealing. // However, if the reference is already set to point to something else, then don't change it. var victimPrincipalEntry = stateManager.GetPrincipal(newTargetEntry, foreignKey); if (victimPrincipalEntry != null && victimPrincipalEntry != entry && ReferenceEquals(victimPrincipalEntry[navigation], newTargetEntry.Entity)) { SetNavigation(victimPrincipalEntry, navigation, null); } SetForeignKeyProperties(newTargetEntry, entry, foreignKey, setModified: true); SetNavigation(newTargetEntry, inverse, entry.Entity); } if (oldTargetEntry != null) { // Null the FK properties on the old dependent, unless they have already been changed ConditionallyNullForeignKeyProperties(oldTargetEntry, entry, foreignKey); // Clear the inverse reference, unless it has already been changed if (inverse != null && ReferenceEquals(oldTargetEntry[inverse], entry.Entity)) { SetNavigation(oldTargetEntry, inverse, null); } } } } finally { _inFixup = false; } if (newValue != null && newTargetEntry == null) { stateManager.RecordReferencedUntrackedEntity(newValue, navigation, entry); entry.SetRelationshipSnapshotValue(navigation, newValue); _attacher.AttachGraph(stateManager.GetOrCreateEntry(newValue), EntityState.Added); } }
/// <summary> /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// </summary> public virtual void NavigationReferenceChanged(InternalEntityEntry entry, INavigation navigation, object oldValue, object newValue) { if (_inFixup) { return; } var foreignKey = navigation.ForeignKey; var stateManager = entry.StateManager; var inverse = navigation.Inverse; var targetEntityType = navigation.TargetEntityType; var oldTargetEntry = oldValue == null ? null : stateManager.TryGetEntry(oldValue, targetEntityType); if (oldTargetEntry?.EntityState == EntityState.Detached) { oldTargetEntry = null; } var newTargetEntry = newValue == null ? null : stateManager.TryGetEntry(newValue, targetEntityType); if (newTargetEntry?.EntityState == EntityState.Detached) { newTargetEntry = null; } try { _inFixup = true; if (navigation.IsOnDependent) { if (newValue != null) { if (newTargetEntry != null) { if (foreignKey.IsUnique) { // Navigation points to principal. Find the dependent that previously pointed to that principal and // null out its FKs and navigation property. A.k.a. reference stealing. // However, if the FK has already been changed or the reference is already set to point // to something else, then don't change it. var victimDependentEntry = stateManager.GetDependents(newTargetEntry, foreignKey).FirstOrDefault(); if (victimDependentEntry != null && victimDependentEntry != entry) { ConditionallyNullForeignKeyProperties(victimDependentEntry, newTargetEntry, foreignKey); if (ReferenceEquals(victimDependentEntry[navigation], newTargetEntry.Entity) && victimDependentEntry.StateManager .TryGetEntry(victimDependentEntry.Entity, navigation.DeclaringEntityType) != null) { SetNavigation(victimDependentEntry, navigation, null, fromQuery: false); } } } // Set the FK properties to reflect the change to the navigation. SetForeignKeyProperties(entry, newTargetEntry, foreignKey, setModified: true, fromQuery: false); } } else { // Null the FK properties to reflect that the navigation has been nulled out. ConditionallyNullForeignKeyProperties(entry, oldTargetEntry, foreignKey); entry.SetRelationshipSnapshotValue(navigation, null); } if (inverse != null) { // Set the inverse reference or add the entity to the inverse collection if (newTargetEntry != null) { SetReferenceOrAddToCollection(newTargetEntry, inverse, entry, fromQuery: false); } // Remove the entity from the old collection, or null the old inverse unless it was already // changed to point to something else if (oldTargetEntry != null && oldTargetEntry.EntityState != EntityState.Deleted) { ResetReferenceOrRemoveCollection(oldTargetEntry, inverse, entry, fromQuery: false); } } } else { Check.DebugAssert(foreignKey.IsUnique, $"foreignKey {foreignKey} is not unique"); if (oldTargetEntry != null) { // Null the FK properties on the old dependent, unless they have already been changed ConditionallyNullForeignKeyProperties(oldTargetEntry, entry, foreignKey); // Clear the inverse reference, unless it has already been changed if (inverse != null && ReferenceEquals(oldTargetEntry[inverse], entry.Entity) && (!oldTargetEntry.EntityType.HasDefiningNavigation() || entry.EntityType.GetNavigations().All( n => n == navigation || !ReferenceEquals(oldTargetEntry.Entity, entry[n])))) { SetNavigation(oldTargetEntry, inverse, null, fromQuery: false); } } if (newTargetEntry != null) { // Navigation points to dependent and is 1:1. Find the principal that previously pointed to that // dependent and null out its navigation property. A.k.a. reference stealing. // However, if the reference is already set to point to something else, then don't change it. var victimPrincipalEntry = stateManager.FindPrincipal(newTargetEntry, foreignKey); if (victimPrincipalEntry != null && victimPrincipalEntry != entry && ReferenceEquals(victimPrincipalEntry[navigation], newTargetEntry.Entity)) { SetNavigation(victimPrincipalEntry, navigation, null, fromQuery: false); } SetForeignKeyProperties(newTargetEntry, entry, foreignKey, setModified: true, fromQuery: false); SetNavigation(newTargetEntry, inverse, entry, fromQuery: false); } } if (newValue == null) { entry.SetIsLoaded(navigation, loaded: false); } } finally { _inFixup = false; } if (newValue != null && newTargetEntry == null) { stateManager.RecordReferencedUntrackedEntity(newValue, navigation, entry); entry.SetRelationshipSnapshotValue(navigation, newValue); newTargetEntry = stateManager.GetOrCreateEntry(newValue, targetEntityType); _attacher.AttachGraph( newTargetEntry, EntityState.Added, EntityState.Modified, forceStateWhenUnknownKey: false); } }
/// <summary> /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> public virtual void NavigationCollectionChanged( InternalEntityEntry entry, INavigation navigation, IEnumerable<object> added, IEnumerable<object> removed) { if (_inFixup) { return; } var foreignKey = navigation.ForeignKey; var stateManager = entry.StateManager; var inverse = navigation.FindInverse(); foreach (var oldValue in removed) { var oldTargetEntry = stateManager.TryGetEntry(oldValue); if (oldTargetEntry != null && oldTargetEntry.EntityState != EntityState.Detached) { try { _inFixup = true; // Null FKs and navigations of dependents that have been removed, unless they // have already been changed. ConditionallyNullForeignKeyProperties(oldTargetEntry, entry, foreignKey); if (inverse != null && ReferenceEquals(oldTargetEntry[inverse], entry.Entity) && oldTargetEntry.StateManager.TryGetEntry(oldTargetEntry.Entity, throwOnNonUniqueness: false) != null) { SetNavigation(oldTargetEntry, inverse, null); } entry.RemoveFromCollectionSnapshot(navigation, oldValue); } finally { _inFixup = false; } } } foreach (var newValue in added) { var newTargetEntry = stateManager.GetOrCreateEntry(newValue); if (newTargetEntry.EntityState != EntityState.Detached) { try { _inFixup = true; // For a dependent added to the collection, remove it from the collection of // the principal entity that it was previously part of var oldPrincipalEntry = stateManager.GetPrincipalUsingRelationshipSnapshot(newTargetEntry, foreignKey); if (oldPrincipalEntry != null && oldPrincipalEntry != entry) { RemoveFromCollection(oldPrincipalEntry, navigation, newTargetEntry); } // Set the FK properties on added dependents to match this principal SetForeignKeyProperties(newTargetEntry, entry, foreignKey, setModified: true); // Set the inverse navigation to point to this principal SetNavigation(newTargetEntry, inverse, entry); } finally { _inFixup = false; } } else { stateManager.RecordReferencedUntrackedEntity(newValue, navigation, entry); _attacher.AttachGraph(newTargetEntry, EntityState.Added, forceStateWhenUnknownKey: false); } entry.AddToCollectionSnapshot(navigation, newValue); } }