Skip to content

Commit

Permalink
Implement ChangeTracker.Clear() to stop tracking all entities
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ajcvickers committed Jun 8, 2020
1 parent 31f219d commit c8c948b
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 8 deletions.
21 changes: 20 additions & 1 deletion src/EFCore/ChangeTracking/ChangeTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ private void TryDetectChanges()
/// </para>
/// <para>
/// Note that this method calls <see cref="DetectChanges" /> unless
/// <see cref="AutoDetectChangesEnabled" /> has been set to <see langword="false"/>.
/// <see cref="AutoDetectChangesEnabled" /> has been set to <see langword="false"/>.
/// </para>
/// </summary>
/// <returns> <see langword="true"/> if there are changes to save, otherwise <see langword="false"/>. </returns>
Expand Down Expand Up @@ -396,6 +396,25 @@ Task IResettableService.ResetStateAsync(CancellationToken cancellationToken)
return default;
}

/// <summary>
/// <para>
/// Stops tracking all currently tracked entities.
/// </para>
/// <para>
/// <see cref="DbContext"/> 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.
/// </para>
/// <para>
/// 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.
/// </para>
/// </summary>
public virtual void Clear()
=> StateManager.Clear();

/// <summary>
/// <para>
/// Expand this property in the debugger for a human-readable view of the entities being tracked.
Expand Down
8 changes: 8 additions & 0 deletions src/EFCore/ChangeTracking/Internal/IStateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -449,5 +449,13 @@ IEnumerable<IUpdateEntry> GetDependentsUsingRelationshipSnapshot(
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
IDiagnosticsLogger<DbLoggerCategory.Update> UpdateLogger { get; }

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
void Clear();
}
}
17 changes: 14 additions & 3 deletions src/EFCore/ChangeTracking/Internal/StateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,20 @@ public virtual void Unsubscribe()
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual void ResetState()
{
Clear();

Tracked = null;
StateChanged = null;
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
public virtual void Clear()
{
Unsubscribe();
ChangedCount = 0;
Expand All @@ -657,9 +671,6 @@ public virtual void ResetState()

_needsUnsubscribe = false;

Tracked = null;
StateChanged = null;

SavingChanges = false;
}

Expand Down
45 changes: 44 additions & 1 deletion test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DefaultOptionsPooledContext>();

Expand Down
31 changes: 31 additions & 0 deletions test/EFCore.Tests/ChangeTracking/ChangeTrackerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Hat>().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<Hat>().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)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<IChangeDetector, TestPropertyListener>(),
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions test/EFCore.Tests/TestUtilities/FakeStateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ public int GetCountForState(

public IDiagnosticsLogger<DbLoggerCategory.Update> UpdateLogger { get; }

public void Clear() => throw new NotImplementedException();

public bool SavingChanges => throw new NotImplementedException();

public IEnumerable<TEntity> GetNonDeletedEntities<TEntity>()
Expand Down

0 comments on commit c8c948b

Please sign in to comment.