using System; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using EntityFrameworkCore.Jet.Data; using EntityFrameworkCore.Jet.FunctionalTests.Query; using EntityFrameworkCore.Jet.FunctionalTests.TestUtilities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.Extensions.DependencyInjection; using Xunit; using Xunit.Abstractions; // ReSharper disable MethodHasAsyncOverload // ReSharper disable InconsistentNaming // ReSharper disable UnusedAutoPropertyAccessor.Local // ReSharper disable UnusedMember.Local // ReSharper disable ClassNeverInstantiated.Local // ReSharper disable VirtualMemberCallInConstructor namespace EntityFrameworkCore.Jet.FunctionalTests; public class DbContextPoolingTest : IClassFixture> { private static DbContextOptionsBuilder ConfigureOptions(DbContextOptionsBuilder optionsBuilder) where TContext : DbContext => optionsBuilder .UseJet(JetNorthwindTestStoreFactory.NorthwindConnectionString) .EnableServiceProviderCaching(false); private static DbContextOptionsBuilder ConfigureOptions(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .UseJet(JetNorthwindTestStoreFactory.NorthwindConnectionString) .EnableServiceProviderCaching(false); private static IServiceProvider BuildServiceProvider(Action optionsAction = null) where TContextService : class where TContext : DbContext, TContextService => new ServiceCollection() .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(Action optionsAction = null) where TContext : DbContext => new ServiceCollection() .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() where TContext : DbContext => new ServiceCollection() .AddPooledDbContextFactory(ob => ConfigureOptions(ob)) .AddDbContextPool(ob => ConfigureOptions(ob)) .BuildServiceProvider(validateScopes: true); private static IServiceProvider BuildServiceProvider(int poolSize) where TContextService : class where TContext : DbContext, TContextService => new ServiceCollection() .AddDbContextPool(ob => ConfigureOptions(ob), poolSize) .AddDbContextPool(ob => ConfigureOptions(ob), poolSize) .BuildServiceProvider(validateScopes: true); private static IServiceProvider BuildServiceProvider(int poolSize) where TContext : DbContext => new ServiceCollection() .AddDbContextPool(ob => ConfigureOptions(ob), poolSize) .AddDbContextPool(ob => ConfigureOptions(ob), poolSize) .BuildServiceProvider(validateScopes: true); private static IServiceProvider BuildServiceProviderWithFactory(int poolSize) where TContext : DbContext => new ServiceCollection() .AddPooledDbContextFactory(ob => ConfigureOptions(ob), poolSize) .AddDbContextPool(ob => ConfigureOptions(ob), poolSize) .BuildServiceProvider(validateScopes: true); private static IDbContextFactory BuildFactory(bool withDependencyInjection) where TContext : DbContext => withDependencyInjection ? BuildServiceProviderWithFactory().GetService>() : new PooledDbContextFactory(ConfigureOptions(new DbContextOptionsBuilder()).Options); private static IDbContextFactory BuildFactory(bool withDependencyInjection, int poolSize) where TContext : DbContext => withDependencyInjection ? BuildServiceProviderWithFactory(poolSize).GetService>() : new PooledDbContextFactory(ConfigureOptions(new DbContextOptionsBuilder()).Options, poolSize); private interface IPooledContext { } private class DefaultOptionsPooledContext : DbContext { public DefaultOptionsPooledContext(DbContextOptions options) : base(options) { } } private class PooledContext : DbContext, IPooledContext { public static int DisposedCount; public static int InstanceCount; public static bool ModifyOptions; public PooledContext(DbContextOptions options) : base(options) { Interlocked.Increment(ref InstanceCount); ChangeTracker.AutoDetectChangesEnabled = false; ChangeTracker.LazyLoadingEnabled = false; Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; Database.AutoSavepointsEnabled = false; ChangeTracker.CascadeDeleteTiming = CascadeTiming.Never; ChangeTracker.DeleteOrphansTiming = CascadeTiming.Never; SavingChanges += (sender, args) => { }; SavedChanges += (sender, args) => { }; SaveChangesFailed += (sender, args) => { }; ChangeTracker.Tracking += (sender, args) => { }; ChangeTracker.Tracked += (sender, args) => { }; ChangeTracker.StateChanging += (sender, args) => { }; ChangeTracker.StateChanged += (sender, args) => { }; ChangeTracker.DetectingAllChanges += (sender, args) => { }; ChangeTracker.DetectedAllChanges += (sender, args) => { }; ChangeTracker.DetectingEntityChanges += (sender, args) => { }; ChangeTracker.DetectedEntityChanges += (sender, args) => { }; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (ModifyOptions) { optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); } } public DbSet Customers { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().ToTable("Customers"); modelBuilder.Entity().ToTable("Orders"); } public override void Dispose() { base.Dispose(); Interlocked.Increment(ref DisposedCount); } } private class PooledContextWithOverrides : DbContext, IPooledContext { public PooledContextWithOverrides(DbContextOptions options) : base(options) { } public DbSet Customers { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().ToTable("Customers"); modelBuilder.Entity().ToTable("Orders"); } } public class Customer { public string CustomerId { get; set; } public string CompanyName { get; set; } public ILazyLoader LazyLoader { get; set; } public ObservableCollection Orders { get; } = new(); } public class Order { public int OrderId { get; set; } public ILazyLoader LazyLoader { get; set; } public string CustomerId { get; set; } public Customer Customer { get; set; } } private interface ISecondContext { } private class SecondContext : DbContext, ISecondContext { public DbSet Blogs { get; set; } public SecondContext(DbContextOptions options) : base(options) { } public class Blog { public int Id { get; set; } } } [ConditionalFact] public void Invalid_pool_size() { Assert.Throws( () => BuildServiceProvider(poolSize: 0)); Assert.Throws( () => BuildServiceProvider(poolSize: -1)); Assert.Throws( () => BuildServiceProvider(poolSize: 0)); Assert.Throws( () => BuildServiceProvider(poolSize: -1)); } [ConditionalTheory] [InlineData(false)] [InlineData(true)] public void Invalid_pool_size_with_factory(bool withDependencyInjection) { Assert.Throws( () => BuildFactory(withDependencyInjection, poolSize: 0)); Assert.Throws( () => BuildFactory(withDependencyInjection, poolSize: -1)); } [ConditionalFact] public void Validate_pool_size() { var serviceProvider = BuildServiceProvider(poolSize: 64); using var scope = serviceProvider.CreateScope(); Assert.Equal( 64, scope.ServiceProvider .GetRequiredService() .GetService() .FindExtension()!.MaxPoolSize); } [ConditionalFact] public void Validate_pool_size_with_service_interface() { var serviceProvider = BuildServiceProvider(poolSize: 64); using var scope = serviceProvider.CreateScope(); Assert.Equal( 64, ((DbContext)scope.ServiceProvider .GetRequiredService()) .GetService() .FindExtension()!.MaxPoolSize); } [ConditionalFact] public void Validate_pool_size_with_factory() { var serviceProvider = BuildServiceProviderWithFactory(poolSize: 64); using var context = serviceProvider.GetRequiredService>().CreateDbContext(); Assert.Equal( 64, context.GetService() .FindExtension()!.MaxPoolSize); } [ConditionalTheory] [InlineData(false)] [InlineData(true)] public void Validate_pool_size_behavior_with_factory(bool withDependencyInjection) { var factory = BuildFactory(withDependencyInjection, poolSize: 1); var (ctx1, ctx2) = (factory.CreateDbContext(), factory.CreateDbContext()); ctx1.Dispose(); ctx2.Dispose(); using var ctx3 = factory.CreateDbContext(); using var ctx4 = factory.CreateDbContext(); Assert.Same(ctx1, ctx3); Assert.NotSame(ctx2, ctx4); } [ConditionalFact] public void Validate_pool_size_default() { var serviceProvider = BuildServiceProvider(); using var scope = serviceProvider.CreateScope(); Assert.Equal( 1024, scope.ServiceProvider .GetRequiredService() .GetService() .FindExtension()!.MaxPoolSize); } [ConditionalFact] public void Validate_pool_size_with_service_interface_default() { var serviceProvider = BuildServiceProvider(); using var scope = serviceProvider.CreateScope(); Assert.Equal( 1024, ((DbContext)scope.ServiceProvider .GetRequiredService()) .GetService() .FindExtension()!.MaxPoolSize); } [ConditionalFact] public void Pool_can_get_context_by_concrete_type_even_when_service_interface_is_used() { var serviceProvider = BuildServiceProvider(); using var scope = serviceProvider.CreateScope(); Assert.Same( scope.ServiceProvider.GetRequiredService(), scope.ServiceProvider.GetRequiredService()); } [ConditionalFact] public void Validate_pool_size_with_factory_default() { var serviceProvider = BuildServiceProviderWithFactory(); using var context = serviceProvider.GetRequiredService>().CreateDbContext(); Assert.Equal( 1024, context.GetService() .FindExtension()!.MaxPoolSize); } [ConditionalTheory] [InlineData(true)] [InlineData(false)] public void Options_modified_in_on_configuring(bool useInterface) { var serviceProvider = useInterface ? BuildServiceProvider() : BuildServiceProvider(); var scopedProvider = serviceProvider.CreateScope().ServiceProvider; PooledContext.ModifyOptions = true; try { Assert.Throws( () => useInterface ? scopedProvider.GetService() : scopedProvider.GetService()); } finally { PooledContext.ModifyOptions = false; } } [ConditionalFact] public void Options_modified_in_on_configuring_with_factory() { var serviceProvider = BuildServiceProviderWithFactory(); var scopedProvider = serviceProvider.CreateScope().ServiceProvider; PooledContext.ModifyOptions = true; try { var factory = scopedProvider.GetService>(); Assert.Throws(() => factory!.CreateDbContext()); } finally { PooledContext.ModifyOptions = false; } } private class BadCtorContext : DbContext { } [ConditionalFact] public void Throws_when_used_with_parameterless_constructor_context() { var serviceCollection = new ServiceCollection(); Assert.Equal( CoreStrings.DbContextMissingConstructor(nameof(BadCtorContext)), Assert.Throws( () => serviceCollection.AddDbContextPool( _ => { })).Message); Assert.Equal( CoreStrings.DbContextMissingConstructor(nameof(BadCtorContext)), Assert.Throws( () => serviceCollection.AddDbContextPool( (_, __) => { })).Message); Assert.Equal( CoreStrings.DbContextMissingConstructor(nameof(BadCtorContext)), Assert.Throws( () => serviceCollection.AddPooledDbContextFactory( (_, __) => { })).Message); } [ConditionalFact] public void Throws_when_pooled_context_constructor_has_second_parameter_that_cannot_be_resolved_from_service_provider() { var serviceProvider = new ServiceCollection().AddDbContextPool(_ => { }) .BuildServiceProvider(validateScopes: true); using var scope = serviceProvider.CreateScope(); Assert.Throws(() => scope.ServiceProvider.GetService()); } private class TwoParameterConstructorContext : DbContext { public string StringParameter { get; } public TwoParameterConstructorContext(DbContextOptions options, string x) : base(options) { StringParameter = x; } } [ConditionalFact] public void Throws_when_pooled_context_constructor_has_single_parameter_that_cannot_be_resolved_from_service_provider() { var serviceProvider = new ServiceCollection().AddDbContextPool(_ => { }) .BuildServiceProvider(validateScopes: true); using var scope = serviceProvider.CreateScope(); Assert.Throws(() => scope.ServiceProvider.GetService()); } private class WrongParameterConstructorContext : DbContext { public WrongParameterConstructorContext(string x) : base(new DbContextOptions()) { } } [ConditionalFact] public void Throws_when_pooled_context_constructor_has_scoped_service() { var serviceProvider = new ServiceCollection() .AddDbContextPool(_ => { }) .AddScoped(sp => "string") .BuildServiceProvider(validateScopes: true); using var scope = serviceProvider.CreateScope(); Assert.Throws(() => scope.ServiceProvider.GetService()); } [ConditionalFact] public void Does_not_throw_when_pooled_context_constructor_has_singleton_service() { var serviceProvider = new ServiceCollection() .AddDbContextPool(_ => { }) .AddSingleton("string") .BuildServiceProvider(validateScopes: true); using var scope = serviceProvider.CreateScope(); var context = scope.ServiceProvider.GetService(); Assert.Equal("string", context.StringParameter); } [ConditionalFact] public void Does_not_throw_when_parameterless_and_correct_constructor() { var serviceProvider = new ServiceCollection().AddDbContextPool(_ => { }) .BuildServiceProvider(validateScopes: true); using var scope = serviceProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); Assert.Equal("Options", context.ConstructorUsed); } [ConditionalFact] public void Does_not_throw_when_parameterless_and_correct_constructor_using_factory_pool() { var serviceProvider = new ServiceCollection().AddPooledDbContextFactory(_ => { }) .BuildServiceProvider(validateScopes: true); using var scope = serviceProvider.CreateScope(); var factory = scope.ServiceProvider.GetRequiredService>(); using var context = factory.CreateDbContext(); Assert.Equal("Options", context.ConstructorUsed); } private class WithParameterlessConstructorContext : DbContext { public string ConstructorUsed { get; } public WithParameterlessConstructorContext() { ConstructorUsed = "Parameterless"; } public WithParameterlessConstructorContext(DbContextOptions options) : base(options) { ConstructorUsed = "Options"; } } [ConditionalTheory] [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] public async Task Can_pool_non_derived_context(bool useFactory, bool async) { var serviceProvider = useFactory ? BuildServiceProviderWithFactory() : BuildServiceProvider(); var serviceScope1 = serviceProvider.CreateScope(); var context1 = await GetContextAsync(serviceScope1); var serviceScope2 = serviceProvider.CreateScope(); var context2 = await GetContextAsync(serviceScope2); Assert.NotSame(context1, context2); var id1 = context1.ContextId; var id2 = context2.ContextId; Assert.NotEqual(default, id1.InstanceId); Assert.NotEqual(id1, id2); Assert.NotEqual(id1.InstanceId, id2.InstanceId); Assert.Equal(1, id1.Lease); Assert.Equal(1, id2.Lease); if (useFactory) { await Dispose(context1, async); } await Dispose(serviceScope1, async); await Dispose(serviceScope2, async); if (useFactory) { await Dispose(context2, async); } var id1d = context1.ContextId; var id2d = context2.ContextId; Assert.Equal(id1, id1d); Assert.Equal(id1.InstanceId, id1d.InstanceId); Assert.Equal(1, id1d.Lease); Assert.Equal(1, id2d.Lease); var serviceScope3 = serviceProvider.CreateScope(); var context3 = await GetContextAsync(serviceScope3); var id1r = context3.ContextId; Assert.Same(context1, context3); Assert.Equal(id1.InstanceId, id1r.InstanceId); Assert.NotEqual(default, id1r.InstanceId); Assert.NotEqual(id1, id1r); Assert.Equal(2, id1r.Lease); var serviceScope4 = serviceProvider.CreateScope(); var context4 = await GetContextAsync(serviceScope4); var id2r = context4.ContextId; Assert.Same(context2, context4); Assert.Equal(id2.InstanceId, id2r.InstanceId); Assert.NotEqual(default, id2r.InstanceId); Assert.NotEqual(id2, id2r); Assert.Equal(2, id2r.Lease); async Task GetContextAsync(IServiceScope serviceScope) => useFactory ? async ? await serviceScope.ServiceProvider.GetService>()!.CreateDbContextAsync() : serviceScope.ServiceProvider.GetService>()!.CreateDbContext() : serviceScope.ServiceProvider.GetService(); } [ConditionalTheory] [InlineData(false)] [InlineData(true)] public async Task ContextIds_make_sense_when_not_pooling(bool async) { var serviceProvider = new ServiceCollection() .AddDbContext( ob => ob.UseJet(JetNorthwindTestStoreFactory.NorthwindConnectionString) .EnableServiceProviderCaching(false)) .BuildServiceProvider(validateScopes: true); var serviceScope1 = serviceProvider.CreateScope(); var context1 = serviceScope1.ServiceProvider.GetService(); var serviceScope2 = serviceProvider.CreateScope(); var context2 = serviceScope2.ServiceProvider.GetService(); Assert.NotSame(context1, context2); var id1 = context1!.ContextId; var id2 = context2!.ContextId; Assert.NotEqual(default, id1.InstanceId); Assert.NotEqual(default, id2.InstanceId); Assert.NotEqual(id1, id2); Assert.Equal(0, id1.Lease); Assert.Equal(0, id2.Lease); await Dispose(serviceScope1, async); await Dispose(serviceScope2, async); var id1d = context1.ContextId; var id2d = context2.ContextId; Assert.Equal(id1.InstanceId, id1d.InstanceId); Assert.Equal(id2.InstanceId, id2d.InstanceId); Assert.Equal(0, id1d.Lease); Assert.Equal(0, id2d.Lease); var serviceScope3 = serviceProvider.CreateScope(); var context3 = serviceScope3.ServiceProvider.GetService(); var id1r = context3.ContextId; Assert.NotSame(context1, context3); Assert.NotEqual(default, id1r.InstanceId); Assert.NotEqual(id1.InstanceId, id1r.InstanceId); Assert.NotEqual(id1, id1r); Assert.Equal(0, id1r.Lease); var serviceScope4 = serviceProvider.CreateScope(); var context4 = serviceScope4.ServiceProvider.GetService(); var id2r = context4.ContextId; Assert.NotSame(context2, context4); Assert.NotEqual(default, id2r.InstanceId); Assert.NotEqual(id2.InstanceId, id2r.InstanceId); Assert.NotEqual(id2, id2r); Assert.Equal(0, id2r.Lease); } [ConditionalTheory] [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] public async Task Contexts_are_pooled(bool useInterface, bool async) { var serviceProvider = useInterface ? BuildServiceProvider() : BuildServiceProvider(); var serviceScope1 = serviceProvider.CreateScope(); var scopedProvider1 = serviceScope1.ServiceProvider; var context1 = useInterface ? scopedProvider1.GetService() : scopedProvider1.GetService(); var secondContext1 = useInterface ? scopedProvider1.GetService() : scopedProvider1.GetService(); var serviceScope2 = serviceProvider.CreateScope(); var scopedProvider2 = serviceScope2.ServiceProvider; var context2 = useInterface ? scopedProvider2.GetService() : scopedProvider2.GetService(); var secondContext2 = useInterface ? scopedProvider2.GetService() : scopedProvider2.GetService(); Assert.NotSame(context1, context2); Assert.NotSame(secondContext1, secondContext2); await Dispose(serviceScope1, async); await Dispose(serviceScope2, async); var serviceScope3 = serviceProvider.CreateScope(); var scopedProvider3 = serviceScope3.ServiceProvider; var context3 = useInterface ? scopedProvider3.GetService() : scopedProvider3.GetService(); var secondContext3 = useInterface ? scopedProvider3.GetService() : scopedProvider3.GetService(); Assert.Same(context1, context3); Assert.Same(secondContext1, secondContext3); var serviceScope4 = serviceProvider.CreateScope(); var scopedProvider4 = serviceScope4.ServiceProvider; var context4 = useInterface ? scopedProvider4.GetService() : scopedProvider4.GetService(); var secondContext4 = useInterface ? scopedProvider4.GetService() : scopedProvider4.GetService(); Assert.Same(context2, context4); Assert.Same(secondContext2, secondContext4); await Dispose(serviceScope3, async); await Dispose(serviceScope4, async); } [ConditionalTheory] [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] public async Task Contexts_are_pooled_with_factory(bool async, bool withDependencyInjection) { var factory = BuildFactory(withDependencyInjection); var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); var secondContext1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); var context2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); var secondContext2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); Assert.NotSame(context1, context2); Assert.NotSame(secondContext1, secondContext2); await Dispose(context1, async); await Dispose(secondContext1, async); await Dispose(context2, async); await Dispose(secondContext2, async); var context3 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); var secondContext3 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); Assert.Same(context1, context3); Assert.Same(secondContext1, secondContext3); var context4 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); var secondContext4 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); Assert.Same(context2, context4); Assert.Same(secondContext2, secondContext4); await Dispose(context1, async); await Dispose(secondContext1, async); await Dispose(context2, async); await Dispose(secondContext2, async); } [ConditionalTheory] [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(b => UseQueryTrackingBehavior(b, queryTrackingBehavior)) : BuildServiceProvider(b => UseQueryTrackingBehavior(b, queryTrackingBehavior)); var serviceScope = serviceProvider.CreateScope(); var scopedProvider = serviceScope.ServiceProvider; var context1 = useInterface ? (PooledContext)scopedProvider.GetService() : scopedProvider.GetService(); 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; context1.ChangeTracker.CascadeDeleteTiming = CascadeTiming.Immediate; context1.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Immediate; context1.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; context1.Database.AutoSavepointsEnabled = true; context1.Database.SetCommandTimeout(1); context1.ChangeTracker.Tracking += ChangeTracker_OnTracking; context1.ChangeTracker.Tracked += ChangeTracker_OnTracked; context1.ChangeTracker.StateChanging += ChangeTracker_OnStateChanging; context1.ChangeTracker.StateChanged += ChangeTracker_OnStateChanged; context1.ChangeTracker.DetectingAllChanges += ChangeTracker_OnDetectingAllChanges; context1.ChangeTracker.DetectedAllChanges += ChangeTracker_OnDetectedAllChanges; context1.ChangeTracker.DetectingEntityChanges += ChangeTracker_OnDetectingEntityChanges; context1.ChangeTracker.DetectedEntityChanges += ChangeTracker_OnDetectedEntityChanges; context1.SavingChanges += Context_OnSavingChanges; context1.SavedChanges += Context_OnSavedChanges; context1.SaveChangesFailed += Context_OnSaveChangesFailed; _localView_OnPropertyChanging = false; _localView_OnPropertyChanged = false; _localView_OnCollectionChanged = false; await Dispose(serviceScope, async); serviceScope = serviceProvider.CreateScope(); scopedProvider = serviceScope.ServiceProvider; var context2 = useInterface ? (PooledContext)scopedProvider.GetService() : scopedProvider.GetService(); Assert.Same(context1, context2); Assert.False(context2!.ChangeTracker.AutoDetectChangesEnabled); Assert.False(context2.ChangeTracker.LazyLoadingEnabled); 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); Assert.False(context2.Database.AutoSavepointsEnabled); Assert.Null(context1.Database.GetCommandTimeout()); 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); Assert.False(_changeTracker_OnStateChanging); Assert.False(_changeTracker_OnStateChanged); context2.SaveChanges(); Assert.False(_changeTracker_OnDetectingAllChanges); Assert.False(_changeTracker_OnDetectedAllChanges); Assert.False(_changeTracker_OnDetectingEntityChanges); Assert.False(_changeTracker_OnDetectedEntityChanges); 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] [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(b => UseQueryTrackingBehavior(b, queryTrackingBehavior)); var serviceScope = serviceProvider.CreateScope(); var ctx = serviceScope.ServiceProvider.GetRequiredService(); await Dispose(ctx, async); await Dispose(serviceScope, async); serviceScope = serviceProvider.CreateScope(); var ctx2 = serviceScope.ServiceProvider.GetRequiredService(); Assert.Same(ctx, ctx2); ctx2.Blogs.Add(new SecondContext.Blog()); await Dispose(ctx2, async); await Dispose(serviceScope, async); serviceScope = serviceProvider.CreateScope(); var ctx3 = serviceScope.ServiceProvider.GetRequiredService(); Assert.Same(ctx, ctx3); Assert.Empty(ctx3.ChangeTracker.Entries()); await Dispose(ctx2, async); await Dispose(serviceScope, async); } [ConditionalTheory] [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] public async Task Context_configuration_is_reset_with_factory(bool async, bool withDependencyInjection) { var factory = BuildFactory(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; context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; context1.ChangeTracker.CascadeDeleteTiming = CascadeTiming.Immediate; context1.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Immediate; context1.Database.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; context1.Database.AutoSavepointsEnabled = true; context1.ChangeTracker.Tracking += ChangeTracker_OnTracking; context1.ChangeTracker.Tracked += ChangeTracker_OnTracked; context1.ChangeTracker.StateChanging += ChangeTracker_OnStateChanging; context1.ChangeTracker.StateChanged += ChangeTracker_OnStateChanged; context1.ChangeTracker.DetectingAllChanges += ChangeTracker_OnDetectingAllChanges; context1.ChangeTracker.DetectedAllChanges += ChangeTracker_OnDetectedAllChanges; context1.ChangeTracker.DetectingEntityChanges += ChangeTracker_OnDetectingEntityChanges; context1.ChangeTracker.DetectedEntityChanges += ChangeTracker_OnDetectedEntityChanges; context1.SavingChanges += Context_OnSavingChanges; 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); 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); Assert.False(_changeTracker_OnStateChanging); Assert.False(_changeTracker_OnStateChanged); context2.SaveChanges(); Assert.False(_changeTracker_OnDetectingAllChanges); Assert.False(_changeTracker_OnDetectedAllChanges); Assert.False(_changeTracker_OnDetectingEntityChanges); Assert.False(_changeTracker_OnDetectedEntityChanges); 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] public void Change_tracker_can_be_cleared_without_resetting_context_config() { var context = new PooledContext( new DbContextOptionsBuilder().UseJet( JetNorthwindTestStoreFactory.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.AutoTransactionBehavior = AutoTransactionBehavior.WhenNeeded; context.Database.AutoSavepointsEnabled = true; context.ChangeTracker.Tracking += ChangeTracker_OnTracking; context.ChangeTracker.Tracked += ChangeTracker_OnTracked; context.ChangeTracker.StateChanging += ChangeTracker_OnStateChanging; context.ChangeTracker.StateChanged += ChangeTracker_OnStateChanged; context.ChangeTracker.DetectingAllChanges += ChangeTracker_OnDetectingAllChanges; context.ChangeTracker.DetectedAllChanges += ChangeTracker_OnDetectedAllChanges; context.ChangeTracker.DetectingEntityChanges += ChangeTracker_OnDetectingEntityChanges; context.ChangeTracker.DetectedEntityChanges += ChangeTracker_OnDetectedEntityChanges; context.SavingChanges += Context_OnSavingChanges; context.SavedChanges += Context_OnSavedChanges; context.SaveChangesFailed += Context_OnSaveChangesFailed; 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.Equal(AutoTransactionBehavior.WhenNeeded, context.Database.AutoTransactionBehavior); Assert.True(context.Database.AutoSavepointsEnabled); Assert.False(_changeTracker_OnTracking); Assert.False(_changeTracker_OnTracked); Assert.False(_changeTracker_OnStateChanging); Assert.False(_changeTracker_OnStateChanged); Assert.False(_changeTracker_OnDetectingAllChanges); Assert.False(_changeTracker_OnDetectedAllChanges); Assert.False(_changeTracker_OnDetectingEntityChanges); Assert.False(_changeTracker_OnDetectedEntityChanges); var customer = new Customer { CustomerId = "C" }; context.Customers.Attach(customer).State = EntityState.Modified; context.Customers.Attach(customer).State = EntityState.Unchanged; Assert.True(_changeTracker_OnTracking); Assert.True(_changeTracker_OnTracked); Assert.True(_changeTracker_OnStateChanging); Assert.True(_changeTracker_OnStateChanged); Assert.False(_changeTracker_OnDetectingAllChanges); Assert.False(_changeTracker_OnDetectedAllChanges); Assert.False(_changeTracker_OnDetectingEntityChanges); Assert.False(_changeTracker_OnDetectedEntityChanges); context.SaveChanges(); Assert.True(_changeTracker_OnDetectingAllChanges); Assert.True(_changeTracker_OnDetectedAllChanges); Assert.True(_changeTracker_OnDetectingEntityChanges); Assert.True(_changeTracker_OnDetectedEntityChanges); Assert.True(_context_OnSavedChanges); Assert.True(_context_OnSavingChanges); Assert.False(_context_OnSaveChangesFailed); } private void Context_OnSavingChanges(object sender, SavingChangesEventArgs e) => _context_OnSavingChanges = true; private bool _context_OnSavingChanges; private void Context_OnSavedChanges(object sender, SavedChangesEventArgs e) => _context_OnSavedChanges = true; private bool _context_OnSavedChanges; private void Context_OnSaveChangesFailed(object sender, SaveChangesFailedEventArgs e) => _context_OnSaveChangesFailed = true; 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) => _changeTracker_OnTracking = true; private bool _changeTracker_OnTracked; private void ChangeTracker_OnTracked(object sender, EntityTrackedEventArgs e) => _changeTracker_OnTracked = true; private bool _changeTracker_OnStateChanging; private void ChangeTracker_OnStateChanging(object sender, EntityStateChangingEventArgs e) => _changeTracker_OnStateChanging = true; private bool _changeTracker_OnStateChanged; private void ChangeTracker_OnStateChanged(object sender, EntityStateChangedEventArgs e) => _changeTracker_OnStateChanged = true; private bool _changeTracker_OnDetectingAllChanges; private void ChangeTracker_OnDetectingAllChanges(object sender, DetectChangesEventArgs e) => _changeTracker_OnDetectingAllChanges = true; private bool _changeTracker_OnDetectedAllChanges; private void ChangeTracker_OnDetectedAllChanges(object sender, DetectedChangesEventArgs e) => _changeTracker_OnDetectedAllChanges = true; private bool _changeTracker_OnDetectingEntityChanges; private void ChangeTracker_OnDetectingEntityChanges(object sender, DetectEntityChangesEventArgs e) => _changeTracker_OnDetectingEntityChanges = true; private bool _changeTracker_OnDetectedEntityChanges; private void ChangeTracker_OnDetectedEntityChanges(object sender, DetectedEntityChangesEventArgs e) => _changeTracker_OnDetectedEntityChanges = true; [ConditionalTheory] [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(b => UseQueryTrackingBehavior(b, queryTrackingBehavior)); var serviceScope = serviceProvider.CreateScope(); var scopedProvider = serviceScope.ServiceProvider; var context1 = scopedProvider.GetService(); context1!.ChangeTracker.AutoDetectChangesEnabled = false; context1.ChangeTracker.LazyLoadingEnabled = false; context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; context1.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; context1.Database.AutoSavepointsEnabled = false; context1.ChangeTracker.CascadeDeleteTiming = CascadeTiming.Immediate; context1.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Immediate; await Dispose(serviceScope, async); serviceScope = serviceProvider.CreateScope(); scopedProvider = serviceScope.ServiceProvider; var context2 = scopedProvider.GetService(); Assert.Same(context1, context2); Assert.True(context2!.ChangeTracker.AutoDetectChangesEnabled); Assert.True(context2.ChangeTracker.LazyLoadingEnabled); 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); Assert.True(context2.Database.AutoSavepointsEnabled); } [ConditionalTheory] [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] public async Task Default_Context_configuration_is_reset_with_factory(bool async, bool withDependencyInjection) { var factory = BuildFactory(withDependencyInjection); var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); context1.ChangeTracker.AutoDetectChangesEnabled = false; context1.ChangeTracker.LazyLoadingEnabled = false; context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; context1.Database.AutoTransactionBehavior = AutoTransactionBehavior.Never; context1.Database.AutoSavepointsEnabled = false; context1.ChangeTracker.CascadeDeleteTiming = CascadeTiming.Immediate; context1.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Immediate; await Dispose(context1, async); var context2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); Assert.Same(context1, context2); Assert.True(context2.ChangeTracker.AutoDetectChangesEnabled); Assert.True(context2.ChangeTracker.LazyLoadingEnabled); Assert.Equal(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); Assert.True(context2.Database.AutoSavepointsEnabled); } [ConditionalTheory] [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] public async Task State_manager_is_reset(bool useInterface, bool async) { var weakRef = await Scoper( async () => { var serviceProvider = useInterface ? BuildServiceProvider() : BuildServiceProvider(); var serviceScope = serviceProvider.CreateScope(); var scopedProvider = serviceScope.ServiceProvider; var context1 = useInterface ? (PooledContext)scopedProvider.GetService() : scopedProvider.GetService(); var entity = context1.Customers.First(c => c.CustomerId == "ALFKI"); Assert.Single(context1.ChangeTracker.Entries()); await Dispose(serviceScope, async); serviceScope = serviceProvider.CreateScope(); scopedProvider = serviceScope.ServiceProvider; var context2 = useInterface ? (PooledContext)scopedProvider.GetService() : scopedProvider.GetService(); Assert.Same(context1, context2); Assert.Empty(context2.ChangeTracker.Entries()); return new WeakReference(entity); }); GC.Collect(); Assert.False(weakRef.IsAlive); } private static async Task Scoper(Func> getter) => await getter(); [ConditionalTheory] [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] public async Task State_manager_is_reset_with_factory(bool async, bool withDependencyInjection) { var weakRef = await Scoper( async () => { var factory = BuildFactory(withDependencyInjection); var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); var entity = context1.Customers.First(c => c.CustomerId == "ALFKI"); Assert.Single(context1.ChangeTracker.Entries()); await Dispose(context1, async); var context2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); Assert.Same(context1, context2); Assert.Empty(context2.ChangeTracker.Entries()); return new WeakReference(entity); }); GC.Collect(); Assert.False(weakRef.IsAlive); } [ConditionalTheory] // Issue #25486 [InlineData(false, false, false)] [InlineData(true, false, false)] [InlineData(false, true, false)] [InlineData(true, true, false)] [InlineData(false, false, true)] [InlineData(true, false, true)] [InlineData(false, true, true)] [InlineData(true, true, true)] public async Task Service_properties_are_disposed(bool useInterface, bool async, bool load) { var serviceProvider = useInterface ? BuildServiceProvider() : BuildServiceProvider(); var serviceScope = serviceProvider.CreateScope(); var scopedProvider = serviceScope.ServiceProvider; var context1 = useInterface ? (PooledContext)scopedProvider.GetService() : scopedProvider.GetService(); context1.ChangeTracker.LazyLoadingEnabled = true; var entity = context1.Customers.First(c => c.CustomerId == "ALFKI"); var orderLoader = entity.LazyLoader; if (load) { orderLoader.Load(entity, nameof(Customer.Orders)); Assert.True(orderLoader.IsLoaded(entity, nameof(Customer.Orders))); } Assert.Equal(load ? 7 : 1, context1.ChangeTracker.Entries().Count()); await Dispose(serviceScope, async); if (load) { orderLoader.Load(entity, nameof(Customer.Orders)); Assert.True(orderLoader.IsLoaded(entity, nameof(Customer.Orders))); orderLoader.SetLoaded(entity, nameof(Customer.Orders), loaded: false); } AssertDisposed(() => orderLoader.Load(entity, nameof(Customer.Orders)), "Customer", "Orders"); } [ConditionalTheory] // Issue #25486 [InlineData(false, false, false)] [InlineData(true, false, false)] [InlineData(false, true, false)] [InlineData(true, true, false)] [InlineData(false, false, true)] [InlineData(true, false, true)] [InlineData(false, true, true)] [InlineData(true, true, true)] public async Task Service_properties_are_disposed_with_factory(bool async, bool withDependencyInjection, bool load) { var factory = BuildFactory(withDependencyInjection); var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); context1.ChangeTracker.LazyLoadingEnabled = true; var entity = context1.Customers.First(c => c.CustomerId == "ALFKI"); var orderLoader = entity.LazyLoader; if (load) { orderLoader.Load(entity, nameof(Customer.Orders)); Assert.True(orderLoader.IsLoaded(entity, nameof(Customer.Orders))); } Assert.Equal(load ? 7 : 1, context1.ChangeTracker.Entries().Count()); await Dispose(context1, async); if (load) { orderLoader.Load(entity, nameof(Customer.Orders)); Assert.True(orderLoader.IsLoaded(entity, nameof(Customer.Orders))); orderLoader.SetLoaded(entity, nameof(Customer.Orders), loaded: false); } AssertDisposed(() => orderLoader.Load(entity, nameof(Customer.Orders)), "Customer", "Orders"); } private static void AssertDisposed(Action testCode, string entityTypeName, string navigationName) => Assert.Equal( CoreStrings.WarningAsErrorTemplate( CoreEventId.LazyLoadOnDisposedContextWarning.ToString(), CoreResources.LogLazyLoadOnDisposedContext(new TestLogger()) .GenerateMessage(entityTypeName, navigationName), "CoreEventId.LazyLoadOnDisposedContextWarning"), Assert.Throws( testCode).Message); [ConditionalTheory] [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] public async Task Pool_disposes_context_when_context_not_pooled(bool useInterface, bool async) { var serviceProvider = useInterface ? BuildServiceProvider() : BuildServiceProvider(); var serviceScope1 = serviceProvider.CreateScope(); var scopedProvider1 = serviceScope1.ServiceProvider; var context1 = useInterface ? (PooledContext)scopedProvider1.GetService() : scopedProvider1.GetService(); var serviceScope2 = serviceProvider.CreateScope(); var scopedProvider2 = serviceScope2.ServiceProvider; var context2 = useInterface ? (PooledContext)scopedProvider2.GetService() : scopedProvider2.GetService(); await Dispose(serviceScope1, async); await Dispose(serviceScope2, async); Assert.Throws(() => context1.Customers.ToList()); Assert.Throws(() => context2.Customers.ToList()); } [ConditionalTheory] [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] public async Task Pool_disposes_contexts_when_disposed(bool useInterface, bool async) { var serviceProvider = useInterface ? BuildServiceProvider() : BuildServiceProvider(); var serviceScope = serviceProvider.CreateScope(); var scopedProvider = serviceScope.ServiceProvider; var context = useInterface ? (PooledContext)scopedProvider.GetService() : scopedProvider.GetService(); await Dispose(serviceScope, async); await Dispose((IDisposable)serviceProvider, async); Assert.Throws(() => context.Customers.ToList()); } [ConditionalTheory] [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] public async Task Object_in_pool_is_disposed(bool useInterface, bool async) { var serviceProvider = useInterface ? BuildServiceProvider() : BuildServiceProvider(); var serviceScope = serviceProvider.CreateScope(); var scopedProvider = serviceScope.ServiceProvider; var context = useInterface ? (PooledContext)scopedProvider.GetService() : scopedProvider.GetService(); await Dispose(serviceScope, async); Assert.Throws(() => context!.Customers.ToList()); } [ConditionalTheory] [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] public async Task Double_dispose_does_not_enter_pool_twice(bool useInterface, bool async) { var serviceProvider = useInterface ? BuildServiceProvider() : BuildServiceProvider(); var scope = serviceProvider.CreateScope(); var lease = scope.ServiceProvider.GetRequiredService>(); var context = lease.Context; await Dispose(scope, async); await Dispose(scope, async); using var scope1 = serviceProvider.CreateScope(); var lease1 = scope1.ServiceProvider.GetRequiredService>(); using var scope2 = serviceProvider.CreateScope(); var lease2 = scope2.ServiceProvider.GetRequiredService>(); Assert.Same(context, lease1.Context); Assert.NotSame(lease1.Context, lease2.Context); } [ConditionalTheory] [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] public async Task Double_dispose_with_standalone_lease_does_not_enter_pool_twice(bool useInterface, bool async) { var serviceProvider = useInterface ? BuildServiceProvider() : BuildServiceProvider(); var pool = serviceProvider.GetRequiredService>(); var lease = new DbContextLease(pool, standalone: true); var context = (PooledContext)lease.Context; ((IDbContextPoolable)context).SetLease(lease); await Dispose(context, async); await Dispose(context, async); using var context1 = new DbContextLease(pool, standalone: true).Context; using var context2 = new DbContextLease(pool, standalone: true).Context; Assert.Same(context, context1); Assert.NotSame(context1, context2); } [ConditionalTheory] [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] public async Task Can_double_dispose_with_factory(bool async, bool withDependencyInjection) { var factory = BuildFactory(withDependencyInjection); var context = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); context.Customers.Load(); await Dispose(context, async); Assert.Throws(() => context.Customers.ToList()); await Dispose(context, async); Assert.Throws(() => context.Customers.ToList()); } [ConditionalTheory] [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] public async Task Provider_services_are_reset(bool useInterface, bool async) { var serviceProvider = useInterface ? BuildServiceProvider() : BuildServiceProvider(o => o.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking)); var serviceScope = serviceProvider.CreateScope(); var scopedProvider = serviceScope.ServiceProvider; var context1 = useInterface ? (PooledContext)scopedProvider.GetService() : scopedProvider.GetService(); context1!.Database.BeginTransaction(); Assert.NotNull(context1.Database.CurrentTransaction); await Dispose(serviceScope, async); serviceScope = serviceProvider.CreateScope(); scopedProvider = serviceScope.ServiceProvider; var context2 = useInterface ? (PooledContext)scopedProvider.GetService() : scopedProvider.GetService(); Assert.Same(context1, context2); Assert.Null(context2!.Database.CurrentTransaction); context2.Database.BeginTransaction(); Assert.NotNull(context2.Database.CurrentTransaction); await Dispose(serviceScope, async); serviceScope = serviceProvider.CreateScope(); scopedProvider = serviceScope.ServiceProvider; var context3 = useInterface ? (PooledContext)scopedProvider.GetService() : scopedProvider.GetService(); Assert.Same(context2, context3); Assert.Null(context3!.Database.CurrentTransaction); } [ConditionalTheory] [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] public async Task Provider_services_are_reset_with_factory(bool async, bool withDependencyInjection) { var factory = BuildFactory(withDependencyInjection); var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); context1.Database.BeginTransaction(); Assert.NotNull(context1.Database.CurrentTransaction); await Dispose(context1, async); var context2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); Assert.Same(context1, context2); Assert.Null(context2.Database.CurrentTransaction); context2.Database.BeginTransaction(); Assert.NotNull(context2.Database.CurrentTransaction); await Dispose(context2, async); var context3 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); Assert.Same(context2, context3); Assert.Null(context3.Database.CurrentTransaction); } [ConditionalTheory] // Issue #27308. [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] public async Task Handle_open_connection_when_returning_to_pool_for_owned_connection(bool async, bool openWithEf) { var serviceProvider = new ServiceCollection() .AddDbContextPool( ob => ob.UseJet(JetNorthwindTestStoreFactory.NorthwindConnectionString) .EnableServiceProviderCaching(false)) .BuildServiceProvider(validateScopes: true); var serviceScope = serviceProvider.CreateScope(); var scopedProvider = serviceScope.ServiceProvider; var context1 = scopedProvider.GetRequiredService(); var connection1 = context1.Database.GetDbConnection(); if (async) { if (openWithEf) { await context1.Database.OpenConnectionAsync(); } else { await connection1.OpenAsync(); } } else { if (openWithEf) { context1.Database.OpenConnection(); } else { connection1.Open(); } } Assert.Equal(ConnectionState.Open, connection1.State); await Dispose(serviceScope, async); Assert.Equal(ConnectionState.Closed, connection1.State); serviceScope = serviceProvider.CreateScope(); scopedProvider = serviceScope.ServiceProvider; var context2 = scopedProvider.GetRequiredService(); Assert.Same(context1, context2); var connection2 = context2.Database.GetDbConnection(); Assert.Same(connection1, connection2); Assert.Equal(ConnectionState.Closed, connection1.State); await Dispose(serviceScope, async); } [ConditionalTheory] // Issue #27308. [InlineData(false, false, false)] [InlineData(true, false, false)] [InlineData(false, true, false)] [InlineData(true, true, false)] [InlineData(false, false, true)] [InlineData(true, false, true)] public async Task Handle_open_connection_when_returning_to_pool_for_external_connection(bool async, bool startsOpen, bool openWithEf) { using var connection = new JetConnection(JetNorthwindTestStoreFactory.NorthwindConnectionString); if (startsOpen) { if (async) { await connection.OpenAsync(); } else { connection.Open(); } } var serviceProvider = new ServiceCollection() .AddDbContextPool( ob => ob.UseJet(connection) .EnableServiceProviderCaching(false)) .BuildServiceProvider(validateScopes: true); var serviceScope = serviceProvider.CreateScope(); var scopedProvider = serviceScope.ServiceProvider; var context1 = scopedProvider.GetRequiredService(); Assert.Same(connection, context1.Database.GetDbConnection()); if (!startsOpen) { if (async) { if (openWithEf) { await context1.Database.OpenConnectionAsync(); } else { await connection.OpenAsync(); } } else { if (openWithEf) { context1.Database.OpenConnection(); } else { connection.Open(); } } } Assert.Equal(ConnectionState.Open, connection.State); await Dispose(serviceScope, async); Assert.Equal(ConnectionState.Open, connection.State); serviceScope = serviceProvider.CreateScope(); scopedProvider = serviceScope.ServiceProvider; var context2 = scopedProvider.GetRequiredService(); Assert.Same(context1, context2); Assert.Same(connection, context2.Database.GetDbConnection()); Assert.Equal(ConnectionState.Open, connection.State); await Dispose(serviceScope, async); } [ConditionalTheory] // Issue #27308. [InlineData(false, false, false)] [InlineData(true, false, false)] [InlineData(false, true, false)] [InlineData(true, true, false)] [InlineData(false, false, true)] [InlineData(true, false, true)] [InlineData(false, true, true)] [InlineData(true, true, true)] public async Task Handle_open_connection_when_returning_to_pool_for_owned_connection_with_factory( bool async, bool openWithEf, bool withDependencyInjection) { var options = new DbContextOptionsBuilder() .UseJet(JetNorthwindTestStoreFactory.NorthwindConnectionString) .EnableServiceProviderCaching(false) .Options; var factory = withDependencyInjection ? new ServiceCollection() .AddPooledDbContextFactory( ob => ob.UseJet(JetNorthwindTestStoreFactory.NorthwindConnectionString) .EnableServiceProviderCaching(false)) .BuildServiceProvider(validateScopes: true) .GetRequiredService>() : new PooledDbContextFactory(options); var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); var connection1 = context1.Database.GetDbConnection(); if (async) { if (openWithEf) { await context1.Database.OpenConnectionAsync(); } else { await connection1.OpenAsync(); } } else { if (openWithEf) { context1.Database.OpenConnection(); } else { connection1.Open(); } } Assert.Equal(ConnectionState.Open, connection1.State); await Dispose(context1, async); Assert.Equal(ConnectionState.Closed, connection1.State); var context2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); Assert.Same(context1, context2); var connection2 = context2.Database.GetDbConnection(); Assert.Same(connection1, connection2); Assert.Equal(ConnectionState.Closed, connection1.State); await Dispose(context2, async); } [ConditionalTheory] // Issue #27308. [InlineData(false, false, false, false)] [InlineData(true, false, false, false)] [InlineData(false, true, false, false)] [InlineData(true, true, false, false)] [InlineData(false, false, true, false)] [InlineData(true, false, true, false)] [InlineData(false, false, false, true)] [InlineData(true, false, false, true)] [InlineData(false, true, false, true)] [InlineData(true, true, false, true)] [InlineData(false, false, true, true)] [InlineData(true, false, true, true)] public async Task Handle_open_connection_when_returning_to_pool_for_external_connection_with_factory( bool async, bool startsOpen, bool openWithEf, bool withDependencyInjection) { using var connection = new JetConnection(JetNorthwindTestStoreFactory.NorthwindConnectionString); if (startsOpen) { if (async) { await connection.OpenAsync(); } else { connection.Open(); } } var options = new DbContextOptionsBuilder() .UseJet(connection) .EnableServiceProviderCaching(false) .Options; var factory = withDependencyInjection ? new ServiceCollection() .AddPooledDbContextFactory( ob => ob.UseJet(connection) .EnableServiceProviderCaching(false)) .BuildServiceProvider(validateScopes: true) .GetRequiredService>() : new PooledDbContextFactory(options); var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); Assert.Same(connection, context1.Database.GetDbConnection()); if (!startsOpen) { if (async) { if (openWithEf) { await context1.Database.OpenConnectionAsync(); } else { await connection.OpenAsync(); } } else { if (openWithEf) { context1.Database.OpenConnection(); } else { connection.Open(); } } } Assert.Equal(ConnectionState.Open, connection.State); await Dispose(context1, async); Assert.Equal(ConnectionState.Open, connection.State); var context2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); Assert.Same(context1, context2); Assert.Same(connection, context2.Database.GetDbConnection()); Assert.Equal(ConnectionState.Open, connection.State); await Dispose(context2, async); } [ConditionalTheory] [InlineData(true)] [InlineData(false)] public void Double_dispose_concurrency_test(bool useInterface) { var serviceProvider = useInterface ? BuildServiceProvider() : BuildServiceProvider(); Parallel.For( fromInclusive: 0, toExclusive: 32, body: s => { using var scope = serviceProvider.CreateScope(); var scopedProvider = scope.ServiceProvider; var context = useInterface ? (PooledContext)scopedProvider.GetService() : scopedProvider.GetService(); var _ = context.Customers.ToList(); context.Dispose(); }); } [ConditionalTheory] [InlineData(false, false)] [InlineData(true, false)] [InlineData(false, true)] [InlineData(true, true)] public async Task Concurrency_test(bool useInterface, bool async) { PooledContext.InstanceCount = 0; PooledContext.DisposedCount = 0; var results = WriteResults(); var serviceProvider = useInterface ? BuildServiceProvider() : BuildServiceProvider(); async Task ProcessRequest() { while (_stopwatch.IsRunning) { using var serviceScope = serviceProvider.CreateScope(); var scopedProvider = serviceScope.ServiceProvider; var context = useInterface ? (PooledContext)scopedProvider.GetService() : scopedProvider.GetService(); await context!.Customers.AsNoTracking().FirstAsync(c => c.CustomerId == "ALFKI"); Interlocked.Increment(ref _requests); } } var tasks = new Task[32]; for (var i = 0; i < 32; i++) { tasks[i] = ProcessRequest(); } await Task.WhenAll(tasks); await results; Assert.Equal(_requests, PooledContext.DisposedCount); Assert.InRange(PooledContext.InstanceCount, low: 32, high: 64); } private readonly TimeSpan _duration = TimeSpan.FromSeconds(value: 10); private int _stopwatchStarted; private readonly Stopwatch _stopwatch = new(); private long _requests; private async Task WriteResults() { if (Interlocked.Exchange(ref _stopwatchStarted, value: 1) == 0) { _stopwatch.Start(); } var lastRequests = (long)0; var lastElapsed = TimeSpan.Zero; while (_stopwatch.IsRunning) { await Task.Delay(TimeSpan.FromSeconds(value: 1)); var currentRequests = _requests - lastRequests; lastRequests = _requests; var elapsed = _stopwatch.Elapsed; var currentElapsed = elapsed - lastElapsed; lastElapsed = elapsed; _testOutputHelper? .WriteLine( $"[{DateTime.Now:HH:mm:ss.fff}] Requests: {_requests}, " + $"RPS: {Math.Round(currentRequests / currentElapsed.TotalSeconds)}"); if (elapsed > _duration) { _testOutputHelper?.WriteLine(message: ""); _testOutputHelper?.WriteLine($"Average RPS: {Math.Round(_requests / elapsed.TotalSeconds)}"); _stopwatch.Stop(); } } } [ConditionalTheory] [InlineData(false)] [InlineData(true)] public async Task Concurrency_test2(bool async) { var factory = BuildFactory(withDependencyInjection: false); await Task.WhenAll( Enumerable.Range(0, 10).Select( _ => Task.Run( async () => { for (var j = 0; j < 1_000_000; j++) { var ctx = factory.CreateDbContext(); if (async) { await ctx.DisposeAsync(); } else { ctx.Dispose(); } } }))); } private void UseQueryTrackingBehavior(DbContextOptionsBuilder optionsBuilder, QueryTrackingBehavior? queryTrackingBehavior) { if (queryTrackingBehavior.HasValue) { optionsBuilder.UseQueryTrackingBehavior(queryTrackingBehavior.Value); } } private async Task Dispose(IDisposable disposable, bool async) { if (async) { await ((IAsyncDisposable)disposable).DisposeAsync(); } else { disposable.Dispose(); } } private readonly ITestOutputHelper _testOutputHelper; public DbContextPoolingTest(NorthwindQueryJetFixture fixture, ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; } }