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()