From c8c948bf616fc8e9e6fec0edd5783635e25b6344 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Sun, 7 Jun 2020 15:27:55 -0700 Subject: [PATCH] Implement ChangeTracker.Clear() to stop tracking all entities Fixes #15577 This is an alternative to mass-detach for situations where creating a new context instance is difficult. The context configuration is not reset, since it seems likely that setting like lazy-loading and registered events are more useful left as they would be--just as is the case when doing mass-detach. --- src/EFCore/ChangeTracking/ChangeTracker.cs | 21 ++++++++- .../ChangeTracking/Internal/IStateManager.cs | 8 ++++ .../ChangeTracking/Internal/StateManager.cs | 17 +++++-- .../DbContextPoolingTest.cs | 45 ++++++++++++++++++- .../ChangeTracking/ChangeTrackerTest.cs | 31 +++++++++++++ .../InternalEntryEntrySubscriberTest.cs | 15 +++++-- .../TestUtilities/FakeStateManager.cs | 2 + 7 files changed, 131 insertions(+), 8 deletions(-) diff --git a/src/EFCore/ChangeTracking/ChangeTracker.cs b/src/EFCore/ChangeTracking/ChangeTracker.cs index 4cecdcc590f..d06b9604bbd 100644 --- a/src/EFCore/ChangeTracking/ChangeTracker.cs +++ b/src/EFCore/ChangeTracking/ChangeTracker.cs @@ -195,7 +195,7 @@ private void TryDetectChanges() /// /// /// Note that this method calls unless - /// has been set to . + /// has been set to . /// /// /// if there are changes to save, otherwise . @@ -396,6 +396,25 @@ Task IResettableService.ResetStateAsync(CancellationToken cancellationToken) return default; } + /// + /// + /// Stops tracking all currently tracked entities. + /// + /// + /// is designed to have a short lifetime where a new instance is created for each unit-of-work. + /// This manner means all tracked entities are discarded when the context is disposed at the end of each unit-of-work. + /// However, clearing all tracked entities using this method may be useful in situations where creating a new context + /// instance is not practical. + /// + /// + /// This method should always be preferred over detaching every tracked entity. + /// Detaching entities is a slow process that may have side effects. + /// This method is much more efficient at clearing all tracked entities from the context. + /// + /// + public virtual void Clear() + => StateManager.Clear(); + /// /// /// Expand this property in the debugger for a human-readable view of the entities being tracked. diff --git a/src/EFCore/ChangeTracking/Internal/IStateManager.cs b/src/EFCore/ChangeTracking/Internal/IStateManager.cs index ac940e9639d..d54e6e50dcc 100644 --- a/src/EFCore/ChangeTracking/Internal/IStateManager.cs +++ b/src/EFCore/ChangeTracking/Internal/IStateManager.cs @@ -449,5 +449,13 @@ IEnumerable GetDependentsUsingRelationshipSnapshot( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// IDiagnosticsLogger UpdateLogger { 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. + /// + void Clear(); } } diff --git a/src/EFCore/ChangeTracking/Internal/StateManager.cs b/src/EFCore/ChangeTracking/Internal/StateManager.cs index b4e2e82591b..2606ccb555a 100644 --- a/src/EFCore/ChangeTracking/Internal/StateManager.cs +++ b/src/EFCore/ChangeTracking/Internal/StateManager.cs @@ -645,6 +645,20 @@ public virtual void Unsubscribe() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual void ResetState() + { + Clear(); + + Tracked = null; + StateChanged = 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 virtual void Clear() { Unsubscribe(); ChangedCount = 0; @@ -657,9 +671,6 @@ public virtual void ResetState() _needsUnsubscribe = false; - Tracked = null; - StateChanged = null; - SavingChanges = false; } diff --git a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs index bb3534ac363..b61c5a109c2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs @@ -410,7 +410,50 @@ public void Context_configuration_is_reset(bool useInterface) } [ConditionalFact] - public void Default_Context_configuration__is_reset() + public void Change_tracker_can_be_cleared_without_resetting_context_config() + { + var context = new PooledContext( + new DbContextOptionsBuilder().UseSqlServer( + SqlServerNorthwindTestStoreFactory.NorthwindConnectionString).Options); + + context.ChangeTracker.AutoDetectChangesEnabled = true; + context.ChangeTracker.LazyLoadingEnabled = true; + context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + context.ChangeTracker.CascadeDeleteTiming = CascadeTiming.Immediate; + context.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Immediate; + context.Database.AutoTransactionsEnabled = true; + context.ChangeTracker.Tracked += ChangeTracker_OnTracked; + context.ChangeTracker.StateChanged += ChangeTracker_OnStateChanged; + + context.ChangeTracker.Clear(); + + Assert.True(context.ChangeTracker.AutoDetectChangesEnabled); + Assert.True(context.ChangeTracker.LazyLoadingEnabled); + Assert.Equal(QueryTrackingBehavior.NoTracking, context.ChangeTracker.QueryTrackingBehavior); + Assert.Equal(CascadeTiming.Immediate, context.ChangeTracker.CascadeDeleteTiming); + Assert.Equal(CascadeTiming.Immediate, context.ChangeTracker.DeleteOrphansTiming); + Assert.True(context.Database.AutoTransactionsEnabled); + + Assert.False(_changeTracker_OnTracked); + Assert.False(_changeTracker_OnStateChanged); + + context.Customers.Attach( + new PooledContext.Customer { CustomerId = "C" }).State = EntityState.Modified; + + Assert.True(_changeTracker_OnTracked); + Assert.True(_changeTracker_OnStateChanged); + } + + private bool _changeTracker_OnTracked; + private void ChangeTracker_OnTracked(object sender, EntityTrackedEventArgs e) + => _changeTracker_OnTracked = true; + + private bool _changeTracker_OnStateChanged; + private void ChangeTracker_OnStateChanged(object sender, EntityStateChangedEventArgs e) + => _changeTracker_OnStateChanged = true; + + [ConditionalFact] + public void Default_Context_configuration_is_reset() { var serviceProvider = BuildServiceProvider(); diff --git a/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs b/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs index dad60b7a60b..23330cca8b3 100644 --- a/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs +++ b/test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs @@ -25,6 +25,37 @@ namespace Microsoft.EntityFrameworkCore.ChangeTracking { public class ChangeTrackerTest { + [ConditionalFact] + public void Change_tracker_can_be_cleared() + { + Seed(); + + using var context = new LikeAZooContext(); + + var cats = context.Cats.ToList(); + var hats = context.Set().ToList(); + + Assert.Equal(3, context.ChangeTracker.Entries().Count()); + Assert.Equal(EntityState.Unchanged, context.Entry(cats[0]).State); + Assert.Equal(EntityState.Unchanged, context.Entry(hats[0]).State); + + context.ChangeTracker.Clear(); + + Assert.Empty(context.ChangeTracker.Entries()); + Assert.Equal(EntityState.Detached, context.Entry(cats[0]).State); + Assert.Equal(EntityState.Detached, context.Entry(hats[0]).State); + + var catsAgain = context.Cats.ToList(); + var hatsAgain = context.Set().ToList(); + + Assert.Equal(3, context.ChangeTracker.Entries().Count()); + Assert.Equal(EntityState.Unchanged, context.Entry(catsAgain[0]).State); + Assert.Equal(EntityState.Unchanged, context.Entry(hatsAgain[0]).State); + + Assert.Equal(EntityState.Detached, context.Entry(cats[0]).State); + Assert.Equal(EntityState.Detached, context.Entry(hats[0]).State); + } + [ConditionalTheory] [InlineData(false)] [InlineData(true)] diff --git a/test/EFCore.Tests/ChangeTracking/Internal/InternalEntryEntrySubscriberTest.cs b/test/EFCore.Tests/ChangeTracking/Internal/InternalEntryEntrySubscriberTest.cs index ff62415ce11..4073424b000 100644 --- a/test/EFCore.Tests/ChangeTracking/Internal/InternalEntryEntrySubscriberTest.cs +++ b/test/EFCore.Tests/ChangeTracking/Internal/InternalEntryEntrySubscriberTest.cs @@ -428,8 +428,10 @@ public void Entry_unsubscribes_to_INotifyCollectionChanged() Assert.Same(entries[2], testListener.CollectionChanged.Skip(2).Single().Item1); } - [ConditionalFact] - public void Entries_are_unsubscribed_when_context_is_disposed() + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public void Entries_are_unsubscribed_when_context_is_disposed_or_cleared(bool useClear) { var context = InMemoryTestHelpers.Instance.CreateContext( new ServiceCollection().AddScoped(), @@ -458,7 +460,14 @@ public void Entries_are_unsubscribed_when_context_is_disposed() Assert.Equal(2, testListener.Changing.Count); Assert.Equal(2, testListener.Changed.Count); - context.Dispose(); + if (useClear) + { + context.ChangeTracker.Clear(); + } + else + { + context.Dispose(); + } entities[5].Name = "Carmack"; Assert.Equal(2, testListener.Changing.Count); diff --git a/test/EFCore.Tests/TestUtilities/FakeStateManager.cs b/test/EFCore.Tests/TestUtilities/FakeStateManager.cs index 1c04235919f..924decc2847 100644 --- a/test/EFCore.Tests/TestUtilities/FakeStateManager.cs +++ b/test/EFCore.Tests/TestUtilities/FakeStateManager.cs @@ -68,6 +68,8 @@ public int GetCountForState( public IDiagnosticsLogger UpdateLogger { get; } + public void Clear() => throw new NotImplementedException(); + public bool SavingChanges => throw new NotImplementedException(); public IEnumerable GetNonDeletedEntities()