diff --git a/src/EFCore/DbContext.cs b/src/EFCore/DbContext.cs index a29e165496c..78ea16d6900 100644 --- a/src/EFCore/DbContext.cs +++ b/src/EFCore/DbContext.cs @@ -52,6 +52,9 @@ public class DbContext : IDbSetCache, IDbContextPoolable { + private static readonly bool QuirkEnabled29733 + = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue29733", out var enabled) && enabled; + private readonly DbContextOptions _options; private IDictionary<(Type Type, string? Name), object>? _sets; @@ -882,8 +885,20 @@ private void SetLeaseInternal(DbContextLease lease) || _configurationSnapshot.HasChangeTrackerConfiguration) { var changeTracker = ChangeTracker; + if (QuirkEnabled29733 + && _configurationSnapshot.QueryTrackingBehavior.HasValue) + { + changeTracker.QueryTrackingBehavior = _configurationSnapshot.QueryTrackingBehavior.Value; + } + else + { + ((IResettableService)changeTracker).ResetState(); + if (_configurationSnapshot.QueryTrackingBehavior.HasValue) + { + changeTracker.QueryTrackingBehavior = _configurationSnapshot.QueryTrackingBehavior.Value; + } + } changeTracker.AutoDetectChangesEnabled = _configurationSnapshot.AutoDetectChangesEnabled; - changeTracker.QueryTrackingBehavior = _configurationSnapshot.QueryTrackingBehavior; changeTracker.LazyLoadingEnabled = _configurationSnapshot.LazyLoadingEnabled; changeTracker.CascadeDeleteTiming = _configurationSnapshot.CascadeDeleteTiming; changeTracker.DeleteOrphansTiming = _configurationSnapshot.DeleteOrphansTiming; @@ -940,7 +955,9 @@ void IDbContextPoolable.SnapshotConfiguration() _changeTracker != null, changeDetectorEvents != null, _changeTracker?.AutoDetectChangesEnabled ?? true, - _changeTracker?.QueryTrackingBehavior ?? QueryTrackingBehavior.TrackAll, + QuirkEnabled29733 + ? _changeTracker?.QueryTrackingBehavior ?? QueryTrackingBehavior.TrackAll + : _changeTracker?.QueryTrackingBehavior, _database?.AutoTransactionBehavior ?? AutoTransactionBehavior.WhenNeeded, _database?.AutoSavepointsEnabled ?? true, _changeTracker?.LazyLoadingEnabled ?? true, diff --git a/src/EFCore/Internal/DbContextPoolConfigurationSnapshot.cs b/src/EFCore/Internal/DbContextPoolConfigurationSnapshot.cs index e8bc81d54a3..b5cd4c856cd 100644 --- a/src/EFCore/Internal/DbContextPoolConfigurationSnapshot.cs +++ b/src/EFCore/Internal/DbContextPoolConfigurationSnapshot.cs @@ -23,7 +23,7 @@ public DbContextPoolConfigurationSnapshot( bool hasChangeTrackerConfiguration, bool hasChangeDetectorConfiguration, bool autoDetectChangesEnabled, - QueryTrackingBehavior queryTrackingBehavior, + QueryTrackingBehavior? queryTrackingBehavior, AutoTransactionBehavior autoTransactionBehavior, bool autoSavepointsEnabled, bool lazyLoadingEnabled, @@ -135,7 +135,7 @@ public DbContextPoolConfigurationSnapshot( /// 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 QueryTrackingBehavior QueryTrackingBehavior { get; } + public QueryTrackingBehavior? QueryTrackingBehavior { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs index c4e3449b4a2..672525eb67c 100644 --- a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs @@ -30,19 +30,51 @@ private static DbContextOptionsBuilder ConfigureOptions(DbContextOptionsBuilder .UseSqlServer(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString) .EnableServiceProviderCaching(false); - private static IServiceProvider BuildServiceProvider() + private static IServiceProvider BuildServiceProvider(Action optionsAction = null) where TContextService : class where TContext : DbContext, TContextService => new ServiceCollection() - .AddDbContextPool(ob => ConfigureOptions(ob)) - .AddDbContextPool(ob => ConfigureOptions(ob)) + .AddDbContextPool( + ob => + { + var builder = ConfigureOptions(ob); + if (optionsAction != null) + { + optionsAction(builder); + } + }) + .AddDbContextPool( + ob => + { + var builder = ConfigureOptions(ob); + if (optionsAction != null) + { + optionsAction(builder); + } + }) .BuildServiceProvider(validateScopes: true); - private static IServiceProvider BuildServiceProvider() + private static IServiceProvider BuildServiceProvider(Action optionsAction = null) where TContext : DbContext => new ServiceCollection() - .AddDbContextPool(ob => ConfigureOptions(ob)) - .AddDbContextPool(ob => ConfigureOptions(ob)) + .AddDbContextPool( + ob => + { + var builder = ConfigureOptions(ob); + if (optionsAction != null) + { + optionsAction(builder); + } + }) + .AddDbContextPool( + ob => + { + var builder = ConfigureOptions(ob); + if (optionsAction != null) + { + optionsAction(builder); + } + }) .BuildServiceProvider(validateScopes: true); private static IServiceProvider BuildServiceProviderWithFactory() @@ -758,15 +790,27 @@ public async Task Contexts_are_pooled_with_factory(bool async, bool withDependen } [ConditionalTheory] - [InlineData(false, false)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(true, true)] - public async Task Context_configuration_is_reset(bool useInterface, bool async) + [InlineData(false, false, null)] + [InlineData(true, false, null)] + [InlineData(false, true, null)] + [InlineData(true, true, null)] + [InlineData(false, false, QueryTrackingBehavior.TrackAll)] + [InlineData(true, false, QueryTrackingBehavior.TrackAll)] + [InlineData(false, true, QueryTrackingBehavior.TrackAll)] + [InlineData(true, true, QueryTrackingBehavior.TrackAll)] + [InlineData(false, false, QueryTrackingBehavior.NoTracking)] + [InlineData(true, false, QueryTrackingBehavior.NoTracking)] + [InlineData(false, true, QueryTrackingBehavior.NoTracking)] + [InlineData(true, true, QueryTrackingBehavior.NoTracking)] + [InlineData(false, false, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(true, false, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(false, true, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(true, true, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public async Task Context_configuration_is_reset(bool useInterface, bool async, QueryTrackingBehavior? queryTrackingBehavior) { var serviceProvider = useInterface - ? BuildServiceProvider() - : BuildServiceProvider(); + ? BuildServiceProvider(b => UseQueryTrackingBehavior(b, queryTrackingBehavior)) + : BuildServiceProvider(b => UseQueryTrackingBehavior(b, queryTrackingBehavior)); var serviceScope = serviceProvider.CreateScope(); var scopedProvider = serviceScope.ServiceProvider; @@ -828,7 +872,7 @@ public async Task Context_configuration_is_reset(bool useInterface, bool async) Assert.False(context2!.ChangeTracker.AutoDetectChangesEnabled); Assert.False(context2.ChangeTracker.LazyLoadingEnabled); - Assert.Equal(QueryTrackingBehavior.TrackAll, context2.ChangeTracker.QueryTrackingBehavior); + Assert.Equal(queryTrackingBehavior ?? QueryTrackingBehavior.TrackAll, context2.ChangeTracker.QueryTrackingBehavior); Assert.Equal(CascadeTiming.Never, context2.ChangeTracker.CascadeDeleteTiming); Assert.Equal(CascadeTiming.Never, context2.ChangeTracker.DeleteOrphansTiming); Assert.Equal(AutoTransactionBehavior.Never, context2.Database.AutoTransactionBehavior); @@ -869,11 +913,17 @@ public async Task Context_configuration_is_reset(bool useInterface, bool async) } [ConditionalTheory] - [InlineData(false)] - [InlineData(true)] - public async Task Uninitialized_context_configuration_is_reset_properly(bool async) + [InlineData(false, null)] + [InlineData(true, null)] + [InlineData(false, QueryTrackingBehavior.TrackAll)] + [InlineData(true, QueryTrackingBehavior.TrackAll)] + [InlineData(false, QueryTrackingBehavior.NoTracking)] + [InlineData(true, QueryTrackingBehavior.NoTracking)] + [InlineData(false, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(true, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public async Task Uninitialized_context_configuration_is_reset_properly(bool async, QueryTrackingBehavior? queryTrackingBehavior) { - var serviceProvider = BuildServiceProvider(); + var serviceProvider = BuildServiceProvider(b => UseQueryTrackingBehavior(b, queryTrackingBehavior)); var serviceScope = serviceProvider.CreateScope(); var ctx = serviceScope.ServiceProvider.GetRequiredService(); @@ -1122,11 +1172,17 @@ private void ChangeTracker_OnDetectedEntityChanges(object sender, DetectedEntity => _changeTracker_OnDetectedEntityChanges = true; [ConditionalTheory] - [InlineData(false)] - [InlineData(true)] - public async Task Default_Context_configuration_is_reset(bool async) + [InlineData(false, null)] + [InlineData(true, null)] + [InlineData(false, QueryTrackingBehavior.TrackAll)] + [InlineData(true, QueryTrackingBehavior.TrackAll)] + [InlineData(false, QueryTrackingBehavior.NoTracking)] + [InlineData(true, QueryTrackingBehavior.NoTracking)] + [InlineData(false, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + [InlineData(true, QueryTrackingBehavior.NoTrackingWithIdentityResolution)] + public async Task Default_Context_configuration_is_reset(bool async, QueryTrackingBehavior? queryTrackingBehavior) { - var serviceProvider = BuildServiceProvider(); + var serviceProvider = BuildServiceProvider(b => UseQueryTrackingBehavior(b, queryTrackingBehavior)); var serviceScope = serviceProvider.CreateScope(); var scopedProvider = serviceScope.ServiceProvider; @@ -1152,7 +1208,7 @@ public async Task Default_Context_configuration_is_reset(bool async) Assert.True(context2!.ChangeTracker.AutoDetectChangesEnabled); Assert.True(context2.ChangeTracker.LazyLoadingEnabled); - Assert.Equal(QueryTrackingBehavior.TrackAll, context2.ChangeTracker.QueryTrackingBehavior); + Assert.Equal(queryTrackingBehavior ?? QueryTrackingBehavior.TrackAll, context2.ChangeTracker.QueryTrackingBehavior); Assert.Equal(CascadeTiming.Immediate, context2.ChangeTracker.CascadeDeleteTiming); Assert.Equal(CascadeTiming.Immediate, context2.ChangeTracker.DeleteOrphansTiming); Assert.Equal(AutoTransactionBehavior.WhenNeeded, context2.Database.AutoTransactionBehavior); @@ -1441,7 +1497,7 @@ public async Task Provider_services_are_reset(bool useInterface, bool async) { var serviceProvider = useInterface ? BuildServiceProvider() - : BuildServiceProvider(); + : BuildServiceProvider(o => o.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)); var serviceScope = serviceProvider.CreateScope(); var scopedProvider = serviceScope.ServiceProvider; @@ -1968,6 +2024,14 @@ await Task.WhenAll( }))); } + private void UseQueryTrackingBehavior(DbContextOptionsBuilder optionsBuilder, QueryTrackingBehavior? queryTrackingBehavior) + { + if (queryTrackingBehavior.HasValue) + { + optionsBuilder.UseQueryTrackingBehavior(queryTrackingBehavior.Value); + } + } + private async Task Dispose(IDisposable disposable, bool async) { if (async)