// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using EntityFrameworkCore.Jet.Data; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using EntityFrameworkCore.Jet.FunctionalTests.Query; using EntityFrameworkCore.Jet.FunctionalTests.TestUtilities; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Query; using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.EntityFrameworkCore.TestUtilities.Xunit; using Microsoft.Extensions.DependencyInjection; using Xunit; using Xunit.Abstractions; // 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 IServiceProvider BuildServiceProvider(int poolSize = 32) where TContextService : class where TContext : DbContext, TContextService => new ServiceCollection() .AddDbContextPool( ob => ob.UseJet(JetNorthwindTestStoreFactory.NorthwindConnectionString, TestEnvironment.DataAccessProviderFactory) .EnableServiceProviderCaching(false), poolSize) .AddDbContextPool( ob => ob.UseJet(JetNorthwindTestStoreFactory.NorthwindConnectionString, TestEnvironment.DataAccessProviderFactory) .EnableServiceProviderCaching(false), poolSize).BuildServiceProvider(); private static IServiceProvider BuildServiceProvider(int poolSize = 32) where TContext : DbContext => new ServiceCollection() .AddDbContextPool( ob => ob.UseJet(JetNorthwindTestStoreFactory.NorthwindConnectionString, TestEnvironment.DataAccessProviderFactory) .EnableServiceProviderCaching(false), poolSize) .AddDbContextPool( ob => ob.UseJet(JetNorthwindTestStoreFactory.NorthwindConnectionString, TestEnvironment.DataAccessProviderFactory) .EnableServiceProviderCaching(false), poolSize) .BuildServiceProvider(); 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.AutoTransactionsEnabled = false; ChangeTracker.CascadeDeleteTiming = CascadeTiming.Never; ChangeTracker.DeleteOrphansTiming = CascadeTiming.Never; } 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"); public override void Dispose() { base.Dispose(); Interlocked.Increment(ref DisposedCount); } public class Customer { public string CustomerId { get; set; } public string CompanyName { get; set; } } } private interface ISecondContext { } private class SecondContext : DbContext, ISecondContext { public SecondContext(DbContextOptions options) : base(options) { } } [ConditionalFact] public void Invalid_pool_size() { Assert.Throws( () => BuildServiceProvider(poolSize: 0)); Assert.Throws( () => BuildServiceProvider(poolSize: -1)); } [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; } } 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); } [ConditionalFact] public void Can_pool_non_derived_context() { var serviceProvider = BuildServiceProvider(); 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(id1, id2); Assert.NotEqual(id1.InstanceId, id2.InstanceId); Assert.Equal(1, id1.Lease); Assert.Equal(1, id2.Lease); serviceScope1.Dispose(); serviceScope2.Dispose(); 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 = serviceScope3.ServiceProvider.GetService(); 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 = serviceScope4.ServiceProvider.GetService(); 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); } [ConditionalFact] public void ContextIds_make_sense_when_not_pooling() { var serviceProvider = new ServiceCollection() .AddDbContext( ob => ob.UseJet(JetNorthwindTestStoreFactory.NorthwindConnectionString, TestEnvironment.DataAccessProviderFactory) .EnableServiceProviderCaching(false)) .BuildServiceProvider(); 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); serviceScope1.Dispose(); serviceScope2.Dispose(); 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(true)] [InlineData(false)] public void Contexts_are_pooled(bool useInterface) { 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); serviceScope1.Dispose(); serviceScope2.Dispose(); 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); } [ConditionalTheory] [InlineData(true)] [InlineData(false)] public void Context_configuration_is_reset(bool useInterface) { var serviceProvider = useInterface ? BuildServiceProvider() : BuildServiceProvider(); var serviceScope = serviceProvider.CreateScope(); var scopedProvider = serviceScope.ServiceProvider; var context1 = useInterface ? (DbContext)scopedProvider.GetService() : scopedProvider.GetService(); 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.AutoTransactionsEnabled = true; serviceScope.Dispose(); serviceScope = serviceProvider.CreateScope(); scopedProvider = serviceScope.ServiceProvider; var context2 = useInterface ? (DbContext)scopedProvider.GetService() : scopedProvider.GetService(); Assert.Same(context1, context2); Assert.False(context2.ChangeTracker.AutoDetectChangesEnabled); Assert.False(context2.ChangeTracker.LazyLoadingEnabled); Assert.Equal(QueryTrackingBehavior.TrackAll, context2.ChangeTracker.QueryTrackingBehavior); Assert.Equal(CascadeTiming.Never, context2.ChangeTracker.CascadeDeleteTiming); Assert.Equal(CascadeTiming.Never, context2.ChangeTracker.DeleteOrphansTiming); Assert.False(context2.Database.AutoTransactionsEnabled); } [ConditionalFact] public void Default_Context_configuration__is_reset() { var serviceProvider = BuildServiceProvider(); 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.AutoTransactionsEnabled = false; context1.ChangeTracker.CascadeDeleteTiming = CascadeTiming.Immediate; context1.ChangeTracker.DeleteOrphansTiming = CascadeTiming.Immediate; serviceScope.Dispose(); 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.TrackAll, context2.ChangeTracker.QueryTrackingBehavior); Assert.Equal(CascadeTiming.Immediate, context2.ChangeTracker.CascadeDeleteTiming); Assert.Equal(CascadeTiming.Immediate, context2.ChangeTracker.DeleteOrphansTiming); Assert.True(context2.Database.AutoTransactionsEnabled); } [ConditionalTheory] [InlineData(true)] [InlineData(false)] public void State_manager_is_reset(bool useInterface) { var weakRef = Scoper( () => { 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()); serviceScope.Dispose(); 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 T Scoper(Func getter) => getter(); [ConditionalTheory] [InlineData(true)] [InlineData(false)] public void Pool_disposes_context_when_context_not_pooled(bool useInterface) { var serviceProvider = useInterface ? BuildServiceProvider() : BuildServiceProvider(); var serviceScope1 = serviceProvider.CreateScope(); var scopedProvider1 = serviceScope1.ServiceProvider; if (useInterface) { scopedProvider1.GetService(); } else { scopedProvider1.GetService(); } var serviceScope2 = serviceProvider.CreateScope(); var scopedProvider2 = serviceScope2.ServiceProvider; var context = useInterface ? (PooledContext)scopedProvider2.GetService() : scopedProvider2.GetService(); serviceScope1.Dispose(); serviceScope2.Dispose(); Assert.Throws(() => context.Customers.ToList()); } [ConditionalTheory] [InlineData(true)] [InlineData(false)] public void Pool_disposes_contexts_when_disposed(bool useInterface) { var serviceProvider = useInterface ? BuildServiceProvider() : BuildServiceProvider(); var serviceScope = serviceProvider.CreateScope(); var scopedProvider = serviceScope.ServiceProvider; var context = useInterface ? (PooledContext)scopedProvider.GetService() : scopedProvider.GetService(); serviceScope.Dispose(); ((IDisposable)serviceProvider).Dispose(); Assert.Throws(() => context.Customers.ToList()); } [ConditionalTheory] [InlineData(true)] [InlineData(false)] public void Object_in_pool_is_disposed(bool useInterface) { var serviceProvider = useInterface ? BuildServiceProvider() : BuildServiceProvider(); var serviceScope = serviceProvider.CreateScope(); var scopedProvider = serviceScope.ServiceProvider; var context = useInterface ? (PooledContext)scopedProvider.GetService() : scopedProvider.GetService(); serviceScope.Dispose(); Assert.Throws(() => context.Customers.ToList()); } [ConditionalTheory] [InlineData(true)] [InlineData(false)] public void Double_dispose_does_not_enter_pool_twice(bool useInterface) { var serviceProvider = useInterface ? BuildServiceProvider() : BuildServiceProvider(); var contextPool = serviceProvider.GetService>(); var context = contextPool.Rent(); context.Dispose(); context.Dispose(); var context1 = contextPool.Rent(); var context2 = contextPool.Rent(); Assert.NotSame(context1, context2); } [ConditionalTheory] [InlineData(true)] [InlineData(false)] public void Provider_services_are_reset(bool useInterface) { var serviceProvider = useInterface ? BuildServiceProvider() : BuildServiceProvider(); var serviceScope = serviceProvider.CreateScope(); var scopedProvider = serviceScope.ServiceProvider; var context1 = useInterface ? (PooledContext)scopedProvider.GetService() : scopedProvider.GetService(); context1.Database.BeginTransaction(); Assert.NotNull(context1.Database.CurrentTransaction); serviceScope.Dispose(); 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); serviceScope.Dispose(); 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(true)] [InlineData(false)] [PlatformSkipCondition(TestPlatform.Linux, SkipReason = "Test is flaky on CI.")] public void Double_dispose_concurrency_test(bool useInterface) { var serviceProvider = useInterface ? BuildServiceProvider() : BuildServiceProvider(); Parallel.For( fromInclusive: 0, toExclusive: 100, 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(true)] [InlineData(false)] [PlatformSkipCondition(TestPlatform.Linux, SkipReason = "Test is flaky on CI.")] public async Task Concurrency_test(bool useInterface) { 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 Stopwatch(); 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(); } } } private readonly ITestOutputHelper _testOutputHelper = null; // ReSharper disable once UnusedParameter.Local public DbContextPoolingTest(NorthwindQueryJetFixture fixture, ITestOutputHelper testOutputHelper) { //_testOutputHelper = testOutputHelper; } } }