Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement ChangeTracker.Clear() to stop tracking all entities #21169

Merged
merged 1 commit into from
Jun 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might also want to mention that it doesn't trigger the state changed event

/// </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);
ajcvickers marked this conversation as resolved.
Show resolved Hide resolved
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