diff --git a/src/EFCore/ChangeTracking/ChangeTracker.cs b/src/EFCore/ChangeTracking/ChangeTracker.cs index cce589d5098..9fb6d1490d7 100644 --- a/src/EFCore/ChangeTracking/ChangeTracker.cs +++ b/src/EFCore/ChangeTracking/ChangeTracker.cs @@ -381,25 +381,63 @@ public event EventHandler StateChanged } /// - /// An event fired when detecting changes to the entity graph or a single entity is about to happen, either through an + /// An event fired when detecting changes to 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 + /// + /// is set to for the duration of the event to prevent an infinite + /// loop of recursive automatic calls. + /// + public event EventHandler DetectingEntityChanges { - add => ChangeDetector.DetectingChanges += value; - remove => ChangeDetector.DetectingChanges -= value; + add => ChangeDetector.DetectingEntityChanges += value; + remove => ChangeDetector.DetectingEntityChanges -= value; } /// - /// An event fired when any changes have been detected to the entity graph or a single entity, either through an + /// An event fired when any changes have been detected to a single entity, either through an /// explicit call to or , or automatically, such as part of /// executing or . /// - public event EventHandler DetectedChanges + /// + /// is set to for the duration of the event to prevent an infinite + /// loop of recursive automatic calls. + /// + public event EventHandler DetectedEntityChanges + { + add => ChangeDetector.DetectedEntityChanges += value; + remove => ChangeDetector.DetectedEntityChanges -= value; + } + + /// + /// An event fired when detecting changes to the entity graph about to happen, either through an + /// explicit call to , or automatically, such as part of + /// executing or . + /// + /// + /// is set to for the duration of the event to prevent an infinite + /// loop of recursive automatic calls. + /// + public event EventHandler DetectingAllChanges + { + add => ChangeDetector.DetectingAllChanges += value; + remove => ChangeDetector.DetectingAllChanges -= value; + } + + /// + /// An event fired when any changes have been detected to the entity graph, either through an + /// explicit call to , or automatically, such as part of + /// executing or . + /// + /// + /// is set to for the duration of the event to prevent an infinite + /// loop of recursive automatic calls. + /// + public event EventHandler DetectedAllChanges { - add => ChangeDetector.DetectedChanges += value; - remove => ChangeDetector.DetectedChanges -= value; + add => ChangeDetector.DetectedAllChanges += value; + remove => ChangeDetector.DetectedAllChanges -= value; } /// diff --git a/src/EFCore/ChangeTracking/DetectChangesEventArgs.cs b/src/EFCore/ChangeTracking/DetectChangesEventArgs.cs index 3abc912b858..c6c7acd562c 100644 --- a/src/EFCore/ChangeTracking/DetectChangesEventArgs.cs +++ b/src/EFCore/ChangeTracking/DetectChangesEventArgs.cs @@ -1,39 +1,14 @@ // 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. +/// 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/DetectEntityChangesEventArgs.cs b/src/EFCore/ChangeTracking/DetectEntityChangesEventArgs.cs new file mode 100644 index 00000000000..c84f1ab7992 --- /dev/null +++ b/src/EFCore/ChangeTracking/DetectEntityChangesEventArgs.cs @@ -0,0 +1,36 @@ +// 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 DetectEntityChangesEventArgs : DetectChangesEventArgs +{ + 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 DetectEntityChangesEventArgs(InternalEntityEntry internalEntityEntry) + { + _internalEntityEntry = internalEntityEntry; + } + + /// + /// The for the entity. + /// + public virtual EntityEntry Entry + => _entry ??= new EntityEntry(_internalEntityEntry); +} diff --git a/src/EFCore/ChangeTracking/DetectedChangesEventArgs.cs b/src/EFCore/ChangeTracking/DetectedChangesEventArgs.cs index c6576ec4efc..81019ddec20 100644 --- a/src/EFCore/ChangeTracking/DetectedChangesEventArgs.cs +++ b/src/EFCore/ChangeTracking/DetectedChangesEventArgs.cs @@ -1,17 +1,15 @@ // 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. +/// Event arguments for the event. /// /// /// See State changes of entities in EF Core for more information and examples. /// -public class DetectedChangesEventArgs : DetectChangesEventArgs +public class DetectedChangesEventArgs : EventArgs { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -20,10 +18,7 @@ public class DetectedChangesEventArgs : DetectChangesEventArgs /// 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) + public DetectedChangesEventArgs(bool changesFound) { ChangesFound = changesFound; } diff --git a/src/EFCore/ChangeTracking/DetectedEntityChangesEventArgs.cs b/src/EFCore/ChangeTracking/DetectedEntityChangesEventArgs.cs new file mode 100644 index 00000000000..870f94a860c --- /dev/null +++ b/src/EFCore/ChangeTracking/DetectedEntityChangesEventArgs.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 DetectedEntityChangesEventArgs : DetectedChangesEventArgs +{ + 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 DetectedEntityChangesEventArgs( + InternalEntityEntry internalEntityEntry, + bool changesFound) + : base(changesFound) + { + _internalEntityEntry = internalEntityEntry; + } + + /// + /// The for the entity. + /// + public virtual EntityEntry Entry + => _entry ??= new EntityEntry(_internalEntityEntry); +} diff --git a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs index d4ea7d63b30..ae9052e38e7 100644 --- a/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs +++ b/src/EFCore/ChangeTracking/Internal/ChangeDetector.cs @@ -112,7 +112,7 @@ public virtual void PropertyChanging(InternalEntityEntry entry, IPropertyBase pr /// public virtual void DetectChanges(IStateManager stateManager) { - OnDetectingChanges(stateManager); + OnDetectingAllChanges(stateManager); var changesFound = false; _logger.DetectChangesStarting(stateManager.Context); @@ -140,7 +140,7 @@ public virtual void DetectChanges(IStateManager stateManager) _logger.DetectChangesCompleted(stateManager.Context); - OnDetectedChanges(stateManager, changesFound); + OnDetectedAllChanges(stateManager, changesFound); } /// @@ -150,10 +150,7 @@ 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) - { - OnDetectingChanges(entry); - OnDetectedChanges(entry, DetectChanges(entry, new HashSet { entry })); - } + => DetectChanges(entry, new HashSet { entry }); private bool DetectChanges(InternalEntityEntry entry, HashSet visited) { @@ -182,7 +179,7 @@ private bool DetectChanges(InternalEntityEntry entry, HashSet= 0 @@ -233,6 +232,8 @@ private bool LocalDetectChanges(InternalEntityEntry entry) } } + OnDetectedEntityChanges(entry, changesFound); + return changesFound; } @@ -414,9 +415,11 @@ public bool DetectNavigationChange(InternalEntityEntry entry, INavigationBase na /// 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 (EventHandler? DetectingChanges, - EventHandler? DetectedChanges) CaptureEvents() - => (DetectingChanges, DetectedChanges); + public virtual (EventHandler? DetectingAllChanges, + EventHandler? DetectedAllChanges, + EventHandler? DetectingEntityChanges, + EventHandler? DetectedEntityChanges) CaptureEvents() + => (DetectingAllChanges, DetectedAllChanges, DetectingEntityChanges, DetectedEntityChanges); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -425,11 +428,15 @@ public virtual (EventHandler? DetectingChanges, /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual void SetEvents( - EventHandler? detectingChanges, - EventHandler? detectedChanges) + EventHandler? detectingAllChanges, + EventHandler? detectedAllChanges, + EventHandler? detectingEntityChanges, + EventHandler? detectedEntityChanges) { - DetectingChanges = detectingChanges; - DetectedChanges = detectedChanges; + DetectingAllChanges = detectingAllChanges; + DetectedAllChanges = detectedAllChanges; + DetectingEntityChanges = detectingEntityChanges; + DetectedEntityChanges = detectedEntityChanges; } /// @@ -438,7 +445,7 @@ public virtual void SetEvents( /// 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; + public event EventHandler? DetectingEntityChanges; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -446,13 +453,25 @@ public virtual void SetEvents( /// 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) + public virtual void OnDetectingEntityChanges(InternalEntityEntry internalEntityEntry) { - var @event = DetectingChanges; + var @event = DetectingEntityChanges; + + if (@event != null) + { + var changeTracker = internalEntityEntry.StateManager.Context.ChangeTracker; + var detectChangesEnabled = changeTracker.AutoDetectChangesEnabled; + try + { + changeTracker.AutoDetectChangesEnabled = false; + @event(changeTracker, new DetectEntityChangesEventArgs(internalEntityEntry)); + } + finally + { + changeTracker.AutoDetectChangesEnabled = detectChangesEnabled; + } + } - @event?.Invoke( - internalEntityEntry.StateManager.Context.ChangeTracker, - new DetectChangesEventArgs(internalEntityEntry)); } /// @@ -461,13 +480,32 @@ public virtual void OnDetectingChanges(InternalEntityEntry internalEntityEntry) /// 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) + public event EventHandler? DetectingAllChanges; + + /// + /// 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 OnDetectingAllChanges(IStateManager stateManager) { - var @event = DetectingChanges; + var @event = DetectingAllChanges; - @event?.Invoke( - stateManager.Context.ChangeTracker, - new DetectChangesEventArgs(null)); + if (@event != null) + { + var changeTracker = stateManager.Context.ChangeTracker; + var detectChangesEnabled = changeTracker.AutoDetectChangesEnabled; + try + { + changeTracker.AutoDetectChangesEnabled = false; + @event(changeTracker, new DetectChangesEventArgs()); + } + finally + { + changeTracker.AutoDetectChangesEnabled = detectChangesEnabled; + } + } } /// @@ -476,7 +514,7 @@ public virtual void OnDetectingChanges(IStateManager stateManager) /// 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; + public event EventHandler? DetectedEntityChanges; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -484,13 +522,24 @@ public virtual void OnDetectingChanges(IStateManager stateManager) /// 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) + public virtual void OnDetectedEntityChanges(InternalEntityEntry internalEntityEntry, bool changesFound) { - var @event = DetectedChanges; + var @event = DetectedEntityChanges; - @event?.Invoke( - internalEntityEntry.StateManager.Context.ChangeTracker, - new DetectedChangesEventArgs(internalEntityEntry, changesFound)); + if (@event != null) + { + var changeTracker = internalEntityEntry.StateManager.Context.ChangeTracker; + var detectChangesEnabled = changeTracker.AutoDetectChangesEnabled; + try + { + changeTracker.AutoDetectChangesEnabled = false; + @event(changeTracker, new DetectedEntityChangesEventArgs(internalEntityEntry, changesFound)); + } + finally + { + changeTracker.AutoDetectChangesEnabled = detectChangesEnabled; + } + } } /// @@ -499,13 +548,32 @@ public virtual void OnDetectedChanges(InternalEntityEntry internalEntityEntry, b /// 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) + public event EventHandler? DetectedAllChanges; + + /// + /// 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 OnDetectedAllChanges(IStateManager stateManager, bool changesFound) { - var @event = DetectedChanges; + var @event = DetectedAllChanges; - @event?.Invoke( - stateManager.Context.ChangeTracker, - new DetectedChangesEventArgs(null, changesFound)); + if (@event != null) + { + var changeTracker = stateManager.Context.ChangeTracker; + var detectChangesEnabled = changeTracker.AutoDetectChangesEnabled; + try + { + changeTracker.AutoDetectChangesEnabled = false; + @event(changeTracker, new DetectedChangesEventArgs(changesFound)); + } + finally + { + changeTracker.AutoDetectChangesEnabled = detectChangesEnabled; + } + } } /// @@ -516,7 +584,9 @@ public virtual void OnDetectedChanges(IStateManager stateManager, bool changesFo /// public virtual void ResetState() { - DetectingChanges = null; - DetectedChanges = null; + DetectingEntityChanges = null; + DetectedEntityChanges = null; + DetectingAllChanges = null; + DetectedAllChanges = null; } } diff --git a/src/EFCore/ChangeTracking/Internal/IChangeDetector.cs b/src/EFCore/ChangeTracking/Internal/IChangeDetector.cs index a2df0859cca..3807bc89c54 100644 --- a/src/EFCore/ChangeTracking/Internal/IChangeDetector.cs +++ b/src/EFCore/ChangeTracking/Internal/IChangeDetector.cs @@ -55,8 +55,10 @@ public interface IChangeDetector /// 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. /// - (EventHandler? DetectingChanges, - EventHandler? DetectedChanges) CaptureEvents(); + (EventHandler? DetectingAllChanges, + EventHandler? DetectedAllChanges, + EventHandler? DetectingEntityChanges, + EventHandler? DetectedEntityChanges) CaptureEvents(); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -65,8 +67,10 @@ public interface IChangeDetector /// doing so can result in application failures when updating to a new Entity Framework Core release. /// void SetEvents( - EventHandler? detectingChanges, - EventHandler? detectedChanges); + EventHandler? detectingAllChanges, + EventHandler? detectedAllChanges, + EventHandler? detectingEntityChanges, + EventHandler? detectedEntityChanges); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -74,7 +78,7 @@ void SetEvents( /// 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; + public event EventHandler? DetectingEntityChanges; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -82,7 +86,7 @@ void SetEvents( /// 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); + void OnDetectingEntityChanges(InternalEntityEntry internalEntityEntry); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -90,7 +94,7 @@ void SetEvents( /// 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); + public event EventHandler? DetectingAllChanges; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -98,7 +102,7 @@ void SetEvents( /// 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; + void OnDetectingAllChanges(IStateManager stateManager); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -106,7 +110,7 @@ void SetEvents( /// 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); + event EventHandler? DetectedEntityChanges; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -114,7 +118,23 @@ void SetEvents( /// 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); + void OnDetectedEntityChanges(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. + /// + event EventHandler? DetectedAllChanges; + + /// + /// 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 OnDetectedAllChanges(IStateManager stateManager, bool changesFound); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore/DbContext.cs b/src/EFCore/DbContext.cs index 77ef0637e25..fc248ee421d 100644 --- a/src/EFCore/DbContext.cs +++ b/src/EFCore/DbContext.cs @@ -887,8 +887,10 @@ private void SetLeaseInternal(DbContextLease lease) || _configurationSnapshot.HasChangeDetectorConfiguration) { DbContextDependencies.ChangeDetector.SetEvents( - _configurationSnapshot.DetectingChanges, - _configurationSnapshot.DetectedChanges); + _configurationSnapshot.DetectingAllChanges, + _configurationSnapshot.DetectedAllChanges, + _configurationSnapshot.DetectingEntityChanges, + _configurationSnapshot.DetectedEntityChanges); } SavingChanges = _configurationSnapshot.SavingChanges; @@ -927,8 +929,10 @@ void IDbContextPoolable.SnapshotConfiguration() stateManagerEvents?.Tracked, stateManagerEvents?.StateChanging, stateManagerEvents?.StateChanged, - changeDetectorEvents?.DetectingChanges, - changeDetectorEvents?.DetectedChanges); + changeDetectorEvents?.DetectingAllChanges, + changeDetectorEvents?.DetectedAllChanges, + changeDetectorEvents?.DetectingEntityChanges, + changeDetectorEvents?.DetectedEntityChanges); } /// diff --git a/src/EFCore/Internal/DbContextPoolConfigurationSnapshot.cs b/src/EFCore/Internal/DbContextPoolConfigurationSnapshot.cs index e7b6d43f84f..47a7acf1847 100644 --- a/src/EFCore/Internal/DbContextPoolConfigurationSnapshot.cs +++ b/src/EFCore/Internal/DbContextPoolConfigurationSnapshot.cs @@ -36,8 +36,10 @@ public DbContextPoolConfigurationSnapshot( EventHandler? tracked, EventHandler? stateChanging, EventHandler? stateChanged, - EventHandler? detectingChanges, - EventHandler? detectedChanges) + EventHandler? detectingAllChanges, + EventHandler? detectedAllChanges, + EventHandler? detectingEntityChanges, + EventHandler? detectedEntityChanges) { HasDatabaseConfiguration = hasDatabaseConfiguration; HasStateManagerConfiguration = hasStateManagerConfiguration; @@ -57,8 +59,10 @@ public DbContextPoolConfigurationSnapshot( Tracked = tracked; StateChanging = stateChanging; StateChanged = stateChanged; - DetectingChanges = detectingChanges; - DetectedChanges = detectedChanges; + DetectingAllChanges = detectingAllChanges; + DetectedAllChanges = detectedAllChanges; + DetectingEntityChanges = detectingEntityChanges; + DetectedEntityChanges = detectedEntityChanges; } /// @@ -211,7 +215,7 @@ public DbContextPoolConfigurationSnapshot( /// 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 EventHandler? DetectingChanges { get; } + public EventHandler? DetectingAllChanges { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -219,5 +223,21 @@ public DbContextPoolConfigurationSnapshot( /// 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 EventHandler? DetectedChanges { get; } + public EventHandler? DetectedAllChanges { get; } + + /// + /// 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 EventHandler? DetectingEntityChanges { get; } + + /// + /// 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 EventHandler? DetectedEntityChanges { get; } } diff --git a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs index e79d2f411fe..adc61fa1e57 100644 --- a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs @@ -118,8 +118,10 @@ public PooledContext(DbContextOptions options) ChangeTracker.Tracked += (sender, args) => { }; ChangeTracker.StateChanging += (sender, args) => { }; ChangeTracker.StateChanged += (sender, args) => { }; - ChangeTracker.DetectingChanges += (sender, args) => { }; - ChangeTracker.DetectedChanges += (sender, args) => { }; + ChangeTracker.DetectingAllChanges += (sender, args) => { }; + ChangeTracker.DetectedAllChanges += (sender, args) => { }; + ChangeTracker.DetectingEntityChanges += (sender, args) => { }; + ChangeTracker.DetectedEntityChanges += (sender, args) => { }; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) @@ -731,8 +733,10 @@ public async Task Context_configuration_is_reset(bool useInterface, bool async) context1.ChangeTracker.Tracked += ChangeTracker_OnTracked; context1.ChangeTracker.StateChanging += ChangeTracker_OnStateChanging; context1.ChangeTracker.StateChanged += ChangeTracker_OnStateChanged; - context1.ChangeTracker.DetectingChanges += ChangeTracker_OnDetectingChanges; - context1.ChangeTracker.DetectedChanges += ChangeTracker_OnDetectedChanges; + context1.ChangeTracker.DetectingAllChanges += ChangeTracker_OnDetectingAllChanges; + context1.ChangeTracker.DetectedAllChanges += ChangeTracker_OnDetectedAllChanges; + context1.ChangeTracker.DetectingEntityChanges += ChangeTracker_OnDetectingEntityChanges; + context1.ChangeTracker.DetectedEntityChanges += ChangeTracker_OnDetectedEntityChanges; context1.SavingChanges += Context_OnSavingChanges; context1.SavedChanges += Context_OnSavedChanges; context1.SaveChangesFailed += Context_OnSaveChangesFailed; @@ -768,8 +772,10 @@ public async Task Context_configuration_is_reset(bool useInterface, bool async) context2.SaveChanges(); - Assert.False(_changeTracker_OnDetectingChanges); - Assert.False(_changeTracker_OnDetectedChanges); + Assert.False(_changeTracker_OnDetectingAllChanges); + Assert.False(_changeTracker_OnDetectedAllChanges); + Assert.False(_changeTracker_OnDetectingEntityChanges); + Assert.False(_changeTracker_OnDetectedEntityChanges); Assert.False(_context_OnSavedChanges); Assert.False(_context_OnSavingChanges); Assert.False(_context_OnSaveChangesFailed); @@ -824,8 +830,10 @@ public async Task Context_configuration_is_reset_with_factory(bool async, bool w context1.ChangeTracker.Tracked += ChangeTracker_OnTracked; context1.ChangeTracker.StateChanging += ChangeTracker_OnStateChanging; context1.ChangeTracker.StateChanged += ChangeTracker_OnStateChanged; - context1.ChangeTracker.DetectingChanges += ChangeTracker_OnDetectingChanges; - context1.ChangeTracker.DetectedChanges += ChangeTracker_OnDetectedChanges; + context1.ChangeTracker.DetectingAllChanges += ChangeTracker_OnDetectingAllChanges; + context1.ChangeTracker.DetectedAllChanges += ChangeTracker_OnDetectedAllChanges; + context1.ChangeTracker.DetectingEntityChanges += ChangeTracker_OnDetectingEntityChanges; + context1.ChangeTracker.DetectedEntityChanges += ChangeTracker_OnDetectedEntityChanges; context1.SavingChanges += Context_OnSavingChanges; context1.SavedChanges += Context_OnSavedChanges; context1.SaveChangesFailed += Context_OnSaveChangesFailed; @@ -847,8 +855,10 @@ public async Task Context_configuration_is_reset_with_factory(bool async, bool w context2.SaveChanges(); - Assert.False(_changeTracker_OnDetectingChanges); - Assert.False(_changeTracker_OnDetectedChanges); + Assert.False(_changeTracker_OnDetectingAllChanges); + Assert.False(_changeTracker_OnDetectedAllChanges); + Assert.False(_changeTracker_OnDetectingEntityChanges); + Assert.False(_changeTracker_OnDetectedEntityChanges); Assert.False(_context_OnSavedChanges); Assert.False(_context_OnSavingChanges); Assert.False(_context_OnSaveChangesFailed); @@ -872,8 +882,10 @@ public void Change_tracker_can_be_cleared_without_resetting_context_config() 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.ChangeTracker.DetectingAllChanges += ChangeTracker_OnDetectingAllChanges; + context.ChangeTracker.DetectedAllChanges += ChangeTracker_OnDetectedAllChanges; + context.ChangeTracker.DetectingEntityChanges += ChangeTracker_OnDetectingEntityChanges; + context.ChangeTracker.DetectedEntityChanges += ChangeTracker_OnDetectedEntityChanges; context.SavingChanges += Context_OnSavingChanges; context.SavedChanges += Context_OnSavedChanges; context.SaveChangesFailed += Context_OnSaveChangesFailed; @@ -892,8 +904,10 @@ public void Change_tracker_can_be_cleared_without_resetting_context_config() Assert.False(_changeTracker_OnTracked); Assert.False(_changeTracker_OnStateChanging); Assert.False(_changeTracker_OnStateChanged); - Assert.False(_changeTracker_OnDetectingChanges); - Assert.False(_changeTracker_OnDetectedChanges); + Assert.False(_changeTracker_OnDetectingAllChanges); + Assert.False(_changeTracker_OnDetectedAllChanges); + Assert.False(_changeTracker_OnDetectingEntityChanges); + Assert.False(_changeTracker_OnDetectedEntityChanges); var customer = new Customer { CustomerId = "C" }; context.Customers.Attach(customer).State = EntityState.Modified; @@ -903,13 +917,17 @@ public void Change_tracker_can_be_cleared_without_resetting_context_config() Assert.True(_changeTracker_OnTracked); Assert.True(_changeTracker_OnStateChanging); Assert.True(_changeTracker_OnStateChanged); - Assert.False(_changeTracker_OnDetectingChanges); - Assert.False(_changeTracker_OnDetectedChanges); + Assert.False(_changeTracker_OnDetectingAllChanges); + Assert.False(_changeTracker_OnDetectedAllChanges); + Assert.False(_changeTracker_OnDetectingEntityChanges); + Assert.False(_changeTracker_OnDetectedEntityChanges); context.SaveChanges(); - Assert.True(_changeTracker_OnDetectingChanges); - Assert.True(_changeTracker_OnDetectedChanges); + Assert.True(_changeTracker_OnDetectingAllChanges); + Assert.True(_changeTracker_OnDetectedAllChanges); + Assert.True(_changeTracker_OnDetectingEntityChanges); + Assert.True(_changeTracker_OnDetectedEntityChanges); Assert.True(_context_OnSavedChanges); Assert.True(_context_OnSavingChanges); Assert.False(_context_OnSaveChangesFailed); @@ -950,15 +968,25 @@ private void ChangeTracker_OnStateChanging(object sender, EntityStateChangingEve private void ChangeTracker_OnStateChanged(object sender, EntityStateChangedEventArgs e) => _changeTracker_OnStateChanged = true; - private bool _changeTracker_OnDetectingChanges; + private bool _changeTracker_OnDetectingAllChanges; - private void ChangeTracker_OnDetectingChanges(object sender, DetectChangesEventArgs e) - => _changeTracker_OnDetectingChanges = true; + private void ChangeTracker_OnDetectingAllChanges(object sender, DetectChangesEventArgs e) + => _changeTracker_OnDetectingAllChanges = true; - private bool _changeTracker_OnDetectedChanges; + private bool _changeTracker_OnDetectedAllChanges; - private void ChangeTracker_OnDetectedChanges(object sender, DetectedChangesEventArgs e) - => _changeTracker_OnDetectedChanges = true; + private void ChangeTracker_OnDetectedAllChanges(object sender, DetectedChangesEventArgs e) + => _changeTracker_OnDetectedAllChanges = true; + + private bool _changeTracker_OnDetectingEntityChanges; + + private void ChangeTracker_OnDetectingEntityChanges(object sender, DetectEntityChangesEventArgs e) + => _changeTracker_OnDetectingEntityChanges = true; + + private bool _changeTracker_OnDetectedEntityChanges; + + private void ChangeTracker_OnDetectedEntityChanges(object sender, DetectedEntityChangesEventArgs e) + => _changeTracker_OnDetectedEntityChanges = true; [ConditionalTheory] [InlineData(false)] diff --git a/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs b/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs index 578da0b0d78..e553a3bd821 100644 --- a/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs +++ b/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs @@ -1573,322 +1573,438 @@ public void State_change_events_are_limited_to_the_current_context() [ConditionalFact] public void DetectChanges_events_fire_for_no_change() { - var detecting = new List(); - var detected = new List(); + var detectingAll = new List(); + var detectedAll = new List(); + var detectingEntity = new List(); + var detectedEntity = new List(); using var scope = _poolProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); - RegisterEvents(context, detecting, detected); + RegisterDetectAllEvents(context, detectingAll, detectedAll); + RegisterDetectEntityEvents(context, detectingEntity, detectedEntity); context.AttachRange(new Cat(1), new Cat(2)); - Assert.Empty(detecting); - Assert.Empty(detected); + Assert.Empty(detectingAll); + Assert.Empty(detectedAll); Assert.False(context.ChangeTracker.HasChanges()); - Assert.Single(detecting); - Assert.Single(detected); + Assert.Single(detectingAll); + Assert.Single(detectedAll); + Assert.Equal(2, detectingEntity.Count); + Assert.Equal(2, detectedEntity.Count); - AssertDetectChangesEvent(context, changesFound: false, detecting[0], detected[0]); + Assert.False(detectedAll[0].ChangesFound); + Assert.False(detectedEntity[0].ChangesFound); + Assert.False(detectedEntity[1].ChangesFound); } [ConditionalFact] public void DetectChanges_events_fire_for_property_change() { - var detecting = new List(); - var detected = new List(); + var detectingAll = new List(); + var detectedAll = new List(); + var detectingEntity = new List(); + var detectedEntity = new List(); using var scope = _poolProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); - RegisterEvents(context, detecting, detected); + RegisterDetectAllEvents(context, detectingAll, detectedAll); + RegisterDetectEntityEvents(context, detectingEntity, detectedEntity); var cat = new Cat(1); context.AttachRange(cat, new Cat(2)); cat.Name = "Alice"; - Assert.Empty(detecting); - Assert.Empty(detected); + Assert.Empty(detectingAll); + Assert.Empty(detectedAll); Assert.True(context.ChangeTracker.HasChanges()); - Assert.Single(detecting); - Assert.Single(detected); + Assert.Single(detectingAll); + Assert.Single(detectedAll); + Assert.Equal(2, detectingEntity.Count); + Assert.Equal(2, detectedEntity.Count); - AssertDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + Assert.True(detectedAll[0].ChangesFound); + Assert.True(detectedEntity[0].ChangesFound); + Assert.False(detectedEntity[1].ChangesFound); } [ConditionalFact] public void DetectChanges_events_fire_for_fk_change() { - var detecting = new List(); - var detected = new List(); + var detectingAll = new List(); + var detectedAll = new List(); + var detectingEntity = new List(); + var detectedEntity = new List(); using var scope = _poolProvider.CreateScope(); using var context = new EarlyLearningCenter(); - RegisterEvents(context, detecting, detected); + RegisterDetectAllEvents(context, detectingAll, detectedAll); + RegisterDetectEntityEvents(context, detectingEntity, detectedEntity); var product = new Product { Category = new Category()}; context.Attach(product); product.CategoryId = 2; - Assert.Empty(detecting); - Assert.Empty(detected); + Assert.Empty(detectingAll); + Assert.Empty(detectedAll); Assert.True(context.ChangeTracker.HasChanges()); - Assert.Single(detecting); - Assert.Single(detected); + Assert.Single(detectingAll); + Assert.Single(detectedAll); + Assert.Equal(2, detectingEntity.Count); + Assert.Equal(2, detectedEntity.Count); - AssertDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + Assert.True(detectedAll[0].ChangesFound); + Assert.True(detectedEntity[0].ChangesFound); + Assert.False(detectedEntity[1].ChangesFound); } [ConditionalFact] public void DetectChanges_events_fire_for_reference_navigation_change() { - var detecting = new List(); - var detected = new List(); + var detectingAll = new List(); + var detectedAll = new List(); + var detectingEntity = new List(); + var detectedEntity = new List(); using var scope = _poolProvider.CreateScope(); using var context = new EarlyLearningCenter(); - RegisterEvents(context, detecting, detected); + RegisterDetectAllEvents(context, detectingAll, detectedAll); + RegisterDetectEntityEvents(context, detectingEntity, detectedEntity); var product = new Product { Category = new Category()}; context.Attach(product); product.Category = null; - Assert.Empty(detecting); - Assert.Empty(detected); + Assert.Empty(detectingAll); + Assert.Empty(detectedAll); Assert.True(context.ChangeTracker.HasChanges()); - Assert.Single(detecting); - Assert.Single(detected); + Assert.Single(detectingAll); + Assert.Single(detectedAll); + Assert.Equal(2, detectingEntity.Count); + Assert.Equal(2, detectedEntity.Count); - AssertDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + Assert.True(detectedAll[0].ChangesFound); + Assert.True(detectedEntity[0].ChangesFound); + Assert.False(detectedEntity[1].ChangesFound); } [ConditionalFact] public void DetectChanges_events_fire_for_collection_navigation_change() { - var detecting = new List(); - var detected = new List(); + var detectingAll = new List(); + var detectedAll = new List(); + var detectingEntity = new List(); + var detectedEntity = new List(); using var scope = _poolProvider.CreateScope(); using var context = new EarlyLearningCenter(); - RegisterEvents(context, detecting, detected); + RegisterDetectAllEvents(context, detectingAll, detectedAll); + RegisterDetectEntityEvents(context, detectingEntity, detectedEntity); var product = new Product { Category = new Category()}; context.Attach(product); product.Category.Products.Clear(); - Assert.Empty(detecting); - Assert.Empty(detected); + Assert.Empty(detectingAll); + Assert.Empty(detectedAll); Assert.True(context.ChangeTracker.HasChanges()); - Assert.Single(detecting); - Assert.Single(detected); + Assert.Single(detectingAll); + Assert.Single(detectedAll); + Assert.Equal(2, detectingEntity.Count); + Assert.Equal(2, detectedEntity.Count); - AssertDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + Assert.True(detectedAll[0].ChangesFound); + Assert.False(detectedEntity[0].ChangesFound); + Assert.True(detectedEntity[1].ChangesFound); } [ConditionalFact] public void DetectChanges_events_fire_for_skip_navigation_change() { - var detecting = new List(); - var detected = new List(); + var detectingAll = new List(); + var detectedAll = new List(); + var detectingEntity = new List(); + var detectedEntity = new List(); using var scope = _poolProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); - RegisterEvents(context, detecting, detected); + RegisterDetectAllEvents(context, detectingAll, detectedAll); + RegisterDetectEntityEvents(context, detectingEntity, detectedEntity); var cat = new Cat(1) { Hats = { new Hat(2) }}; context.Attach(cat); cat.Hats.Clear(); - Assert.Empty(detecting); - Assert.Empty(detected); + Assert.Empty(detectingAll); + Assert.Empty(detectedAll); Assert.True(context.ChangeTracker.HasChanges()); - Assert.Single(detecting); - Assert.Single(detected); + Assert.Single(detectingAll); + Assert.Single(detectedAll); + Assert.Equal(2, detectingEntity.Count); + Assert.Equal(2, detectedEntity.Count); - AssertDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + Assert.True(detectedAll[0].ChangesFound); + Assert.True(detectedEntity[0].ChangesFound); + Assert.False(detectedEntity[1].ChangesFound); } [ConditionalFact] public void Local_DetectChanges_events_fire_for_no_change() { - var detecting = new List(); - var detected = new List(); + var detectingAll = new List(); + var detectedAll = new List(); + var detectingEntity = new List(); + var detectedEntity = new List(); using var scope = _poolProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); - RegisterEvents(context, detecting, detected); + RegisterDetectAllEvents(context, detectingAll, detectedAll); + RegisterDetectEntityEvents(context, detectingEntity, detectedEntity); var cat = new Cat(1); context.AttachRange(cat, new Cat(2)); - Assert.Empty(detecting); - Assert.Empty(detected); + Assert.Empty(detectingEntity); + Assert.Empty(detectedEntity); _ = context.Entry(cat); - Assert.Single(detecting); - Assert.Single(detected); + Assert.Empty(detectingAll); + Assert.Empty(detectedAll); + Assert.Single(detectingEntity); + Assert.Single(detectedEntity); - AssertLocalDetectChangesEvent(context, changesFound: false, detecting[0], detected[0]); + AssertLocalDetectChangesEvent(changesFound: false, detectingEntity[0], detectedEntity[0]); } [ConditionalFact] public void Local_DetectChanges_events_fire_for_property_change() { - var detecting = new List(); - var detected = new List(); + var detectingAll = new List(); + var detectedAll = new List(); + var detectingEntity = new List(); + var detectedEntity = new List(); using var scope = _poolProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); - RegisterEvents(context, detecting, detected); + RegisterDetectAllEvents(context, detectingAll, detectedAll); + RegisterDetectEntityEvents(context, detectingEntity, detectedEntity); var cat = new Cat(1); context.AttachRange(cat, new Cat(2)); cat.Name = "Alice"; - Assert.Empty(detecting); - Assert.Empty(detected); + Assert.Empty(detectingEntity); + Assert.Empty(detectedEntity); _ = context.Entry(cat); - Assert.Single(detecting); - Assert.Single(detected); + Assert.Empty(detectingAll); + Assert.Empty(detectedAll); + Assert.Single(detectingEntity); + Assert.Single(detectedEntity); - AssertLocalDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + AssertLocalDetectChangesEvent(changesFound: true, detectingEntity[0], detectedEntity[0]); } [ConditionalFact] public void Local_DetectChanges_events_fire_for_fk_change() { - var detecting = new List(); - var detected = new List(); + var detectingAll = new List(); + var detectedAll = new List(); + var detectingEntity = new List(); + var detectedEntity = new List(); using var scope = _poolProvider.CreateScope(); using var context = new EarlyLearningCenter(); - RegisterEvents(context, detecting, detected); + RegisterDetectAllEvents(context, detectingAll, detectedAll); + RegisterDetectEntityEvents(context, detectingEntity, detectedEntity); var product = new Product { Category = new Category()}; context.Attach(product); product.CategoryId = 2; - Assert.Empty(detecting); - Assert.Empty(detected); + Assert.Empty(detectingEntity); + Assert.Empty(detectedEntity); _ = context.Entry(product); - Assert.Single(detecting); - Assert.Single(detected); + Assert.Empty(detectingAll); + Assert.Empty(detectedAll); + Assert.Single(detectingEntity); + Assert.Single(detectedEntity); - AssertLocalDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + AssertLocalDetectChangesEvent(changesFound: true, detectingEntity[0], detectedEntity[0]); } [ConditionalFact] public void Local_DetectChanges_events_fire_for_reference_navigation_change() { - var detecting = new List(); - var detected = new List(); + var detectingAll = new List(); + var detectedAll = new List(); + var detectingEntity = new List(); + var detectedEntity = new List(); using var scope = _poolProvider.CreateScope(); using var context = new EarlyLearningCenter(); - RegisterEvents(context, detecting, detected); + RegisterDetectAllEvents(context, detectingAll, detectedAll); + RegisterDetectEntityEvents(context, detectingEntity, detectedEntity); var product = new Product { Category = new Category()}; context.Attach(product); product.Category = null; - Assert.Empty(detecting); - Assert.Empty(detected); + Assert.Empty(detectingEntity); + Assert.Empty(detectedEntity); _ = context.Entry(product); - Assert.Single(detecting); - Assert.Single(detected); + Assert.Empty(detectingAll); + Assert.Empty(detectedAll); + Assert.Equal(2, detectingEntity.Count); + Assert.Equal(2, detectedEntity.Count); - AssertLocalDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + AssertLocalDetectChangesEvent(changesFound: false, detectingEntity[0], detectedEntity[0]); + AssertLocalDetectChangesEvent(changesFound: true, detectingEntity[1], detectedEntity[1]); } [ConditionalFact] public void Local_DetectChanges_events_fire_for_collection_navigation_change() { - var detecting = new List(); - var detected = new List(); + var detectingAll = new List(); + var detectedAll = new List(); + var detectingEntity = new List(); + var detectedEntity = new List(); using var scope = _poolProvider.CreateScope(); using var context = new EarlyLearningCenter(); - RegisterEvents(context, detecting, detected); + RegisterDetectAllEvents(context, detectingAll, detectedAll); + RegisterDetectEntityEvents(context, detectingEntity, detectedEntity); var product = new Product { Category = new Category()}; context.Attach(product); product.Category.Products.Clear(); - Assert.Empty(detecting); - Assert.Empty(detected); + Assert.Empty(detectingEntity); + Assert.Empty(detectedEntity); _ = context.Entry(product.Category); - Assert.Single(detecting); - Assert.Single(detected); + Assert.Empty(detectingAll); + Assert.Empty(detectedAll); + Assert.Single(detectingEntity); + Assert.Single(detectedEntity); - AssertLocalDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + AssertLocalDetectChangesEvent(changesFound: true, detectingEntity[0], detectedEntity[0]); } [ConditionalFact] public void Local_DetectChanges_events_fire_for_skip_navigation_change() { - var detecting = new List(); - var detected = new List(); + var detectingAll = new List(); + var detectedAll = new List(); + var detectingEntity = new List(); + var detectedEntity = new List(); using var scope = _poolProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); - RegisterEvents(context, detecting, detected); + RegisterDetectAllEvents(context, detectingAll, detectedAll); + RegisterDetectEntityEvents(context, detectingEntity, detectedEntity); var cat = new Cat(1) { Hats = { new Hat(2) }}; context.Attach(cat); cat.Hats.Clear(); - Assert.Empty(detecting); - Assert.Empty(detected); + Assert.Empty(detectingEntity); + Assert.Empty(detectedEntity); _ = context.Entry(cat); - Assert.Single(detecting); - Assert.Single(detected); + Assert.Empty(detectingAll); + Assert.Empty(detectedAll); + Assert.Single(detectingEntity); + Assert.Single(detectedEntity); - AssertLocalDetectChangesEvent(context, changesFound: true, detecting[0], detected[0]); + AssertLocalDetectChangesEvent(changesFound: true, detectingEntity[0], detectedEntity[0]); + } + + [ConditionalFact] // Issue #26506 + public void DetectChanges_event_can_be_used_to_know_when_all_properties_have_changed() + { + using var scope = _poolProvider.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + + var hat = new Hat(2) { Color = "Orange" }; + var cat1 = new Cat(1) { Hats = { hat }}; + var cat2 = new Cat(2); + context.AttachRange(cat1, cat2); + + var stateChangedCalled = false; + context.ChangeTracker.StateChanged += (s, a) => + { + stateChangedCalled = true; + Assert.Equal("Black", a.Entry.Property(nameof(Hat.Color)).CurrentValue); + Assert.Equal(1, a.Entry.Property(nameof(Hat.CatId)).CurrentValue); + }; + + var detectedChangesCalled = false; + context.ChangeTracker.DetectedEntityChanges += (s, a) => + { + if (a.ChangesFound) + { + detectedChangesCalled = true; + + Assert.Equal("Black", a.Entry.Property(nameof(Hat.Color)).CurrentValue); + Assert.Equal(2, a.Entry.Property(nameof(Hat.CatId)).CurrentValue); + } + }; + + hat.Cat = cat2; + hat.Color = "Black"; + + context.ChangeTracker.DetectChanges(); + + Assert.True(stateChangedCalled); + Assert.True(detectedChangesCalled); + + Assert.Equal(2, hat.CatId); } private static void AssertTrackedEvent( @@ -1928,26 +2044,13 @@ 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) + DetectEntityChangesEventArgs detectingEntity, + DetectedEntityChangesEventArgs detectedEntity) { - Assert.NotNull(detecting.Entry); - Assert.Same(detecting.Entry!.Entity, detected.Entry!.Entity); - Assert.Equal(changesFound, detected.ChangesFound); + Assert.Same(detectingEntity.Entry.Entity, detectedEntity.Entry.Entity); + Assert.Equal(changesFound, detectedEntity.ChangesFound); } private static void RegisterEvents( @@ -1984,21 +2087,45 @@ private static void RegisterEvents( }; } - private static void RegisterEvents( + private static void RegisterDetectAllEvents( + DbContext context, + IList detectingAll, + IList detectedAll) + { + context.ChangeTracker.DetectingAllChanges += (s, e) => + { + _ = context.ChangeTracker.Entries().ToList(); // Should not recursively call DetectChanges + Assert.Same(context.ChangeTracker, s); + detectingAll.Add(e); + }; + + context.ChangeTracker.DetectedAllChanges += (s, e) => + { + _ = context.ChangeTracker.Entries().ToList(); // Should not recursively call DetectChanges + Assert.Same(context.ChangeTracker, s); + detectedAll.Add(e); + }; + } + + private static void RegisterDetectEntityEvents( DbContext context, - IList detecting, - IList detected) + IList detectingEntity, + IList detectedEntity) { - context.ChangeTracker.DetectingChanges += (s, e) => + context.ChangeTracker.DetectingEntityChanges += (s, e) => { + _ = context.ChangeTracker.Entries().ToList(); // Should not recursively call DetectChanges Assert.Same(context.ChangeTracker, s); - detecting.Add(e); + Assert.NotNull(e.Entry); + detectingEntity.Add(e); }; - context.ChangeTracker.DetectedChanges += (s, e) => + context.ChangeTracker.DetectedEntityChanges += (s, e) => { + _ = context.ChangeTracker.Entries().ToList(); // Should not recursively call DetectChanges Assert.Same(context.ChangeTracker, s); - detected.Add(e); + Assert.NotNull(e.Entry); + detectedEntity.Add(e); }; } diff --git a/test/EFCore.Tests/ChangeTracking/Internal/InternalEntryEntrySubscriberTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/InternalEntryEntrySubscriberTest.cs index 4c444dff194..850691cd798 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/InternalEntryEntrySubscriberTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/InternalEntryEntrySubscriberTest.cs @@ -490,37 +490,48 @@ public void DetectChanges(InternalEntityEntry entry) { } - public (EventHandler DetectingChanges, EventHandler DetectedChanges) - CaptureEvents() - => (null, null); - - public void SetEvents(EventHandler detectingChanges, EventHandler detectedChanges) + public (EventHandler DetectingAllChanges, + EventHandler DetectedAllChanges, + EventHandler DetectingEntityChanges, + EventHandler + DetectedEntityChanges) CaptureEvents() + => (null, null, null, null); + + public void SetEvents( + EventHandler detectingAllChanges, + EventHandler detectedAllChanges, + EventHandler detectingEntityChanges, + EventHandler detectedEntityChanges) { } - public void Suspend() - { - } + public event EventHandler DetectingEntityChanges; - public void Resume() - { - } + public void OnDetectingEntityChanges(InternalEntityEntry internalEntityEntry) + => DetectingEntityChanges?.Invoke(null, null); + + public event EventHandler DetectingAllChanges; - public event EventHandler DetectingChanges; + public void OnDetectingAllChanges(IStateManager stateManager) + => DetectingAllChanges?.Invoke(null, null); - public void OnDetectingChanges(InternalEntityEntry internalEntityEntry) - => DetectingChanges?.Invoke(null, null); + public event EventHandler DetectedEntityChanges; - public void OnDetectingChanges(IStateManager stateManager) - => DetectingChanges?.Invoke(null, null); + public void OnDetectedEntityChanges(InternalEntityEntry internalEntityEntry, bool changesFound) + => DetectedEntityChanges?.Invoke(null, null); - public event EventHandler DetectedChanges; + public event EventHandler DetectedAllChanges; - public void OnDetectedChanges(InternalEntityEntry internalEntityEntry, bool changesFound) - => DetectedChanges?.Invoke(null, null); + public void OnDetectedAllChanges(IStateManager stateManager, bool changesFound) + => DetectedAllChanges?.Invoke(null, null); - public void OnDetectedChanges(IStateManager stateManager, bool changesFound) - => DetectedChanges?.Invoke(null, null); + public void Suspend() + { + } + + public void Resume() + { + } public void ResetState() { diff --git a/test/EFCore.Tests/DbContextTest.cs b/test/EFCore.Tests/DbContextTest.cs index 549a4b5312b..f6351e61779 100644 --- a/test/EFCore.Tests/DbContextTest.cs +++ b/test/EFCore.Tests/DbContextTest.cs @@ -186,14 +186,6 @@ public void DetectChanges(InternalEntityEntry entry) { } - public (EventHandler DetectingChanges, EventHandler DetectedChanges) - CaptureEvents() - => (null, null); - - public void SetEvents(EventHandler detectingChanges, EventHandler detectedChanges) - { - } - public void PropertyChanged(InternalEntityEntry entry, IPropertyBase property, bool setModifed) { } @@ -210,21 +202,40 @@ public virtual void Resume() { } - public event EventHandler DetectingChanges; + public (EventHandler DetectingAllChanges, + EventHandler DetectedAllChanges, + EventHandler DetectingEntityChanges, + EventHandler + DetectedEntityChanges) CaptureEvents() + => (null, null, null, null); + + public void SetEvents( + EventHandler detectingAllChanges, + EventHandler detectedAllChanges, + EventHandler detectingEntityChanges, + EventHandler detectedEntityChanges) + { + } + + public event EventHandler DetectingEntityChanges; + + public void OnDetectingEntityChanges(InternalEntityEntry internalEntityEntry) + => DetectingEntityChanges?.Invoke(null, null); + + public event EventHandler DetectingAllChanges; - public void OnDetectingChanges(InternalEntityEntry internalEntityEntry) - => DetectingChanges?.Invoke(null, null); + public void OnDetectingAllChanges(IStateManager stateManager) + => DetectingAllChanges?.Invoke(null, null); - public void OnDetectingChanges(IStateManager stateManager) - => DetectingChanges?.Invoke(null, null); + public event EventHandler DetectedEntityChanges; - public event EventHandler DetectedChanges; + public void OnDetectedEntityChanges(InternalEntityEntry internalEntityEntry, bool changesFound) + => DetectedEntityChanges?.Invoke(null, null); - public void OnDetectedChanges(InternalEntityEntry internalEntityEntry, bool changesFound) - => DetectedChanges?.Invoke(null, null); + public event EventHandler DetectedAllChanges; - public void OnDetectedChanges(IStateManager stateManager, bool changesFound) - => DetectedChanges?.Invoke(null, null); + public void OnDetectedAllChanges(IStateManager stateManager, bool changesFound) + => DetectedAllChanges?.Invoke(null, null); public void ResetState() {