Skip to content

Commit

Permalink
[release/7.0] Reset LocalView when returning context to the pool (#29181
Browse files Browse the repository at this point in the history
)
  • Loading branch information
ajcvickers committed Sep 22, 2022
1 parent 0a76a0b commit e7f1bd1
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 9 deletions.
19 changes: 19 additions & 0 deletions src/EFCore/ChangeTracking/LocalView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -499,4 +499,23 @@ IList IListSource.GetList()
/// </summary>
bool IListSource.ContainsListCollection
=> false;

/// <summary>
/// Resets this view, clearing any <see cref="IBindingList" /> created with <see cref="ToBindingList" /> and
/// any <see cref="ObservableCollection{T}" /> created with <see cref="ToObservableCollection" />, and clearing any
/// events registered on <see cref="PropertyChanged" />, <see cref="PropertyChanging" />, or <see cref="CollectionChanged" />.
/// </summary>
public virtual void Reset()
{
_bindingList = null;
_observable = null;
_countChanges = 0;
_count = 0;
_triggeringStateManagerChange = false;
_triggeringObservableChange = false;
_triggeringLocalViewChange = false;
PropertyChanged = null;
PropertyChanging = null;
CollectionChanged = null;
}
}
4 changes: 1 addition & 3 deletions src/EFCore/Internal/InternalDbSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -520,16 +520,14 @@ IServiceProvider IInfrastructure<IServiceProvider>.Instance
/// doing so can result in application failures when updating to a new Entity Framework Core release.
/// </summary>
void IResettableService.ResetState()
=> _localView = null;
=> _localView?.Reset();

/// <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>
/// <param name="cancellationToken">A <see cref="CancellationToken" /> to observe while waiting for the task to complete.</param>
/// <exception cref="OperationCanceledException">If the <see cref="CancellationToken" /> is canceled.</exception>
Task IResettableService.ResetStateAsync(CancellationToken cancellationToken)
{
((IResettableService)this).ResetState();
Expand Down
98 changes: 92 additions & 6 deletions test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Data;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore.Internal;
Expand Down Expand Up @@ -164,6 +167,12 @@ public class Customer
{
public string CustomerId { get; set; }
public string CompanyName { get; set; }
public ObservableCollection<Order> Orders { get; } = new();
}

public class Order
{
public string OrderId { get; set; }
}

private interface ISecondContext
Expand Down Expand Up @@ -768,6 +777,20 @@ public async Task Context_configuration_is_reset(bool useInterface, bool async)

Assert.Null(context1!.Database.GetCommandTimeout());

var set = context1.Customers;
var localView = set.Local;
localView.PropertyChanged += LocalView_OnPropertyChanged;
localView.PropertyChanging += LocalView_OnPropertyChanging;
localView.CollectionChanged += LocalView_OnCollectionChanged;
var customer1 = new Customer { CustomerId = "C" };
context1.Customers.Attach(customer1);
Assert.Equal(1, localView.Count);
Assert.Same(customer1, localView.ToBindingList().Single());
Assert.Same(customer1, localView.ToObservableCollection().Single());
Assert.True(_localView_OnPropertyChanging);
Assert.True(_localView_OnPropertyChanged);
Assert.True(_localView_OnCollectionChanged);

context1.ChangeTracker.AutoDetectChangesEnabled = true;
context1.ChangeTracker.LazyLoadingEnabled = true;
context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
Expand All @@ -788,6 +811,10 @@ public async Task Context_configuration_is_reset(bool useInterface, bool async)
context1.SavedChanges += Context_OnSavedChanges;
context1.SaveChangesFailed += Context_OnSaveChangesFailed;

_localView_OnPropertyChanging = false;
_localView_OnPropertyChanged = false;
_localView_OnCollectionChanged = false;

await Dispose(serviceScope, async);

serviceScope = serviceProvider.CreateScope();
Expand All @@ -808,9 +835,13 @@ public async Task Context_configuration_is_reset(bool useInterface, bool async)
Assert.False(context2.Database.AutoSavepointsEnabled);
Assert.Null(context1.Database.GetCommandTimeout());

var customer = new Customer { CustomerId = "C" };
context2.Customers.Attach(customer).State = EntityState.Modified;
context2.Customers.Attach(customer).State = EntityState.Unchanged;
Assert.Empty(localView);
Assert.Empty(localView.ToBindingList());
Assert.Empty(localView.ToObservableCollection());

var customer2 = new Customer { CustomerId = "C" };
context2.Customers.Attach(customer2).State = EntityState.Modified;
context2.Customers.Attach(customer2).State = EntityState.Unchanged;

Assert.False(_changeTracker_OnTracking);
Assert.False(_changeTracker_OnTracked);
Expand All @@ -826,6 +857,15 @@ public async Task Context_configuration_is_reset(bool useInterface, bool async)
Assert.False(_context_OnSavedChanges);
Assert.False(_context_OnSavingChanges);
Assert.False(_context_OnSaveChangesFailed);

Assert.Same(set, context2!.Customers);
Assert.Same(localView, context2!.Customers.Local);
Assert.Equal(1, localView.Count);
Assert.Same(customer2, localView.ToBindingList().Single());
Assert.Same(customer2, localView.ToObservableCollection().Single());
Assert.False(_localView_OnPropertyChanging);
Assert.False(_localView_OnPropertyChanged);
Assert.False(_localView_OnCollectionChanged);
}

