diff --git a/src/EFCore/ChangeTracking/ChangeTracker.cs b/src/EFCore/ChangeTracking/ChangeTracker.cs index 2291ec5236b..cce589d5098 100644 --- a/src/EFCore/ChangeTracking/ChangeTracker.cs +++ b/src/EFCore/ChangeTracking/ChangeTracker.cs @@ -332,6 +332,16 @@ public virtual void TrackGraph( private IEntityEntryGraphIterator GraphIterator { get; } + /// + /// An event fired when an entity is about to be tracked by the context, either because it is returned + /// from a tracking query, or because it is being attached or added to the context. + /// + public event EventHandler Tracking + { + add => StateManager.Tracking += value; + remove => StateManager.Tracking -= value; + } + /// /// An event fired when an entity is tracked by the context, either because it was returned /// from a tracking query, or because it was attached or added to the context. @@ -342,6 +352,20 @@ public event EventHandler Tracked remove => StateManager.Tracked -= value; } + /// + /// An event fired when an entity that is tracked by the associated is moving + /// from one to another. + /// + /// + /// Note that this event does not fire for entities when they are first tracked by the context. + /// Use the event to get notified when the context begins tracking an entity. + /// + public event EventHandler StateChanging + { + add => StateManager.StateChanging += value; + remove => StateManager.StateChanging -= value; + } + /// /// An event fired when an entity that is tracked by the associated has moved /// from one to another. @@ -356,6 +380,28 @@ public event EventHandler StateChanged remove => StateManager.StateChanged -= value; } + /// + /// An event fired when detecting changes to the entity graph or a single entity is about to happen, either through an + /// explicit call to or , or automatically, such as part of + /// executing or . + /// + public event EventHandler DetectingChanges + { + add => ChangeDetector.DetectingChanges += value; + remove => ChangeDetector.DetectingChanges -= value; + } + + /// + /// An event fired when any changes have been detected to the entity graph or a single entity, either through an + /// explicit call to or , or automatically, such as part of + /// executing or . + /// + public event EventHandler DetectedChanges + { + add => ChangeDetector.DetectedChanges += value; + remove => ChangeDetector.DetectedChanges -= value; + } + /// /// Forces immediate cascading deletion of child/dependent entities when they are either /// severed from a required parent/principal entity, or the required parent/principal entity diff --git a/src/EFCore/ChangeTracking/DetectChangesEventArgs.cs b/src/EFCore/ChangeTracking/DetectChangesEventArgs.cs new file mode 100644 index 00000000000..3abc912b858 --- /dev/null +++ b/src/EFCore/ChangeTracking/DetectChangesEventArgs.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; + +namespace Microsoft.EntityFrameworkCore.ChangeTracking; + +/// +/// Event arguments for the event. +/// +/// +/// See State changes of entities in EF Core for more information and examples. +/// +public class DetectChangesEventArgs : EventArgs +{ + private readonly InternalEntityEntry? _internalEntityEntry; + private EntityEntry? _entry; + + /// + /// 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. + /// + [EntityFrameworkInternal] + public DetectChangesEventArgs(InternalEntityEntry? internalEntityEntry) + { + _internalEntityEntry = internalEntityEntry; + } + + /// + /// If detecting changes for a single entity, then this is the for that entity. + /// If detecting changes for an entire graph, then . + /// + public virtual EntityEntry? Entry + => _internalEntityEntry == null + ? null + : (_entry ??= new EntityEntry(_internalEntityEntry)); +} diff --git a/src/EFCore/ChangeTracking/DetectedChangesEventArgs.cs b/src/EFCore/ChangeTracking/DetectedChangesEventArgs.cs new file mode 100644 index 00000000000..c6576ec4efc --- /dev/null +++ b/src/EFCore/ChangeTracking/DetectedChangesEventArgs.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; + +namespace Microsoft.EntityFrameworkCore.ChangeTracking; + +/// +/// Event arguments for the event. +/// +/// +/// See State changes of entities in EF Core for more information and examples. +/// +public class DetectedChangesEventArgs : DetectChangesEventArgs +{ + /// + /// 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. + /// + [EntityFrameworkInternal] + public DetectedChangesEventArgs( + InternalEntityEntry? internalEntityEntry, + bool changesFound) + : base(internalEntityEntry) + { + ChangesFound = changesFound; + } + + /// + /// Returns if changes were found, otherwise. + /// + public virtual bool ChangesFound { get; } +} diff --git a/src/EFCore/ChangeTracking/EntityStateChangingEventArgs.cs b/src/EFCore/ChangeTracking/EntityStateChangingEventArgs.cs new file mode 100644 index 00000000000..9379e8c4a4e --- /dev/null +++ b/src/EFCore/ChangeTracking/EntityStateChangingEventArgs.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; + +namespace Microsoft.EntityFrameworkCore.ChangeTracking; + +/// +/// Event arguments for the event. +/// +/// +/// See State changes of entities in EF Core for more information and examples. +/// +public class EntityStateChangingEventArgs : EntityEntryEventArgs +{ + /// + /// 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. + /// + [EntityFrameworkInternal] + public EntityStateChangingEventArgs( + InternalEntityEntry internalEntityEntry, + EntityState oldState, + EntityState newState) + : base(internalEntityEntry) + { + OldState = oldState; + NewState = newState; + } + + /// + /// The state that the entity is transitioning from. + /// + public virtual EntityState OldState { get; } + + /// + /// The state that the entity is transitioning to. + /// + public virtual EntityState NewState { get; } +} diff --git a/src/EFCore/ChangeTracking/EntityTrackingEventArgs.cs b/src/EFCore/ChangeTracking/EntityTrackingEventArgs.cs new file mode 100644 index 00000000000..985e43e173c --- /dev/null +++ b/src/EFCore/ChangeTracking/EntityTrackingEventArgs.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; + +namespace Microsoft.EntityFrameworkCore.ChangeTracking; + +/// +/// Event arguments for the event. +/// +/// +/// See State changes of entities in EF Core for more information and examples. +/// +public class EntityTrackingEventArgs : EntityEntryEventArgs +{ + /// + /// 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. + /// + [EntityFrameworkInternal] + public EntityTrackingEventArgs( + InternalEntityEntry internalEntityEntry, + EntityState state, + bool fromQuery) + : base(internalEntityEntry) + { + State = state; + FromQuery = fromQuery; + } + + /// + /// The that the tracked entity will be tracked with. + /// + public virtual EntityState State { get; } + + /// + /// if the entity is being tracked as part of a database query; otherwise. + /// + public virtual bool FromQuery { get; } +} diff --git a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs index 637af5e8f33..b552fd66a05 100644 --- a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs +++ b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs @@ -112,8 +112,11 @@ public virtual void PropertyChanging(InternalEntityEntry entry, IPropertyBase pr /// public virtual void DetectChanges(IStateManager stateManager) { - _logger.DetectChangesStarting(stateManager.Context); + OnDetectingChanges(stateManager); + var changesFound = false; + _logger.DetectChangesStarting(stateManager.Context); + foreach (var entry in stateManager.ToList()) // Might be too big, but usually _all_ entities are using Snapshot tracking { switch (entry.EntityState) @@ -125,16 +128,19 @@ public virtual void DetectChanges(IStateManager stateManager) { continue; } - - LocalDetectChanges(entry); - break; + goto default; default: - LocalDetectChanges(entry); + if (LocalDetectChanges(entry)) + { + changesFound = true; + } break; } } _logger.DetectChangesCompleted(stateManager.Context); + + OnDetectedChanges(stateManager, changesFound); } /// @@ -144,10 +150,15 @@ public virtual void DetectChanges(IStateManager stateManager) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual void DetectChanges(InternalEntityEntry entry) - => DetectChanges(entry, new HashSet { entry }); + { + OnDetectingChanges(entry); + OnDetectedChanges(entry, DetectChanges(entry, new HashSet { entry })); + } - private void DetectChanges(InternalEntityEntry entry, HashSet visited) + private bool DetectChanges(InternalEntityEntry entry, HashSet visited) { + var changesFound = false; + if (entry.EntityState != EntityState.Detached) { foreach (var foreignKey in entry.EntityType.GetForeignKeys()) @@ -158,21 +169,31 @@ private void DetectChanges(InternalEntityEntry entry, HashSet @@ -207,7 +242,7 @@ private void LocalDetectChanges(InternalEntityEntry entry) /// 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. /// - public void DetectValueChange(InternalEntityEntry entry, IProperty property) + public bool DetectValueChange(InternalEntityEntry entry, IProperty property) { var current = entry[property]; var original = entry.GetOriginalValue(property); @@ -222,8 +257,11 @@ public void DetectValueChange(InternalEntityEntry entry, IProperty property) { LogChangeDetected(entry, property, original, current); entry.SetPropertyModified(property); + return true; } } + + return false; } private void LogChangeDetected(InternalEntityEntry entry, IProperty property, object? original, object? current) @@ -238,11 +276,11 @@ private void LogChangeDetected(InternalEntityEntry entry, IProperty property, ob } } - private void DetectKeyChange(InternalEntityEntry entry, IProperty property) + private bool DetectKeyChange(InternalEntityEntry entry, IProperty property) { if (property.GetRelationshipIndex() < 0) { - return; + return false; } var snapshotValue = entry.GetRelationshipSnapshotValue(property); @@ -269,7 +307,11 @@ private void DetectKeyChange(InternalEntityEntry entry, IProperty property) entry.StateManager.InternalEntityEntryNotifier.KeyPropertyChanged( entry, property, keys, foreignKeys, snapshotValue, currentValue); + + return true; } + + return false; } /// @@ -278,7 +320,7 @@ private void DetectKeyChange(InternalEntityEntry entry, IProperty property) /// 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. /// - public void DetectNavigationChange(InternalEntityEntry entry, INavigationBase navigationBase) + public bool DetectNavigationChange(InternalEntityEntry entry, INavigationBase navigationBase) { var snapshotValue = entry.GetRelationshipSnapshotValue(navigationBase); var currentValue = entry[navigationBase]; @@ -337,9 +379,14 @@ public void DetectNavigationChange(InternalEntityEntry entry, INavigationBase na } stateManager.InternalEntityEntryNotifier.NavigationCollectionChanged(entry, navigationBase, added, removed); + + return true; } + + return false; } - else if (!ReferenceEquals(currentValue, snapshotValue)) + + if (!ReferenceEquals(currentValue, snapshotValue)) { Check.DebugAssert(navigationBase is INavigation, "Issue #21673. Non-collection skip navigations not supported."); @@ -354,6 +401,98 @@ public void DetectNavigationChange(InternalEntityEntry entry, INavigationBase na } stateManager.InternalEntityEntryNotifier.NavigationReferenceChanged(entry, navigation, snapshotValue, currentValue); + + return true; } + + return false; + } + + /// + /// 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. + /// + public event EventHandler? DetectingChanges; + + /// + /// 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. + /// + public virtual void OnDetectingChanges(InternalEntityEntry internalEntityEntry) + { + var @event = DetectingChanges; + + @event?.Invoke( + internalEntityEntry.StateManager.Context.ChangeTracker, + new DetectChangesEventArgs(internalEntityEntry)); + } + + /// + /// 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. + /// + public virtual void OnDetectingChanges(IStateManager stateManager) + { + var @event = DetectingChanges; + + @event?.Invoke( + stateManager.Context.ChangeTracker, + new DetectChangesEventArgs(null)); + } + + /// + /// 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. + /// + public event EventHandler? DetectedChanges; + + /// + /// 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. + /// + public virtual void OnDetectedChanges(InternalEntityEntry internalEntityEntry, bool changesFound) + { + var @event = DetectedChanges; + + @event?.Invoke( + internalEntityEntry.StateManager.Context.ChangeTracker, + new DetectedChangesEventArgs(internalEntityEntry, changesFound)); + } + + /// + /// 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. + /// + public virtual void OnDetectedChanges(IStateManager stateManager, bool changesFound) + { + var @event = DetectedChanges; + + @event?.Invoke( + stateManager.Context.ChangeTracker, + new DetectedChangesEventArgs(null, changesFound)); + } + + /// + /// 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. + /// + public virtual void ResetState() + { + DetectingChanges = null; + DetectedChanges = null; } } diff --git a/src/EFCore/ChangeTracking/Internal/IChangeDetector.cs b/src/EFCore/ChangeTracking/Internal/IChangeDetector.cs index 3ecc0534702..99089b9dc04 100644 --- a/src/EFCore/ChangeTracking/Internal/IChangeDetector.cs +++ b/src/EFCore/ChangeTracking/Internal/IChangeDetector.cs @@ -48,4 +48,60 @@ public interface IChangeDetector /// doing so can result in application failures when updating to a new Entity Framework Core release. /// void DetectChanges(InternalEntityEntry entry); + + /// + /// 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. + /// + event EventHandler? DetectingChanges; + + /// + /// 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. + /// + void OnDetectingChanges(InternalEntityEntry internalEntityEntry); + + /// + /// 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. + /// + void OnDetectingChanges(IStateManager stateManager); + + /// + /// 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. + /// + event EventHandler? DetectedChanges; + + /// + /// 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. + /// + void OnDetectedChanges(InternalEntityEntry internalEntityEntry, bool changesFound); + + /// + /// 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. + /// + void OnDetectedChanges(IStateManager stateManager, bool changesFound); + + /// + /// 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. + /// + void ResetState(); } diff --git a/src/EFCore/ChangeTracking/Internal/IStateManager.cs b/src/EFCore/ChangeTracking/Internal/IStateManager.cs index 1f69a9b167a..53e44c020df 100644 --- a/src/EFCore/ChangeTracking/Internal/IStateManager.cs +++ b/src/EFCore/ChangeTracking/Internal/IStateManager.cs @@ -191,7 +191,7 @@ IEnumerable GetNonDeletedEntities() /// 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. /// - void StateChanging(InternalEntityEntry entry, EntityState newState); + void ChangingState(InternalEntityEntry entry, EntityState newState); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -427,6 +427,22 @@ IEnumerable GetDependentsUsingRelationshipSnapshot( /// void Unsubscribe(); + /// + /// 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. + /// + event EventHandler? Tracking; + + /// + /// 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. + /// + void OnTracking(InternalEntityEntry internalEntityEntry, EntityState state, bool fromQuery); + /// /// 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 @@ -443,6 +459,22 @@ IEnumerable GetDependentsUsingRelationshipSnapshot( /// void OnTracked(InternalEntityEntry internalEntityEntry, bool fromQuery); + /// + /// 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. + /// + event EventHandler? StateChanging; + + /// + /// 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. + /// + void OnStateChanging(InternalEntityEntry internalEntityEntry, EntityState newState); + /// /// 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 diff --git a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs index b5ab1e4fcae..90576bc61ec 100644 --- a/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs +++ b/src/EFCore/ChangeTracking/Internal/InternalEntityEntry.cs @@ -295,7 +295,7 @@ private void SetEntityState(EntityState oldState, EntityState newState, bool acc _stateData.EntityState = oldState; } - StateManager.StateChanging(this, newState); + FireStateChanging(newState); if (newState == EntityState.Unchanged && oldState == EntityState.Modified) @@ -397,6 +397,20 @@ private void HandleSharedIdentityEntry(EntityState newState) } } + private void FireStateChanging(EntityState newState) + { + if (EntityState != EntityState.Detached) + { + StateManager.OnStateChanging(this, newState); + } + else + { + StateManager.OnTracking(this, newState, fromQuery: false); + } + + StateManager.ChangingState(this, newState); + } + private void FireStateChanged(EntityState oldState) { StateManager.InternalEntityEntryNotifier.StateChanged(this, oldState, fromQuery: false); @@ -445,6 +459,8 @@ private void SetServiceProperties(EntityState oldState, EntityState newState) /// public void MarkUnchangedFromQuery() { + StateManager.OnTracking(this, EntityState.Unchanged, fromQuery: true); + StateManager.InternalEntityEntryNotifier.StateChanging(this, EntityState.Unchanged); _stateData.EntityState = EntityState.Unchanged; @@ -562,7 +578,7 @@ public void SetPropertyModified( { if (changeState) { - StateManager.StateChanging(this, EntityState.Modified); + FireStateChanging(EntityState.Modified); SetServiceProperties(currentState, EntityState.Modified); @@ -585,7 +601,7 @@ public void SetPropertyModified( && !isModified && !_stateData.AnyPropertiesFlagged(PropertyFlag.Modified)) { - StateManager.StateChanging(this, EntityState.Unchanged); + FireStateChanging(EntityState.Unchanged); _stateData.EntityState = EntityState.Unchanged; StateManager.ChangedCount--; FireStateChanged(currentState); diff --git a/src/EFCore/ChangeTracking/Internal/StateManager.cs b/src/EFCore/ChangeTracking/Internal/StateManager.cs index 7d20dde900e..952ad4ec580 100644 --- a/src/EFCore/ChangeTracking/Internal/StateManager.cs +++ b/src/EFCore/ChangeTracking/Internal/StateManager.cs @@ -128,7 +128,7 @@ public StateManager(StateManagerDependencies dependencies) /// 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. /// - public virtual void StateChanging(InternalEntityEntry entry, EntityState newState) + public virtual void ChangingState(InternalEntityEntry entry, EntityState newState) { InternalEntityEntryNotifier.StateChanging(entry, newState); @@ -643,8 +643,11 @@ public virtual void ResetState() { Clear(); Dependencies.NavigationFixer.AbortDelayedFixup(); + _changeDetector?.ResetState(); + Tracking = null; Tracked = null; + StateChanging = null; StateChanged = null; } @@ -1351,6 +1354,27 @@ private static void AcceptAllChanges(IReadOnlyList changedEntries) } } + /// + /// 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. + /// + public event EventHandler? Tracking; + + /// + /// 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. + /// + public virtual void OnTracking(InternalEntityEntry internalEntityEntry, EntityState state, bool fromQuery) + { + var @event = Tracking; + + @event?.Invoke(Context.ChangeTracker, new EntityTrackingEventArgs(internalEntityEntry, state, fromQuery)); + } + /// /// 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 @@ -1381,6 +1405,28 @@ public virtual void OnTracked(InternalEntityEntry internalEntityEntry, bool from @event?.Invoke(Context.ChangeTracker, new EntityTrackedEventArgs(internalEntityEntry, fromQuery)); } + /// + /// 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. + /// + public event EventHandler? StateChanging; + + /// + /// 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. + /// + public virtual void OnStateChanging(InternalEntityEntry internalEntityEntry, EntityState newState) + { + var @event = StateChanging; + var oldState = internalEntityEntry.EntityState; + + @event?.Invoke(Context.ChangeTracker, new EntityStateChangingEventArgs(internalEntityEntry, oldState, newState)); + } + /// /// 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 diff --git a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs index 55586cdd32e..b933a567c5c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs @@ -834,8 +834,12 @@ public void Change_tracker_can_be_cleared_without_resetting_context_config() context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Immediate; context.Database.AutoTransactionsEnabled = true; context.Database.AutoSavepointsEnabled = true; + context.ChangeTracker.Tracking += ChangeTracker_OnTracking; context.ChangeTracker.Tracked += ChangeTracker_OnTracked; + context.ChangeTracker.StateChanging += ChangeTracker_OnStateChanging; context.ChangeTracker.StateChanged += ChangeTracker_OnStateChanged; + context.ChangeTracker.DetectingChanges += ChangeTracker_OnDetectingChanges; + context.ChangeTracker.DetectedChanges += ChangeTracker_OnDetectedChanges; context.SavingChanges += (sender, args) => { }; context.SavedChanges += (sender, args) => { }; context.SaveChangesFailed += (sender, args) => { }; @@ -850,14 +854,27 @@ public void Change_tracker_can_be_cleared_without_resetting_context_config() Assert.True(context.Database.AutoTransactionsEnabled); Assert.True(context.Database.AutoSavepointsEnabled); + Assert.False(_changeTracker_OnTracking); Assert.False(_changeTracker_OnTracked); + Assert.False(_changeTracker_OnStateChanging); Assert.False(_changeTracker_OnStateChanged); + Assert.False(_changeTracker_OnDetectingChanges); + Assert.False(_changeTracker_OnDetectedChanges); context.Customers.Attach( new Customer { CustomerId = "C" }).State = EntityState.Modified; + Assert.True(_changeTracker_OnTracking); Assert.True(_changeTracker_OnTracked); + Assert.True(_changeTracker_OnStateChanging); Assert.True(_changeTracker_OnStateChanged); + Assert.False(_changeTracker_OnDetectingChanges); + Assert.False(_changeTracker_OnDetectedChanges); + + context.ChangeTracker.DetectChanges(); + + Assert.True(_changeTracker_OnDetectingChanges); + Assert.True(_changeTracker_OnDetectedChanges); Assert.NotNull(GetContextEventField(context, nameof(DbContext.SavingChanges))); Assert.NotNull(GetContextEventField(context, nameof(DbContext.SavedChanges))); @@ -869,16 +886,36 @@ private object GetContextEventField(DbContext context, string eventName) .GetField(eventName, BindingFlags.GetField | BindingFlags.NonPublic | BindingFlags.Instance)! .GetValue(context); + private bool _changeTracker_OnTracking; + + private void ChangeTracker_OnTracking(object sender, EntityTrackingEventArgs e) + => _changeTracker_OnTracking = true; + private bool _changeTracker_OnTracked; private void ChangeTracker_OnTracked(object sender, EntityTrackedEventArgs e) => _changeTracker_OnTracked = true; + private bool _changeTracker_OnStateChanging; + + private void ChangeTracker_OnStateChanging(object sender, EntityStateChangingEventArgs e) + => _changeTracker_OnStateChanging = true; + private bool _changeTracker_OnStateChanged; private void ChangeTracker_OnStateChanged(object sender, EntityStateChangedEventArgs e) => _changeTracker_OnStateChanged = true; + private bool _changeTracker_OnDetectingChanges; + + private void ChangeTracker_OnDetectingChanges(object sender, DetectChangesEventArgs e) + => _changeTracker_OnDetectingChanges = true; + + private bool _changeTracker_OnDetectedChanges; + + private void ChangeTracker_OnDetectedChanges(object sender, DetectedChangesEventArgs e) + => _changeTracker_OnDetectedChanges = true; + [ConditionalTheory] [InlineData(false)] [InlineData(true)] diff --git a/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs b/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs index d23be69e0ac..578da0b0d78 100644 --- a/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs +++ b/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#nullable enable + using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.InMemory.ValueGeneration.Internal; @@ -24,22 +26,22 @@ public void Can_Add_with_identifying_relationships_dependent_first(int principal var added1 = context.Add(new DependentGG { Id = dependentKeyValue, PrincipalGG = new PrincipalGG { Id = principalKeyValue } }) .Entity; Assert.Equal(EntityState.Added, context.Entry(added1).State); - Assert.Equal(EntityState.Added, context.Entry(added1.PrincipalGG).State); + Assert.Equal(EntityState.Added, context.Entry(added1.PrincipalGG!).State); var added2 = context.Add(new DependentNG { Id = dependentKeyValue, PrincipalNG = new PrincipalNG { Id = principalKeyValue } }) .Entity; Assert.Equal(EntityState.Added, context.Entry(added2).State); - Assert.Equal(EntityState.Added, context.Entry(added2.PrincipalNG).State); + Assert.Equal(EntityState.Added, context.Entry(added2.PrincipalNG!).State); var added3 = context.Add(new DependentNN { Id = dependentKeyValue, PrincipalNN = new PrincipalNN { Id = principalKeyValue } }) .Entity; Assert.Equal(EntityState.Added, context.Entry(added3).State); - Assert.Equal(EntityState.Added, context.Entry(added3.PrincipalNN).State); + Assert.Equal(EntityState.Added, context.Entry(added3.PrincipalNN!).State); var added4 = context.Add(new DependentGN { Id = dependentKeyValue, PrincipalGN = new PrincipalGN { Id = principalKeyValue } }) .Entity; Assert.Equal(EntityState.Added, context.Entry(added4).State); - Assert.Equal(EntityState.Added, context.Entry(added4.PrincipalGN).State); + Assert.Equal(EntityState.Added, context.Entry(added4.PrincipalGN!).State); Assert.Equal(8, context.ChangeTracker.Entries().Count()); } @@ -56,22 +58,22 @@ public void Can_Add_with_identifying_relationships_principal_first(int principal var added1 = context.Add(new PrincipalGG { Id = principalKeyValue, DependentGG = new DependentGG { Id = dependentKeyValue } }) .Entity; Assert.Equal(EntityState.Added, context.Entry(added1).State); - Assert.Equal(EntityState.Added, context.Entry(added1.DependentGG).State); + Assert.Equal(EntityState.Added, context.Entry(added1.DependentGG!).State); var added2 = context.Add(new PrincipalNG { Id = principalKeyValue, DependentNG = new DependentNG { Id = dependentKeyValue } }) .Entity; Assert.Equal(EntityState.Added, context.Entry(added2).State); - Assert.Equal(EntityState.Added, context.Entry(added2.DependentNG).State); + Assert.Equal(EntityState.Added, context.Entry(added2.DependentNG!).State); var added3 = context.Add(new PrincipalNN { Id = principalKeyValue, DependentNN = new DependentNN { Id = dependentKeyValue } }) .Entity; Assert.Equal(EntityState.Added, context.Entry(added3).State); - Assert.Equal(EntityState.Added, context.Entry(added3.DependentNN).State); + Assert.Equal(EntityState.Added, context.Entry(added3.DependentNN!).State); var added4 = context.Add(new PrincipalGN { Id = principalKeyValue, DependentGN = new DependentGN { Id = dependentKeyValue } }) .Entity; Assert.Equal(EntityState.Added, context.Entry(added4).State); - Assert.Equal(EntityState.Added, context.Entry(added4.DependentGN).State); + Assert.Equal(EntityState.Added, context.Entry(added4.DependentGN!).State); Assert.Equal(8, context.ChangeTracker.Entries().Count()); } @@ -83,19 +85,19 @@ public void Can_Attach_with_identifying_relationships_dependent_first() var added1 = context.Attach(new DependentGG { PrincipalGG = new PrincipalGG() }).Entity; Assert.Equal(EntityState.Added, context.Entry(added1).State); - Assert.Equal(EntityState.Added, context.Entry(added1.PrincipalGG).State); + Assert.Equal(EntityState.Added, context.Entry(added1.PrincipalGG!).State); var added2 = context.Attach(new DependentNG { PrincipalNG = new PrincipalNG() }).Entity; Assert.Equal(EntityState.Added, context.Entry(added2).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added2.PrincipalNG).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added2.PrincipalNG!).State); var added3 = context.Attach(new DependentNN { PrincipalNN = new PrincipalNN() }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added3).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added3.PrincipalNN).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added3.PrincipalNN!).State); var added4 = context.Attach(new DependentGN { PrincipalGN = new PrincipalGN() }).Entity; Assert.Equal(EntityState.Added, context.Entry(added4).State); - Assert.Equal(EntityState.Added, context.Entry(added4.PrincipalGN).State); + Assert.Equal(EntityState.Added, context.Entry(added4.PrincipalGN!).State); Assert.Equal(8, context.ChangeTracker.Entries().Count()); } @@ -107,19 +109,19 @@ public void Can_Attach_with_identifying_relationships_dependent_first_with_princ var added1 = context.Attach(new DependentGG { PrincipalGG = new PrincipalGG { Id = 1 } }).Entity; Assert.Equal(EntityState.Added, context.Entry(added1).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added1.PrincipalGG).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added1.PrincipalGG!).State); var added2 = context.Attach(new DependentNG { PrincipalNG = new PrincipalNG { Id = 1 } }).Entity; Assert.Equal(EntityState.Added, context.Entry(added2).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added2.PrincipalNG).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added2.PrincipalNG!).State); var added3 = context.Attach(new DependentNN { PrincipalNN = new PrincipalNN { Id = 1 } }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added3).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added3.PrincipalNN).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added3.PrincipalNN!).State); var added4 = context.Attach(new DependentGN { PrincipalGN = new PrincipalGN { Id = 1 } }).Entity; Assert.Equal(EntityState.Added, context.Entry(added4).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added4.PrincipalGN).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added4.PrincipalGN!).State); Assert.Equal(8, context.ChangeTracker.Entries().Count()); } @@ -131,19 +133,19 @@ public void Can_Attach_with_identifying_relationships_dependent_first_with_depen var added1 = context.Attach(new DependentGG { Id = 1, PrincipalGG = new PrincipalGG() }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added1).State); - Assert.Equal(EntityState.Added, context.Entry(added1.PrincipalGG).State); + Assert.Equal(EntityState.Added, context.Entry(added1.PrincipalGG!).State); var added2 = context.Attach(new DependentNG { Id = 1, PrincipalNG = new PrincipalNG() }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added2).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added2.PrincipalNG).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added2.PrincipalNG!).State); var added3 = context.Attach(new DependentNN { Id = 1, PrincipalNN = new PrincipalNN() }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added3).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added3.PrincipalNN).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added3.PrincipalNN!).State); var added4 = context.Attach(new DependentGN { Id = 1, PrincipalGN = new PrincipalGN() }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added4).State); - Assert.Equal(EntityState.Added, context.Entry(added4.PrincipalGN).State); + Assert.Equal(EntityState.Added, context.Entry(added4.PrincipalGN!).State); Assert.Equal(8, context.ChangeTracker.Entries().Count()); } @@ -155,19 +157,19 @@ public void Can_Attach_with_identifying_relationships_dependent_first_with_all_k var added1 = context.Attach(new DependentGG { Id = 1, PrincipalGG = new PrincipalGG { Id = 1 } }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added1).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added1.PrincipalGG).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added1.PrincipalGG!).State); var added2 = context.Attach(new DependentNG { Id = 1, PrincipalNG = new PrincipalNG { Id = 1 } }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added2).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added2.PrincipalNG).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added2.PrincipalNG!).State); var added3 = context.Attach(new DependentNN { Id = 1, PrincipalNN = new PrincipalNN { Id = 1 } }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added3).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added3.PrincipalNN).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added3.PrincipalNN!).State); var added4 = context.Attach(new DependentGN { Id = 1, PrincipalGN = new PrincipalGN { Id = 1 } }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added4).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added4.PrincipalGN).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added4.PrincipalGN!).State); Assert.Equal(8, context.ChangeTracker.Entries().Count()); } @@ -179,19 +181,19 @@ public void Can_Attach_with_identifying_relationships_principal_first() var added1 = context.Attach(new PrincipalGG { DependentGG = new DependentGG() }).Entity; Assert.Equal(EntityState.Added, context.Entry(added1).State); - Assert.Equal(EntityState.Added, context.Entry(added1.DependentGG).State); + Assert.Equal(EntityState.Added, context.Entry(added1.DependentGG!).State); var added2 = context.Attach(new PrincipalNG { DependentNG = new DependentNG() }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added2).State); - Assert.Equal(EntityState.Added, context.Entry(added2.DependentNG).State); + Assert.Equal(EntityState.Added, context.Entry(added2.DependentNG!).State); var added3 = context.Attach(new PrincipalNN { DependentNN = new DependentNN() }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added3).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added3.DependentNN).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added3.DependentNN!).State); var added4 = context.Attach(new PrincipalGN { DependentGN = new DependentGN() }).Entity; Assert.Equal(EntityState.Added, context.Entry(added4).State); - Assert.Equal(EntityState.Added, context.Entry(added4.DependentGN).State); + Assert.Equal(EntityState.Added, context.Entry(added4.DependentGN!).State); Assert.Equal(8, context.ChangeTracker.Entries().Count()); } @@ -203,19 +205,19 @@ public void Can_Attach_with_identifying_relationships_principal_first_with_princ var added1 = context.Attach(new PrincipalGG { Id = 1, DependentGG = new DependentGG() }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added1).State); - Assert.Equal(EntityState.Added, context.Entry(added1.DependentGG).State); + Assert.Equal(EntityState.Added, context.Entry(added1.DependentGG!).State); var added2 = context.Attach(new PrincipalNG { Id = 1, DependentNG = new DependentNG() }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added2).State); - Assert.Equal(EntityState.Added, context.Entry(added2.DependentNG).State); + Assert.Equal(EntityState.Added, context.Entry(added2.DependentNG!).State); var added3 = context.Attach(new PrincipalNN { Id = 1, DependentNN = new DependentNN() }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added3).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added3.DependentNN).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added3.DependentNN!).State); var added4 = context.Attach(new PrincipalGN { Id = 1, DependentGN = new DependentGN() }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added4).State); - Assert.Equal(EntityState.Added, context.Entry(added4.DependentGN).State); + Assert.Equal(EntityState.Added, context.Entry(added4.DependentGN!).State); Assert.Equal(8, context.ChangeTracker.Entries().Count()); } @@ -227,19 +229,19 @@ public void Can_Attach_with_identifying_relationships_principal_first_with_depen var added1 = context.Attach(new PrincipalGG { DependentGG = new DependentGG { Id = 1 } }).Entity; Assert.Equal(EntityState.Added, context.Entry(added1).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added1.DependentGG).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added1.DependentGG!).State); var added2 = context.Attach(new PrincipalNG { DependentNG = new DependentNG { Id = 1 } }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added2).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added2.DependentNG).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added2.DependentNG!).State); var added3 = context.Attach(new PrincipalNN { DependentNN = new DependentNN { Id = 1 } }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added3).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added3.DependentNN).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added3.DependentNN!).State); var added4 = context.Attach(new PrincipalGN { DependentGN = new DependentGN { Id = 1 } }).Entity; Assert.Equal(EntityState.Added, context.Entry(added4).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added4.DependentGN).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added4.DependentGN!).State); Assert.Equal(8, context.ChangeTracker.Entries().Count()); } @@ -251,19 +253,19 @@ public void Can_Attach_with_identifying_relationships_principal_first_with_all_k var added1 = context.Attach(new PrincipalGG { Id = 1, DependentGG = new DependentGG { Id = 1 } }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added1).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added1.DependentGG).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added1.DependentGG!).State); var added2 = context.Attach(new PrincipalNG { Id = 1, DependentNG = new DependentNG { Id = 1 } }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added2).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added2.DependentNG).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added2.DependentNG!).State); var added3 = context.Attach(new PrincipalNN { Id = 1, DependentNN = new DependentNN { Id = 1 } }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added3).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added3.DependentNN).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added3.DependentNN!).State); var added4 = context.Attach(new PrincipalGN { Id = 1, DependentGN = new DependentGN { Id = 1 } }).Entity; Assert.Equal(EntityState.Unchanged, context.Entry(added4).State); - Assert.Equal(EntityState.Unchanged, context.Entry(added4.DependentGN).State); + Assert.Equal(EntityState.Unchanged, context.Entry(added4.DependentGN!).State); Assert.Equal(8, context.ChangeTracker.Entries().Count()); } @@ -379,7 +381,7 @@ public async Task Attached_owned_collection_entity_without_owner_not_saved(bool public class Hero { public Guid Id { get; set; } - public ICollection Weaks { get; set; } + public ICollection Weaks { get; } = new List(); } public class Weak @@ -387,14 +389,14 @@ public class Weak public Guid Id { get; set; } public Guid HeroId { get; set; } - public Hero Hero { get; set; } + public Hero? Hero { get; set; } } public class Mike { public Guid Id { get; set; } - public ICollection TheStreets { get; set; } - public Skinner TheHero { get; set; } + public ICollection TheStreets { get; } = new List(); + public Skinner? TheHero { get; set; } } public class Skinner @@ -461,7 +463,7 @@ public void Detect_property_change_is_logged(bool sensitive, bool callDetectChan Seed(sensitive); using var context = sensitive ? new LikeAZooContextSensitive() : new LikeAZooContext(); - var cat = context.Cats.Find(1); + var cat = context.Cats.Find(1)!; _loggerFactory.Log.Clear(); @@ -508,7 +510,7 @@ public void Property_changes_on_Deleted_entities_are_not_continually_detected(bo Seed(sensitive); using var context = sensitive ? new LikeAZooContextSensitive() : new LikeAZooContext(); - var cat = context.Cats.Find(1); + var cat = context.Cats.Find(1)!; _loggerFactory.Log.Clear(); @@ -813,7 +815,7 @@ public void State_change_is_logged(bool sensitive) Seed(sensitive); using var context = sensitive ? new LikeAZooContextSensitive() : new LikeAZooContext(); - var cat = context.Cats.Find(1); + var cat = context.Cats.Find(1)!; _loggerFactory.Log.Clear(); @@ -844,7 +846,7 @@ public async Task Value_generation_is_logged(bool sensitive, bool async, bool te using var context = sensitive ? new LikeAZooContextSensitive() : new LikeAZooContext(); ResetValueGenerator( context, - context.Model.FindEntityType(typeof(Hat)).FindProperty(nameof(Hat.Id)), + context.Model.FindEntityType(typeof(Hat))!.FindProperty(nameof(Hat.Id))!, temporary); _loggerFactory.Log.Clear(); @@ -967,8 +969,8 @@ public void Cascade_delete_is_logged( var cat = context.Cats.Include(e => e.Hats).Single(e => e.Id == 1); LogLevel? cascadeDeleteLevel = null; - string cascadeDeleteMessage = null; - string deleteOrphansMessage = null; + string? cascadeDeleteMessage = null; + string? deleteOrphansMessage = null; void CaptureMessages() { @@ -1086,8 +1088,8 @@ public void Cascade_delete_orphan_is_logged( var cat = context.Cats.Include(e => e.Hats).Single(e => e.Id == 1); LogLevel? deleteOrphansLevel = null; - string cascadeDeleteMessage = null; - string deleteOrphansMessage = null; + string? cascadeDeleteMessage = null; + string? deleteOrphansMessage = null; void CaptureMessages() { @@ -1161,7 +1163,7 @@ public async Task SaveChanges_is_logged(bool async) Seed(); using var context = new LikeAZooContext(); - var cat = context.Cats.Find(1); + var cat = context.Cats.Find(1)!; context.Entry(cat).State = EntityState.Deleted; @@ -1209,29 +1211,33 @@ public void Context_Dispose_is_logged() [ConditionalFact] public void State_change_events_fire_from_query() { + var tracking = new List(); var tracked = new List(); + var changing = new List(); var changed = new List(); Seed(usePool: true); using (var scope = _poolProvider.CreateScope()) { - var context = scope.ServiceProvider.GetService(); + var context = scope.ServiceProvider.GetRequiredService(); - RegisterEvents(context, tracked, changed); + RegisterEvents(context, tracking, tracked, changing, changed); Assert.Equal(2, context.Cats.OrderBy(e => e.Id).ToList().Count); + Assert.Equal(2, tracking.Count); Assert.Equal(2, tracked.Count); + Assert.Empty(changing); Assert.Empty(changed); - AssertTrackedEvent(context, 1, EntityState.Unchanged, tracked[0], fromQuery: true); - AssertTrackedEvent(context, 2, EntityState.Unchanged, tracked[1], fromQuery: true); + AssertTrackedEvent(context, 1, EntityState.Unchanged, tracking[0], tracked[0], fromQuery: true); + AssertTrackedEvent(context, 2, EntityState.Unchanged, tracking[1], tracked[1], fromQuery: true); } using (var scope = _poolProvider.CreateScope()) { - var context = scope.ServiceProvider.GetService(); + var context = scope.ServiceProvider.GetRequiredService(); Assert.Equal(2, context.Cats.OrderBy(e => e.Id).ToList().Count); @@ -1243,13 +1249,15 @@ public void State_change_events_fire_from_query() [ConditionalFact] public void State_change_events_fire_from_Attach() { + var tracking = new List(); var tracked = new List(); + var changing = new List(); var changed = new List(); using var scope = _poolProvider.CreateScope(); - var context = scope.ServiceProvider.GetService(); + var context = scope.ServiceProvider.GetRequiredService(); - RegisterEvents(context, tracked, changed); + RegisterEvents(context, tracking, tracked, changing, changed); context.Attach(new Cat(1)); @@ -1258,7 +1266,7 @@ public void State_change_events_fire_from_Attach() Assert.Single(tracked); Assert.Empty(changed); - AssertTrackedEvent(context, 1, EntityState.Unchanged, tracked[0], fromQuery: false); + AssertTrackedEvent(context, 1, EntityState.Unchanged, tracking[0], tracked[0], fromQuery: false); context.Entry(new Cat(2)).State = EntityState.Unchanged; @@ -1267,19 +1275,21 @@ public void State_change_events_fire_from_Attach() Assert.Equal(2, tracked.Count); Assert.Empty(changed); - AssertTrackedEvent(context, 2, EntityState.Unchanged, tracked[1], fromQuery: false); + AssertTrackedEvent(context, 2, EntityState.Unchanged, tracking[1], tracked[1], fromQuery: false); } [ConditionalFact] public void State_change_events_fire_from_Add() { + var tracking = new List(); var tracked = new List(); + var changing = new List(); var changed = new List(); using var scope = _poolProvider.CreateScope(); - var context = scope.ServiceProvider.GetService(); + var context = scope.ServiceProvider.GetRequiredService(); - RegisterEvents(context, tracked, changed); + RegisterEvents(context, tracking, tracked, changing, changed); context.Add(new Cat(1)); @@ -1288,7 +1298,7 @@ public void State_change_events_fire_from_Add() Assert.Single(tracked); Assert.Empty(changed); - AssertTrackedEvent(context, 1, EntityState.Added, tracked[0], fromQuery: false); + AssertTrackedEvent(context, 1, EntityState.Added, tracking[0], tracked[0], fromQuery: false); context.Entry(new Cat(2)).State = EntityState.Added; @@ -1297,19 +1307,21 @@ public void State_change_events_fire_from_Add() Assert.Equal(2, tracked.Count); Assert.Empty(changed); - AssertTrackedEvent(context, 2, EntityState.Added, tracked[1], fromQuery: false); + AssertTrackedEvent(context, 2, EntityState.Added, tracking[1], tracked[1], fromQuery: false); } [ConditionalFact] public void State_change_events_fire_from_Update() { + var tracking = new List(); var tracked = new List(); + var changing = new List(); var changed = new List(); using var scope = _poolProvider.CreateScope(); - var context = scope.ServiceProvider.GetService(); + var context = scope.ServiceProvider.GetRequiredService(); - RegisterEvents(context, tracked, changed); + RegisterEvents(context, tracking, tracked, changing, changed); context.Update(new Cat(1)); @@ -1318,7 +1330,7 @@ public void State_change_events_fire_from_Update() Assert.Single(tracked); Assert.Empty(changed); - AssertTrackedEvent(context, 1, EntityState.Modified, tracked[0], fromQuery: false); + AssertTrackedEvent(context, 1, EntityState.Modified, tracking[0], tracked[0], fromQuery: false); context.Entry(new Cat(2)).State = EntityState.Modified; @@ -1327,20 +1339,22 @@ public void State_change_events_fire_from_Update() Assert.Equal(2, tracked.Count); Assert.Empty(changed); - AssertTrackedEvent(context, 2, EntityState.Modified, tracked[1], fromQuery: false); + AssertTrackedEvent(context, 2, EntityState.Modified, tracking[1], tracked[1], fromQuery: false); } [ConditionalFact] public void State_change_events_fire_for_tracked_state_changes() { + var tracking = new List(); var tracked = new List(); + var changing = new List(); var changed = new List(); using (var scope = _poolProvider.CreateScope()) { - var context = scope.ServiceProvider.GetService(); + var context = scope.ServiceProvider.GetRequiredService(); - RegisterEvents(context, tracked, changed); + RegisterEvents(context, tracking, tracked, changing, changed); context.AddRange(new Cat(1), new Cat(2)); @@ -1349,55 +1363,55 @@ public void State_change_events_fire_for_tracked_state_changes() Assert.Equal(2, tracked.Count); Assert.Empty(changed); - AssertTrackedEvent(context, 1, EntityState.Added, tracked[0], fromQuery: false); - AssertTrackedEvent(context, 2, EntityState.Added, tracked[1], fromQuery: false); + AssertTrackedEvent(context, 1, EntityState.Added, tracking[0], tracked[0], fromQuery: false); + AssertTrackedEvent(context, 2, EntityState.Added, tracking[1], tracked[1], fromQuery: false); - context.Entry(context.Cats.Find(1)).State = EntityState.Unchanged; - context.Entry(context.Cats.Find(2)).State = EntityState.Modified; + context.Entry(context.Cats.Find(1)!).State = EntityState.Unchanged; + context.Entry(context.Cats.Find(2)!).State = EntityState.Modified; Assert.Equal(2, tracked.Count); Assert.Equal(2, changed.Count); Assert.True(context.ChangeTracker.HasChanges()); - AssertChangedEvent(context, 1, EntityState.Added, EntityState.Unchanged, changed[0]); - AssertChangedEvent(context, 2, EntityState.Added, EntityState.Modified, changed[1]); + AssertChangedEvent(context, 1, EntityState.Added, EntityState.Unchanged, changing[0], changed[0]); + AssertChangedEvent(context, 2, EntityState.Added, EntityState.Modified, changing[1], changed[1]); - context.Entry(context.Cats.Find(1)).State = EntityState.Added; - context.Entry(context.Cats.Find(2)).State = EntityState.Deleted; + context.Entry(context.Cats.Find(1)!).State = EntityState.Added; + context.Entry(context.Cats.Find(2)!).State = EntityState.Deleted; Assert.Equal(2, tracked.Count); Assert.Equal(4, changed.Count); - AssertChangedEvent(context, 1, EntityState.Unchanged, EntityState.Added, changed[2]); - AssertChangedEvent(context, 2, EntityState.Modified, EntityState.Deleted, changed[3]); + AssertChangedEvent(context, 1, EntityState.Unchanged, EntityState.Added, changing[2], changed[2]); + AssertChangedEvent(context, 2, EntityState.Modified, EntityState.Deleted, changing[3], changed[3]); - context.Remove(context.Cats.Find(1)); - context.Entry(context.Cats.Find(2)).State = EntityState.Detached; + context.Remove(context.Cats.Find(1)!); + context.Entry(context.Cats.Find(2)!).State = EntityState.Detached; Assert.False(context.ChangeTracker.HasChanges()); Assert.Equal(2, tracked.Count); Assert.Equal(6, changed.Count); - AssertChangedEvent(context, null, EntityState.Added, EntityState.Detached, changed[4]); - AssertChangedEvent(context, null, EntityState.Deleted, EntityState.Detached, changed[5]); + AssertChangedEvent(context, null, EntityState.Added, EntityState.Detached, changing[4], changed[4]); + AssertChangedEvent(context, null, EntityState.Deleted, EntityState.Detached, changing[5], changed[5]); } using (var scope = _poolProvider.CreateScope()) { - var context = scope.ServiceProvider.GetService(); + var context = scope.ServiceProvider.GetRequiredService(); context.AddRange(new Cat(1), new Cat(2)); - context.Entry(context.Cats.Find(1)).State = EntityState.Unchanged; - context.Entry(context.Cats.Find(2)).State = EntityState.Modified; + context.Entry(context.Cats.Find(1)!).State = EntityState.Unchanged; + context.Entry(context.Cats.Find(2)!).State = EntityState.Modified; - context.Entry(context.Cats.Find(1)).State = EntityState.Added; - context.Entry(context.Cats.Find(2)).State = EntityState.Deleted; + context.Entry(context.Cats.Find(1)!).State = EntityState.Added; + context.Entry(context.Cats.Find(2)!).State = EntityState.Deleted; - context.Remove(context.Cats.Find(1)); - context.Entry(context.Cats.Find(2)).State = EntityState.Detached; + context.Remove(context.Cats.Find(1)!); + context.Entry(context.Cats.Find(2)!).State = EntityState.Detached; Assert.Equal(2, tracked.Count); Assert.Equal(6, changed.Count); @@ -1409,22 +1423,24 @@ public void State_change_events_fire_for_tracked_state_changes() [InlineData(true)] public void State_change_events_fire_when_saving_changes(bool callDetectChangesTwice) { + var tracking = new List(); var tracked = new List(); + var changing = new List(); var changed = new List(); Seed(usePool: true); using var scope = _poolProvider.CreateScope(); - var context = scope.ServiceProvider.GetService(); + var context = scope.ServiceProvider.GetRequiredService(); - RegisterEvents(context, tracked, changed); + RegisterEvents(context, tracking, tracked, changing, changed); - var cat1 = context.Cats.Find(1); + var cat1 = context.Cats.Find(1)!; Assert.Single(tracked); Assert.Empty(changed); - AssertTrackedEvent(context, 1, EntityState.Unchanged, tracked[0], fromQuery: true); + AssertTrackedEvent(context, 1, EntityState.Unchanged, tracking[0], tracked[0], fromQuery: true); context.Add(new Cat(3)); cat1.Name = "Clippy"; @@ -1439,8 +1455,8 @@ public void State_change_events_fire_when_saving_changes(bool callDetectChangesT Assert.Equal(2, tracked.Count); Assert.Single(changed); - AssertTrackedEvent(context, 3, EntityState.Added, tracked[1], fromQuery: false); - AssertChangedEvent(context, 1, EntityState.Unchanged, EntityState.Modified, changed[0]); + AssertTrackedEvent(context, 3, EntityState.Added, tracking[1], tracked[1], fromQuery: false); + AssertChangedEvent(context, 1, EntityState.Unchanged, EntityState.Modified, changing[0], changed[0]); Assert.True(context.ChangeTracker.HasChanges()); @@ -1451,8 +1467,8 @@ public void State_change_events_fire_when_saving_changes(bool callDetectChangesT Assert.Equal(2, tracked.Count); Assert.Equal(3, changed.Count); - AssertChangedEvent(context, 1, EntityState.Modified, EntityState.Unchanged, changed[2]); - AssertChangedEvent(context, 3, EntityState.Added, EntityState.Unchanged, changed[1]); + AssertChangedEvent(context, 1, EntityState.Modified, EntityState.Unchanged, changing[2], changed[2]); + AssertChangedEvent(context, 3, EntityState.Added, EntityState.Unchanged, changing[1], changed[1]); context.Database.EnsureDeleted(); } @@ -1460,13 +1476,15 @@ public void State_change_events_fire_when_saving_changes(bool callDetectChangesT [ConditionalFact] public void State_change_events_fire_when_property_modified_flags_cause_state_change() { + var tracking = new List(); var tracked = new List(); + var changing = new List(); var changed = new List(); using var scope = _poolProvider.CreateScope(); - var context = scope.ServiceProvider.GetService(); + var context = scope.ServiceProvider.GetRequiredService(); - RegisterEvents(context, tracked, changed); + RegisterEvents(context, tracking, tracked, changing, changed); var cat = context.Attach( new Cat(3) { Name = "Achilles" }).Entity; @@ -1476,7 +1494,7 @@ public void State_change_events_fire_when_property_modified_flags_cause_state_ch Assert.Single(tracked); Assert.Empty(changed); - AssertTrackedEvent(context, 3, EntityState.Unchanged, tracked[0], fromQuery: false); + AssertTrackedEvent(context, 3, EntityState.Unchanged, tracking[0], tracked[0], fromQuery: false); context.Entry(cat).Property(e => e.Name).IsModified = true; @@ -1485,7 +1503,7 @@ public void State_change_events_fire_when_property_modified_flags_cause_state_ch Assert.Single(tracked); Assert.Single(changed); - AssertChangedEvent(context, 3, EntityState.Unchanged, EntityState.Modified, changed[0]); + AssertChangedEvent(context, 3, EntityState.Unchanged, EntityState.Modified, changing[0], changed[0]); context.Entry(cat).Property(e => e.Name).IsModified = false; @@ -1494,36 +1512,40 @@ public void State_change_events_fire_when_property_modified_flags_cause_state_ch Assert.Single(tracked); Assert.Equal(2, changed.Count); - AssertChangedEvent(context, 3, EntityState.Modified, EntityState.Unchanged, changed[1]); + AssertChangedEvent(context, 3, EntityState.Modified, EntityState.Unchanged, changing[1], changed[1]); } [ConditionalFact] public void State_change_events_are_limited_to_the_current_context() { + var tracking1 = new List(); var tracked1 = new List(); + var changing1 = new List(); var changed1 = new List(); + var tracking2 = new List(); var tracked2 = new List(); + var changing2 = new List(); var changed2 = new List(); Seed(usePool: true); using var scope = _poolProvider.CreateScope(); - var context = scope.ServiceProvider.GetService(); + var context = scope.ServiceProvider.GetRequiredService(); - RegisterEvents(context, tracked1, changed1); + RegisterEvents(context, tracking1, tracked1, changing1, changed1); using (var scope2 = _poolProvider.CreateScope()) { - var context2 = scope2.ServiceProvider.GetService(); + var context2 = scope2.ServiceProvider.GetRequiredService(); - RegisterEvents(context2, tracked2, changed2); + RegisterEvents(context2, tracking2, tracked2, changing2, changed2); Assert.Equal(2, context2.Cats.OrderBy(e => e.Id).ToList().Count); Assert.Equal(2, tracked2.Count); Assert.Empty(changed2); - context2.Entry(context2.Cats.Find(1)).State = EntityState.Modified; + context2.Entry(context2.Cats.Find(1)!).State = EntityState.Modified; Assert.Equal(2, tracked2.Count); Assert.Single(changed2); @@ -1537,7 +1559,7 @@ public void State_change_events_are_limited_to_the_current_context() Assert.Equal(2, tracked1.Count); Assert.Empty(changed1); - context.Entry(context.Cats.Find(1)).State = EntityState.Modified; + context.Entry(context.Cats.Find(1)!).State = EntityState.Modified; Assert.Equal(2, tracked1.Count); Assert.Single(changed1); @@ -1548,14 +1570,339 @@ public void State_change_events_are_limited_to_the_current_context() context.Database.EnsureDeleted(); } + [ConditionalFact] + public void DetectChanges_events_fire_for_no_change() + { + var detecting = new List(); + var detected = new List(); + + using var scope = _poolProvider.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + + RegisterEvents(context, detecting, detected); + + context.AttachRange(new Cat(1), new Cat(2)); + + Assert.Empty(detecting); + Assert.Empty(detected); + + Assert.False(context.ChangeTracker.HasChanges()); + + Assert.Single(detecting); + Assert.Single(detected); + + AssertDetectChangesEvent(context, changesFound: false, detecting[0], detected[0]); + } + + [ConditionalFact] + public void DetectChanges_events_fire_for_property_change() + { + var detecting = new List(); + var detected = new List(); + + using var scope = _poolProvider.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + + RegisterEvents(context, detecting, detected); + + var cat = new Cat(1); + context.AttachRange(cat, new Cat(2)); + cat.Name = "Alice"; + + Assert.Empty(detecting); + Assert.Empty(detected); + + Assert.True(context.ChangeTracker.HasChanges()); + + Assert.Single(detecting); + Assert.Single(detected); + + AssertDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + } + + [ConditionalFact] + public void DetectChanges_events_fire_for_fk_change() + { + var detecting = new List(); + var detected = new List(); + + using var scope = _poolProvider.CreateScope(); + + using var context = new EarlyLearningCenter(); + + RegisterEvents(context, detecting, detected); + + var product = new Product { Category = new Category()}; + context.Attach(product); + product.CategoryId = 2; + + Assert.Empty(detecting); + Assert.Empty(detected); + + Assert.True(context.ChangeTracker.HasChanges()); + + Assert.Single(detecting); + Assert.Single(detected); + + AssertDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + } + + [ConditionalFact] + public void DetectChanges_events_fire_for_reference_navigation_change() + { + var detecting = new List(); + var detected = new List(); + + using var scope = _poolProvider.CreateScope(); + + using var context = new EarlyLearningCenter(); + + RegisterEvents(context, detecting, detected); + + var product = new Product { Category = new Category()}; + context.Attach(product); + product.Category = null; + + Assert.Empty(detecting); + Assert.Empty(detected); + + Assert.True(context.ChangeTracker.HasChanges()); + + Assert.Single(detecting); + Assert.Single(detected); + + AssertDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + } + + [ConditionalFact] + public void DetectChanges_events_fire_for_collection_navigation_change() + { + var detecting = new List(); + var detected = new List(); + + using var scope = _poolProvider.CreateScope(); + + using var context = new EarlyLearningCenter(); + + RegisterEvents(context, detecting, detected); + + var product = new Product { Category = new Category()}; + context.Attach(product); + product.Category.Products.Clear(); + + Assert.Empty(detecting); + Assert.Empty(detected); + + Assert.True(context.ChangeTracker.HasChanges()); + + Assert.Single(detecting); + Assert.Single(detected); + + AssertDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + } + + [ConditionalFact] + public void DetectChanges_events_fire_for_skip_navigation_change() + { + var detecting = new List(); + var detected = new List(); + + using var scope = _poolProvider.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + + RegisterEvents(context, detecting, detected); + + var cat = new Cat(1) { Hats = { new Hat(2) }}; + context.Attach(cat); + cat.Hats.Clear(); + + Assert.Empty(detecting); + Assert.Empty(detected); + + Assert.True(context.ChangeTracker.HasChanges()); + + Assert.Single(detecting); + Assert.Single(detected); + + AssertDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + } + + [ConditionalFact] + public void Local_DetectChanges_events_fire_for_no_change() + { + var detecting = new List(); + var detected = new List(); + + using var scope = _poolProvider.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + + RegisterEvents(context, detecting, detected); + + var cat = new Cat(1); + context.AttachRange(cat, new Cat(2)); + + Assert.Empty(detecting); + Assert.Empty(detected); + + _ = context.Entry(cat); + + Assert.Single(detecting); + Assert.Single(detected); + + AssertLocalDetectChangesEvent(context, changesFound: false, detecting[0], detected[0]); + } + + [ConditionalFact] + public void Local_DetectChanges_events_fire_for_property_change() + { + var detecting = new List(); + var detected = new List(); + + using var scope = _poolProvider.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + + RegisterEvents(context, detecting, detected); + + var cat = new Cat(1); + context.AttachRange(cat, new Cat(2)); + cat.Name = "Alice"; + + Assert.Empty(detecting); + Assert.Empty(detected); + + _ = context.Entry(cat); + + Assert.Single(detecting); + Assert.Single(detected); + + AssertLocalDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + } + + [ConditionalFact] + public void Local_DetectChanges_events_fire_for_fk_change() + { + var detecting = new List(); + var detected = new List(); + + using var scope = _poolProvider.CreateScope(); + + using var context = new EarlyLearningCenter(); + + RegisterEvents(context, detecting, detected); + + var product = new Product { Category = new Category()}; + context.Attach(product); + product.CategoryId = 2; + + Assert.Empty(detecting); + Assert.Empty(detected); + + _ = context.Entry(product); + + Assert.Single(detecting); + Assert.Single(detected); + + AssertLocalDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + } + + [ConditionalFact] + public void Local_DetectChanges_events_fire_for_reference_navigation_change() + { + var detecting = new List(); + var detected = new List(); + + using var scope = _poolProvider.CreateScope(); + + using var context = new EarlyLearningCenter(); + + RegisterEvents(context, detecting, detected); + + var product = new Product { Category = new Category()}; + context.Attach(product); + product.Category = null; + + Assert.Empty(detecting); + Assert.Empty(detected); + + _ = context.Entry(product); + + Assert.Single(detecting); + Assert.Single(detected); + + AssertLocalDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + } + + [ConditionalFact] + public void Local_DetectChanges_events_fire_for_collection_navigation_change() + { + var detecting = new List(); + var detected = new List(); + + using var scope = _poolProvider.CreateScope(); + + using var context = new EarlyLearningCenter(); + + RegisterEvents(context, detecting, detected); + + var product = new Product { Category = new Category()}; + context.Attach(product); + product.Category.Products.Clear(); + + Assert.Empty(detecting); + Assert.Empty(detected); + + _ = context.Entry(product.Category); + + Assert.Single(detecting); + Assert.Single(detected); + + AssertLocalDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + } + + [ConditionalFact] + public void Local_DetectChanges_events_fire_for_skip_navigation_change() + { + var detecting = new List(); + var detected = new List(); + + using var scope = _poolProvider.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + + RegisterEvents(context, detecting, detected); + + var cat = new Cat(1) { Hats = { new Hat(2) }}; + context.Attach(cat); + cat.Hats.Clear(); + + Assert.Empty(detecting); + Assert.Empty(detected); + + _ = context.Entry(cat); + + Assert.Single(detecting); + Assert.Single(detected); + + AssertLocalDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + } + private static void AssertTrackedEvent( LikeAZooContext context, int id, EntityState newState, + EntityTrackingEventArgs tracking, EntityTrackedEventArgs tracked, bool fromQuery) { + Assert.Same(tracking.Entry.Entity, tracked.Entry.Entity); + Assert.Equal(newState, tracking.State); Assert.Equal(newState, tracked.Entry.State); + Assert.Equal(fromQuery, tracking.FromQuery); Assert.Equal(fromQuery, tracked.FromQuery); Assert.Same(context.Cats.Find(id), tracked.Entry.Entity); } @@ -1565,8 +1912,12 @@ private static void AssertChangedEvent( int? id, EntityState oldState, EntityState newState, + EntityStateChangingEventArgs changing, EntityStateChangedEventArgs changed) { + Assert.Same(changing.Entry.Entity, changed.Entry.Entity); + Assert.Equal(oldState, changing.OldState); + Assert.Equal(newState, changing.NewState); Assert.Equal(oldState, changed.OldState); Assert.Equal(newState, changed.NewState); Assert.Equal(newState, changed.Entry.State); @@ -1577,17 +1928,54 @@ private static void AssertChangedEvent( } } + private static void AssertDetectChangesEvent( + DbContext context, + bool changesFound, + DetectChangesEventArgs detecting, + DetectedChangesEventArgs detected) + { + Assert.Null(detecting.Entry); + Assert.Null(detected.Entry); + Assert.Equal(changesFound, detected.ChangesFound); + } + + private static void AssertLocalDetectChangesEvent( + DbContext context, + bool changesFound, + DetectChangesEventArgs detecting, + DetectedChangesEventArgs detected) + { + Assert.NotNull(detecting.Entry); + Assert.Same(detecting.Entry!.Entity, detected.Entry!.Entity); + Assert.Equal(changesFound, detected.ChangesFound); + } + private static void RegisterEvents( - LikeAZooContext context, + DbContext context, + IList tracking, IList tracked, + IList changing, IList changed) { + context.ChangeTracker.Tracking += (s, e) => + { + Assert.Same(context.ChangeTracker, s); + tracking.Add(e); + }; + context.ChangeTracker.Tracked += (s, e) => { Assert.Same(context.ChangeTracker, s); tracked.Add(e); }; + context.ChangeTracker.StateChanging += (s, e) => + { + Assert.Same(context.ChangeTracker, s); + Assert.Equal(e.OldState, e.Entry.State); + changing.Add(e); + }; + context.ChangeTracker.StateChanged += (s, e) => { Assert.Same(context.ChangeTracker, s); @@ -1596,6 +1984,24 @@ private static void RegisterEvents( }; } + private static void RegisterEvents( + DbContext context, + IList detecting, + IList detected) + { + context.ChangeTracker.DetectingChanges += (s, e) => + { + Assert.Same(context.ChangeTracker, s); + detecting.Add(e); + }; + + context.ChangeTracker.DetectedChanges += (s, e) => + { + Assert.Same(context.ChangeTracker, s); + detected.Add(e); + }; + } + private class Cat { public Cat(int id) @@ -1606,7 +2012,7 @@ public Cat(int id) // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Local public int Id { get; private set; } - public string Name { get; set; } + public string? Name { get; set; } public ICollection Hats { get; } = new List(); @@ -1623,10 +2029,10 @@ public Hat(int id) // ReSharper disable once AutoPropertyCanBeMadeGetOnly.Local public int Id { get; private set; } - public string Color { get; set; } + public string? Color { get; set; } public int CatId { get; set; } - public Cat Cat { get; set; } + public Cat? Cat { get; set; } } private class Mat @@ -1686,7 +2092,8 @@ protected LikeAZooContext(DbContextOptions options) { } - public DbSet Cats { get; set; } + public DbSet Cats + => Set(); protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder @@ -1751,7 +2158,7 @@ void Seed(LikeAZooContext context) if (usePool) { using var scope = _poolProvider.CreateScope(); - Seed(scope.ServiceProvider.GetService()); + Seed(scope.ServiceProvider.GetRequiredService()); } else { @@ -1934,7 +2341,7 @@ public void Dependent_FKs_are_not_nulled_when_principal_is_detached(bool delayCa var category = new OptionalCategory { Id = 1, - Products = new List + Products = { new() { Id = 1 }, new() { Id = 2 }, @@ -1971,12 +2378,13 @@ public void Dependent_FKs_are_not_nulled_when_principal_is_detached(bool delayCa if (trackNewDependents) { - newCategory.Products = new List - { - new() { Id = 1, CategoryId = category.Id }, - new() { Id = 2, CategoryId = category.Id }, - new() { Id = 3, CategoryId = category.Id } - }; + newCategory.Products.AddRange( + new OptionalProduct[] + { + new() { Id = 1, CategoryId = category.Id }, + new() { Id = 2, CategoryId = category.Id }, + new() { Id = 3, CategoryId = category.Id } + }); } context.Update(newCategory); @@ -2108,7 +2516,7 @@ public void Optional_relationship_with_cascade_does_not_delete_orphans( Assert.Equal(3, context.ChangeTracker.Entries().Count()); Assert.Equal(EntityState.Unchanged, context.Entry(attachedContainer).State); Assert.Equal(EntityState.Unchanged, context.Entry(attachedRoom).State); - Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct).State); + Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct!).State); if (orphanTiming != null) { @@ -2148,7 +2556,7 @@ public void Optional_relationship_with_cascade_does_not_delete_orphans( Assert.Equal(3, context.ChangeTracker.Entries().Count()); Assert.Equal(EntityState.Unchanged, context.Entry(attachedContainer).State); - Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct).State); + Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct!).State); Assert.Equal(EntityState.Modified, context.Entry(attachedRoom).State); if (forceCascade) @@ -2158,14 +2566,14 @@ public void Optional_relationship_with_cascade_does_not_delete_orphans( Assert.Equal(3, context.ChangeTracker.Entries().Count()); Assert.Equal(EntityState.Unchanged, context.Entry(attachedContainer).State); - Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct).State); + Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct!).State); Assert.Equal(EntityState.Modified, context.Entry(attachedRoom).State); context.SaveChanges(); Assert.Equal(3, context.ChangeTracker.Entries().Count()); Assert.Equal(EntityState.Unchanged, context.Entry(attachedContainer).State); - Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct).State); + Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct!).State); Assert.Equal(EntityState.Unchanged, context.Entry(attachedRoom).State); } } @@ -2271,7 +2679,7 @@ public void Optional_relationship_with_cascade_can_be_forced_to_delete_orphans( Assert.Equal(3, context.ChangeTracker.Entries().Count()); Assert.Equal(EntityState.Unchanged, context.Entry(attachedContainer).State); Assert.Equal(EntityState.Unchanged, context.Entry(attachedRoom).State); - Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct).State); + Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct!).State); if (orphanTiming != null) { @@ -2314,7 +2722,7 @@ public void Optional_relationship_with_cascade_can_be_forced_to_delete_orphans( Assert.Equal(3, context.ChangeTracker.Entries().Count()); Assert.Equal(EntityState.Unchanged, context.Entry(attachedContainer).State); - Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct).State); + Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct!).State); if (orphanTiming == null || orphanTiming == CascadeTiming.Immediate) @@ -2333,7 +2741,7 @@ public void Optional_relationship_with_cascade_can_be_forced_to_delete_orphans( Assert.Equal(3, context.ChangeTracker.Entries().Count()); Assert.Equal(EntityState.Unchanged, context.Entry(attachedContainer).State); - Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct).State); + Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct!).State); if (orphanTiming == null || orphanTiming == CascadeTiming.Immediate @@ -2356,7 +2764,7 @@ public void Optional_relationship_with_cascade_can_be_forced_to_delete_orphans( Assert.Equal(3, context.ChangeTracker.Entries().Count()); Assert.Equal(EntityState.Unchanged, context.Entry(attachedContainer).State); - Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct).State); + Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct!).State); Assert.Equal(EntityState.Modified, context.Entry(attachedRoom).State); } else @@ -2364,7 +2772,7 @@ public void Optional_relationship_with_cascade_can_be_forced_to_delete_orphans( context.SaveChanges(); Assert.Equal(2, context.ChangeTracker.Entries().Count()); Assert.Equal(EntityState.Unchanged, context.Entry(attachedContainer).State); - Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct).State); + Assert.Equal(EntityState.Unchanged, context.Entry(attachedTroduct!).State); Assert.Equal(EntityState.Detached, context.Entry(attachedRoom).State); } } @@ -2373,7 +2781,7 @@ public void Optional_relationship_with_cascade_can_be_forced_to_delete_orphans( private class Kontainer { public int Id { get; set; } - public string Name { get; set; } + public string? Name { get; set; } public List Rooms { get; } = new(); } @@ -2382,15 +2790,15 @@ private class KontainerRoom public int Id { get; set; } public int Number { get; set; } public int KontainerId { get; set; } - public Kontainer Kontainer { get; set; } + public Kontainer? Kontainer { get; set; } public int? TroductId { get; set; } - public Troduct Troduct { get; set; } + public Troduct? Troduct { get; set; } } private class Troduct { public int Id { get; set; } - public string Description { get; set; } + public string? Description { get; set; } public List Rooms { get; } = new(); } @@ -2671,7 +3079,7 @@ public void Can_add_identifying_dependents_and_principal_with_reverse_post_nav_f context.ChangeTracker.DetectChanges(); } - AssertProductAndDetailsFixedUp(context, product1.Details.Tag.TagDetails, product2.Details.Tag.TagDetails); + AssertProductAndDetailsFixedUp(context, product1.Details!.Tag.TagDetails, product2.Details!.Tag.TagDetails); } private static void AssertProductAndDetailsFixedUp( @@ -2682,13 +3090,13 @@ private static void AssertProductAndDetailsFixedUp( Assert.Equal(8, context.ChangeTracker.Entries().Count()); Assert.Equal(EntityState.Added, context.Entry(tagDetails1).State); - Assert.Equal(EntityState.Added, context.Entry(tagDetails1.Tag).State); - Assert.Equal(EntityState.Added, context.Entry(tagDetails1.Tag.Details).State); + Assert.Equal(EntityState.Added, context.Entry(tagDetails1.Tag!).State); + Assert.Equal(EntityState.Added, context.Entry(tagDetails1.Tag!.Details).State); Assert.Equal(EntityState.Added, context.Entry(tagDetails1.Tag.Details.Product).State); Assert.Equal(EntityState.Added, context.Entry(tagDetails2).State); - Assert.Equal(EntityState.Added, context.Entry(tagDetails2.Tag).State); - Assert.Equal(EntityState.Added, context.Entry(tagDetails2.Tag.Details).State); + Assert.Equal(EntityState.Added, context.Entry(tagDetails2.Tag!).State); + Assert.Equal(EntityState.Added, context.Entry(tagDetails2.Tag!.Details).State); Assert.Equal(EntityState.Added, context.Entry(tagDetails2.Tag.Details.Product).State); Assert.Equal(tagDetails1.Id, tagDetails1.Tag.Id); @@ -2710,12 +3118,12 @@ private static void AssertProductAndDetailsFixedUp( Assert.Same(tagDetails2.Tag.Details, tagDetails2.Tag.Details.Product.Details); var product1 = tagDetails1.Tag.Details.Product; - Assert.Same(product1, product1.Details.Product); + Assert.Same(product1, product1.Details!.Product); Assert.Same(product1.Details, product1.Details.Tag.Details); Assert.Same(product1.Details.Tag, product1.Details.Tag.TagDetails.Tag); var product2 = tagDetails2.Tag.Details.Product; - Assert.Same(product2, product2.Details.Product); + Assert.Same(product2, product2.Details!.Product); Assert.Same(product2.Details, product2.Details.Tag.Details); Assert.Same(product2.Details.Tag, product2.Details.Tag.TagDetails.Tag); } @@ -2971,7 +3379,7 @@ public void Shadow_properties_are_not_included_in_update_unless_value_explicitly entry.Property("Id").CurrentValue = id; entry.State = EntityState.Modified; entry.Property("SomeInt").CurrentValue = 0; - entry.Property("SomeString").CurrentValue = null; + entry.Property("SomeString").CurrentValue = null; context.SaveChanges(); } @@ -2979,7 +3387,7 @@ public void Shadow_properties_are_not_included_in_update_unless_value_explicitly AssertValuesSaved(id, 0, null); } - private static void AssertValuesSaved(int id, int someInt, string someString) + private static void AssertValuesSaved(int id, int someInt, string? someString) { using var context = new TheShadows(); var entry = context.Entry(context.Set().Single(e => EF.Property(e, "Id") == id)); @@ -3014,7 +3422,7 @@ private class Category { public int Id { get; set; } - public List Products { get; set; } + public List Products { get; } = new(); } private class Product @@ -3022,20 +3430,20 @@ private class Product public int Id { get; set; } public int CategoryId { get; set; } - public Category Category { get; set; } + public Category? Category { get; set; } - public ProductDetails Details { get; set; } + public ProductDetails? Details { get; set; } // ReSharper disable once CollectionNeverUpdated.Local // ReSharper disable once MemberHidesStaticFromOuterClass - public List OrderDetails { get; set; } + public List OrderDetails { get; } = new(); } private class OptionalCategory { public int Id { get; set; } - public List Products { get; set; } + public List Products { get; } = new(); } private class OptionalProduct @@ -3043,7 +3451,7 @@ private class OptionalProduct public int Id { get; set; } public int? CategoryId { get; set; } - public OptionalCategory Category { get; set; } + public OptionalCategory? Category { get; set; } } private class SpecialProduct : Product @@ -3054,25 +3462,25 @@ private class ProductDetails { public int Id { get; set; } - public Product Product { get; set; } + public Product Product { get; set; } = null!; - public ProductDetailsTag Tag { get; set; } + public ProductDetailsTag Tag { get; set; } = null!; } private class ProductDetailsTag { public int Id { get; set; } - public ProductDetails Details { get; set; } + public ProductDetails Details { get; set; } = null!; - public ProductDetailsTagDetails TagDetails { get; set; } + public ProductDetailsTagDetails TagDetails { get; set; } = null!; } private class ProductDetailsTagDetails { public int Id { get; set; } - public ProductDetailsTag Tag { get; set; } + public ProductDetailsTag? Tag { get; set; } } private class Order @@ -3081,7 +3489,7 @@ private class Order // ReSharper disable once CollectionNeverUpdated.Local // ReSharper disable once MemberHidesStaticFromOuterClass - public List OrderDetails { get; set; } + public List OrderDetails { get; } = new(); } private class OrderDetails @@ -3089,22 +3497,22 @@ private class OrderDetails public int OrderId { get; set; } public int ProductId { get; set; } - public Order Order { get; set; } - public Product Product { get; set; } + public Order Order { get; set; } = null!; + public Product Product { get; set; } = null!; } private class Sweet { public int? Id { get; set; } - public Dreams Dreams { get; set; } + public Dreams? Dreams { get; set; } } private class Dreams { - public Sweet Sweet { get; set; } - public AreMade Are { get; set; } - public AreMade Made { get; set; } - public OfThis OfThis { get; set; } + public Sweet? Sweet { get; set; } + public AreMade? Are { get; set; } + public AreMade? Made { get; set; } + public OfThis? OfThis { get; set; } } private class AreMade @@ -3117,55 +3525,55 @@ private class OfThis : AreMade private class WhoAmI { - public string ToDisagree { get; set; } + public string? ToDisagree { get; set; } } private class PrincipalGG { public int Id { get; set; } - public DependentGG DependentGG { get; set; } + public DependentGG? DependentGG { get; set; } } private class DependentGG { public int Id { get; set; } - public PrincipalGG PrincipalGG { get; set; } + public PrincipalGG? PrincipalGG { get; set; } } private class PrincipalNN { public int Id { get; set; } - public DependentNN DependentNN { get; set; } + public DependentNN? DependentNN { get; set; } } private class DependentNN { public int Id { get; set; } - public PrincipalNN PrincipalNN { get; set; } + public PrincipalNN? PrincipalNN { get; set; } } private class PrincipalNG { public int Id { get; set; } - public DependentNG DependentNG { get; set; } + public DependentNG? DependentNG { get; set; } } private class DependentNG { public int Id { get; set; } - public PrincipalNG PrincipalNG { get; set; } + public PrincipalNG? PrincipalNG { get; set; } } private class PrincipalGN { public int Id { get; set; } - public DependentGN DependentGN { get; set; } + public DependentGN? DependentGN { get; set; } } private class DependentGN { public int Id { get; set; } - public PrincipalGN PrincipalGN { get; set; } + public PrincipalGN? PrincipalGN { get; set; } } private class EarlyLearningCenter : DbContext diff --git a/test/EFCore.Tests/ChangeTracking/Internal/InternalEntryEntrySubscriberTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/InternalEntryEntrySubscriberTest.cs index a89a4adf386..f251ef48dea 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/InternalEntryEntrySubscriberTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/InternalEntryEntrySubscriberTest.cs @@ -497,6 +497,26 @@ public void Suspend() public void Resume() { } + + public event EventHandler DetectingChanges; + + public void OnDetectingChanges(InternalEntityEntry internalEntityEntry) + => DetectingChanges?.Invoke(null, null); + + public void OnDetectingChanges(IStateManager stateManager) + => DetectingChanges?.Invoke(null, null); + + public event EventHandler DetectedChanges; + + public void OnDetectedChanges(InternalEntityEntry internalEntityEntry, bool changesFound) + => DetectedChanges?.Invoke(null, null); + + public void OnDetectedChanges(IStateManager stateManager, bool changesFound) + => DetectedChanges?.Invoke(null, null); + + public void ResetState() + { + } } private class TestNavigationListener : INavigationFixer diff --git a/test/EFCore.Tests/DbContextTest.cs b/test/EFCore.Tests/DbContextTest.cs index 409451dda70..6bea73d69fe 100644 --- a/test/EFCore.Tests/DbContextTest.cs +++ b/test/EFCore.Tests/DbContextTest.cs @@ -201,6 +201,26 @@ public virtual void Suspend() public virtual void Resume() { } + + public event EventHandler DetectingChanges; + + public void OnDetectingChanges(InternalEntityEntry internalEntityEntry) + => DetectingChanges?.Invoke(null, null); + + public void OnDetectingChanges(IStateManager stateManager) + => DetectingChanges?.Invoke(null, null); + + public event EventHandler DetectedChanges; + + public void OnDetectedChanges(InternalEntityEntry internalEntityEntry, bool changesFound) + => DetectedChanges?.Invoke(null, null); + + public void OnDetectedChanges(IStateManager stateManager, bool changesFound) + => DetectedChanges?.Invoke(null, null); + + public void ResetState() + { + } } [ConditionalTheory] diff --git a/test/EFCore.Tests/TestUtilities/FakeStateManager.cs b/test/EFCore.Tests/TestUtilities/FakeStateManager.cs index 807580ccb4a..fb4dd6d54f6 100644 --- a/test/EFCore.Tests/TestUtilities/FakeStateManager.cs +++ b/test/EFCore.Tests/TestUtilities/FakeStateManager.cs @@ -131,7 +131,7 @@ public InternalEntityEntry TryGetEntry(object entity, IEntityType type, bool thr public IInternalEntityEntryNotifier InternalEntityEntryNotifier => throw new NotImplementedException(); - public void StateChanging(InternalEntityEntry entry, EntityState newState) + public void ChangingState(InternalEntityEntry entry, EntityState newState) => throw new NotImplementedException(); public IValueGenerationManager ValueGenerationManager @@ -195,11 +195,21 @@ public DbContext Context public IModel Model => throw new NotImplementedException(); + public event EventHandler Tracking; + + public void OnTracking(InternalEntityEntry internalEntityEntry, EntityState state, bool fromQuery) + => Tracking?.Invoke(null, null); + public event EventHandler Tracked; public void OnTracked(InternalEntityEntry internalEntityEntry, bool fromQuery) => Tracked?.Invoke(null, null); + public event EventHandler StateChanging; + + public void OnStateChanging(InternalEntityEntry internalEntityEntry, EntityState newState) + => StateChanging?.Invoke(null, null); + public event EventHandler StateChanged; public void OnStateChanged(InternalEntityEntry internalEntityEntry, EntityState oldState)