[ConditionalTheory]
Expand Down Expand Up @@ -865,6 +905,20 @@ public async Task Context_configuration_is_reset_with_factory(bool async, bool w
var factory = BuildFactory<PooledContext>(withDependencyInjection);

var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext();
var set = context1.Customers;

var localView = set.Local;
localView.PropertyChanged += LocalView_OnPropertyChanged;
localView.PropertyChanging += LocalView_OnPropertyChanging;
localView.CollectionChanged += LocalView_OnCollectionChanged;
var customer1 = new Customer { CustomerId = "C" };
context1.Customers.Attach(customer1);
Assert.Equal(1, localView.Count);
Assert.Same(customer1, localView.ToBindingList().Single());
Assert.Same(customer1, localView.ToObservableCollection().Single());
Assert.True(_localView_OnPropertyChanging);
Assert.True(_localView_OnPropertyChanged);
Assert.True(_localView_OnCollectionChanged);

context1.ChangeTracker.AutoDetectChangesEnabled = true;
context1.ChangeTracker.LazyLoadingEnabled = true;
Expand All @@ -885,15 +939,23 @@ public async Task Context_configuration_is_reset_with_factory(bool async, bool w
context1.SavedChanges += Context_OnSavedChanges;
context1.SaveChangesFailed += Context_OnSaveChangesFailed;

_localView_OnPropertyChanging = false;
_localView_OnPropertyChanged = false;
_localView_OnCollectionChanged = false;

await Dispose(context1, async);

var context2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext();

Assert.Same(context1, context2);

var customer = new Customer { CustomerId = "C" };
context2.Customers.Attach(customer).State = EntityState.Modified;
context2.Customers.Attach(customer).State = EntityState.Unchanged;
Assert.Empty(localView);
Assert.Empty(localView.ToBindingList());
Assert.Empty(localView.ToObservableCollection());

var customer2 = new Customer { CustomerId = "C" };
context2.Customers.Attach(customer2).State = EntityState.Modified;
context2.Customers.Attach(customer2).State = EntityState.Unchanged;

Assert.False(_changeTracker_OnTracking);
Assert.False(_changeTracker_OnTracked);
Expand All @@ -909,6 +971,15 @@ public async Task Context_configuration_is_reset_with_factory(bool async, bool w
Assert.False(_context_OnSavedChanges);
Assert.False(_context_OnSavingChanges);
Assert.False(_context_OnSaveChangesFailed);

Assert.Same(set, context2!.Customers);
Assert.Same(localView, context2!.Customers.Local);
Assert.Equal(1, localView.Count);
Assert.Same(customer2, localView.ToBindingList().Single());
Assert.Same(customer2, localView.ToObservableCollection().Single());
Assert.False(_localView_OnPropertyChanging);
Assert.False(_localView_OnPropertyChanged);
Assert.False(_localView_OnCollectionChanged);
}

[ConditionalFact]
Expand Down Expand Up @@ -995,6 +1066,21 @@ private void Context_OnSaveChangesFailed(object sender, SaveChangesFailedEventAr

private bool _context_OnSaveChangesFailed;

private bool _localView_OnPropertyChanged;

private void LocalView_OnPropertyChanged(object sender, PropertyChangedEventArgs e)
=> _localView_OnPropertyChanged = true;

private bool _localView_OnPropertyChanging;

private void LocalView_OnPropertyChanging(object sender, PropertyChangingEventArgs e)
=> _localView_OnPropertyChanging = true;

private bool _localView_OnCollectionChanged;

private void LocalView_OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
=> _localView_OnCollectionChanged = true;

private bool _changeTracker_OnTracking;

private void ChangeTracker_OnTracking(object sender, EntityTrackingEventArgs e)
Expand Down

0 comments on commit e7f1bd1

Please sign in to comment.