diff --git a/src/EFCore/DbContext.cs b/src/EFCore/DbContext.cs index f0e77b4796e..de313d8e950 100644 --- a/src/EFCore/DbContext.cs +++ b/src/EFCore/DbContext.cs @@ -63,6 +63,7 @@ public class DbContext : private IServiceScope _serviceScope; private IDbContextPool _dbContextPool; + private Action _returnToPool; private bool _initializing; private bool _disposed; @@ -623,8 +624,9 @@ public virtual async Task SaveChangesAsync( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - void IDbContextPoolable.SetPool(IDbContextPool contextPool) + void IDbContextPoolable.SetPool(IDbContextPool contextPool, Action returnToPool) { + _returnToPool = returnToPool; _dbContextPool = contextPool; _lease = 1; } @@ -652,8 +654,9 @@ DbContextPoolConfigurationSnapshot IDbContextPoolable.SnapshotConfiguration() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - void IDbContextPoolable.Resurrect(DbContextPoolConfigurationSnapshot configurationSnapshot) + void IDbContextPoolable.Resurrect(DbContextPoolConfigurationSnapshot configurationSnapshot, Action returnToPool) { + _returnToPool = returnToPool; _disposed = false; ++_lease; @@ -757,6 +760,14 @@ public virtual void Dispose() private bool DisposeSync() { + if (_returnToPool != null) + { + _returnToPool(this); + _returnToPool = null; + + return false; + } + if (_dbContextPool == null && !_disposed) { diff --git a/src/EFCore/Extensions/EntityFrameworkServiceCollectionExtensions.cs b/src/EFCore/Extensions/EntityFrameworkServiceCollectionExtensions.cs index d58814ca911..5c3bdc5207e 100644 --- a/src/EFCore/Extensions/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/EFCore/Extensions/EntityFrameworkServiceCollectionExtensions.cs @@ -325,14 +325,33 @@ public static IServiceCollection AddDbContextPool(serviceCollection, optionsAction, poolSize); + + serviceCollection.TryAddSingleton( + sp => new DbContextPool( + sp.GetService>(), standalone: false)); + + serviceCollection.AddScoped.Lease>(); + + serviceCollection.AddScoped( + sp => (TContextService)sp.GetService.Lease>().Context); + + return serviceCollection; + } + + private static void AddPoolingOptions( + IServiceCollection serviceCollection, + Action optionsAction, int poolSize) + where TContext : DbContext + { if (poolSize <= 0) { throw new ArgumentOutOfRangeException(nameof(poolSize), CoreStrings.InvalidPoolSize); } - CheckContextConstructors(); + CheckContextConstructors(); - AddCoreServices( + AddCoreServices( serviceCollection, (sp, ob) => { @@ -344,17 +363,6 @@ public static IServiceCollection AddDbContextPool new DbContextPool( - sp.GetService>())); - - serviceCollection.AddScoped.Lease>(); - - serviceCollection.AddScoped( - sp => (TContextService)sp.GetService.Lease>().Context); - - return serviceCollection; } /// @@ -557,10 +565,10 @@ public static IServiceCollection AddDbContext type. /// /// - /// Using this method to register a factory is recommended for Blazor applications. /// Registering a factory instead of registering the context type directly allows for easy creation of new /// instances. - /// This is most useful where the dependency injection scope is not aligned with the context lifetime, such as in Blazor. + /// Registering a factory is recommended for Blazor applications and other situations where the dependency + /// injection scope is not aligned with the context lifetime. /// /// /// Use this method when using dependency injection in your application, such as with Blazor. @@ -611,10 +619,10 @@ public static IServiceCollection AddDbContextFactory( /// of given type. /// /// - /// Using this method to register a factory is recommended for Blazor applications. /// Registering a factory instead of registering the context type directly allows for easy creation of new /// instances. - /// This is most useful where the dependency injection scope is not aligned with the context lifetime, such as in Blazor. + /// Registering a factory is recommended for Blazor applications and other situations where the dependency + /// injection scope is not aligned with the context lifetime. /// /// /// Use this method when using dependency injection in your application, such as with Blazor. @@ -676,10 +684,10 @@ public static IServiceCollection AddDbContextFactory( /// of given type. /// /// - /// Using this method to register a factory is recommended for Blazor applications. /// Registering a factory instead of registering the context type directly allows for easy creation of new /// instances. - /// This is most useful where the dependency injection scope is not aligned with the context lifetime, such as in Blazor. + /// Registering a factory is recommended for Blazor applications and other situations where the dependency + /// injection scope is not aligned with the context lifetime. /// /// /// Use this method when using dependency injection in your application, such as with Blazor. @@ -738,10 +746,10 @@ public static IServiceCollection AddDbContextFactory( /// of given type. /// /// - /// Using this method to register a factory is recommended for Blazor applications. /// Registering a factory instead of registering the context type directly allows for easy creation of new /// instances. - /// This is most useful where the dependency injection scope is not aligned with the context lifetime, such as in Blazor. + /// Registering a factory is recommended for Blazor applications and other situations where the dependency + /// injection scope is not aligned with the context lifetime. /// /// /// Use this method when using dependency injection in your application, such as with Blazor. @@ -799,6 +807,8 @@ public static IServiceCollection AddDbContextFactory( where TContext : DbContext where TFactory : IDbContextFactory { + Check.NotNull(serviceCollection, nameof(serviceCollection)); + AddCoreServices(serviceCollection, optionsAction, lifetime); serviceCollection.AddSingleton, DbContextFactorySource>(); @@ -812,6 +822,108 @@ public static IServiceCollection AddDbContextFactory( return serviceCollection; } + /// + /// + /// Registers an in the to create instances + /// of given type where instances are pooled for reuse. + /// + /// + /// Registering a factory instead of registering the context type directly allows for easy creation of new + /// instances. + /// Registering a factory is recommended for Blazor applications and other situations where the dependency + /// injection scope is not aligned with the context lifetime. + /// + /// + /// Use this method when using dependency injection in your application, such as with Blazor. + /// For applications that don't use dependency injection, consider creating + /// instances directly with its constructor. The method can then be + /// overridden to configure a connection string and other options. + /// + /// + /// For more information on how to use this method, see the Entity Framework Core documentation at https://aka.ms/efdocs. + /// For more information on using dependency injection, see https://go.microsoft.com/fwlink/?LinkId=526890. + /// + /// + /// The type of to be created by the factory. + /// The to add services to. + /// + /// + /// A required action to configure the for the context. When using + /// context pooling, options configuration must be performed externally; + /// will not be called. + /// + /// + /// + /// Sets the maximum number of instances retained by the pool. + /// + /// + /// The same service collection so that multiple calls can be chained. + /// + public static IServiceCollection AddPooledDbContextFactory( + [NotNull] this IServiceCollection serviceCollection, + [NotNull] Action optionsAction, + int poolSize = 128) + where TContext : DbContext + { + Check.NotNull(optionsAction, nameof(optionsAction)); + + return AddPooledDbContextFactory(serviceCollection, (_, ob) => optionsAction(ob)); + } + + /// + /// + /// Registers an in the to create instances + /// of given type where instances are pooled for reuse. + /// + /// + /// Registering a factory instead of registering the context type directly allows for easy creation of new + /// instances. + /// Registering a factory is recommended for Blazor applications and other situations where the dependency + /// injection scope is not aligned with the context lifetime. + /// + /// + /// Use this method when using dependency injection in your application, such as with Blazor. + /// For applications that don't use dependency injection, consider creating + /// instances directly with its constructor. The method can then be + /// overridden to configure a connection string and other options. + /// + /// + /// For more information on how to use this method, see the Entity Framework Core documentation at https://aka.ms/efdocs. + /// For more information on using dependency injection, see https://go.microsoft.com/fwlink/?LinkId=526890. + /// + /// + /// The type of to be created by the factory. + /// The to add services to. + /// + /// + /// A required action to configure the for the context. When using + /// context pooling, options configuration must be performed externally; + /// will not be called. + /// + /// + /// + /// Sets the maximum number of instances retained by the pool. + /// + /// + /// The same service collection so that multiple calls can be chained. + /// + public static IServiceCollection AddPooledDbContextFactory( + [NotNull] this IServiceCollection serviceCollection, + [NotNull] Action optionsAction, + int poolSize = 128) + where TContext : DbContext + { + Check.NotNull(serviceCollection, nameof(serviceCollection)); + Check.NotNull(optionsAction, nameof(optionsAction)); + + AddPoolingOptions(serviceCollection, optionsAction, poolSize); + + serviceCollection.TryAddSingleton>(); + serviceCollection.TryAddSingleton, PooledDbContextFactory>(); + + return serviceCollection; + } + private static void AddCoreServices( IServiceCollection serviceCollection, Action optionsAction, diff --git a/src/EFCore/Internal/DbContextPool.cs b/src/EFCore/Internal/DbContextPool.cs index 4adc330ad37..6cd57a1ce4a 100644 --- a/src/EFCore/Internal/DbContextPool.cs +++ b/src/EFCore/Internal/DbContextPool.cs @@ -24,6 +24,7 @@ namespace Microsoft.EntityFrameworkCore.Internal public class DbContextPool : IDbContextPool, IDisposable, IAsyncDisposable where TContext : DbContext { + private readonly bool _standalone; private const int DefaultPoolSize = 32; private readonly ConcurrentQueue _pool = new ConcurrentQueue(); @@ -72,7 +73,7 @@ void IDisposable.Dispose() { if (!_contextPool.Return(Context)) { - ((IDbContextPoolable)Context).SetPool(null); + ((IDbContextPoolable)Context).SetPool(null, null); Context.Dispose(); } @@ -87,7 +88,7 @@ async ValueTask IAsyncDisposable.DisposeAsync() { if (!_contextPool.Return(Context)) { - ((IDbContextPoolable)Context).SetPool(null); + ((IDbContextPoolable)Context).SetPool(null, null); await Context.DisposeAsync().ConfigureAwait(false); } @@ -103,8 +104,20 @@ async ValueTask IAsyncDisposable.DisposeAsync() /// 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 DbContextPool([NotNull] DbContextOptions options) + public DbContextPool([NotNull] DbContextOptions options) + : this(options, standalone: true) { + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public DbContextPool([NotNull] DbContextOptions options, bool standalone) + { + _standalone = standalone; _maxSize = options.FindExtension()?.MaxPoolSize ?? DefaultPoolSize; options.Freeze(); @@ -118,7 +131,7 @@ public DbContextPool([NotNull] DbContextOptions options) } } - private static Func CreateActivator(DbContextOptions options) + private static Func CreateActivator(DbContextOptions options) { var constructors = typeof(TContext).GetTypeInfo().DeclaredConstructors @@ -151,13 +164,24 @@ var constructors /// public virtual TContext Rent() { + var returnToPool = _standalone + ? c => + { + if (!Return((TContext)c)) + { + ((IDbContextPoolable)c).SetPool(null, null); + c.Dispose(); + } + } + : (Action)null; + if (_pool.TryDequeue(out var context)) { Interlocked.Decrement(ref _count); Check.DebugAssert(_count >= 0, $"_count is {_count}"); - ((IDbContextPoolable)context).Resurrect(_configurationSnapshot); + ((IDbContextPoolable)context).Resurrect(_configurationSnapshot, returnToPool); return context; } @@ -170,7 +194,8 @@ public virtual TContext Rent() (IDbContextPoolable)context, c => c.SnapshotConfiguration()); - ((IDbContextPoolable)context).SetPool(this); + + ((IDbContextPoolable)context).SetPool(this, returnToPool); return context; } @@ -227,7 +252,7 @@ public virtual void Dispose() while (_pool.TryDequeue(out var context)) { - ((IDbContextPoolable)context).SetPool(null); + ((IDbContextPoolable)context).SetPool(null, null); context.Dispose(); } } @@ -244,7 +269,7 @@ public virtual async ValueTask DisposeAsync() while (_pool.TryDequeue(out var context)) { - ((IDbContextPoolable)context).SetPool(null); + ((IDbContextPoolable)context).SetPool(null, null); await context.DisposeAsync().ConfigureAwait(false); } } diff --git a/src/EFCore/Internal/IDbContextPoolable.cs b/src/EFCore/Internal/IDbContextPoolable.cs index 4e4c9225fad..4161023a739 100644 --- a/src/EFCore/Internal/IDbContextPoolable.cs +++ b/src/EFCore/Internal/IDbContextPoolable.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -20,7 +21,7 @@ public interface IDbContextPoolable : IResettableService /// 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. /// - void SetPool([CanBeNull] IDbContextPool contextPool); + void SetPool([CanBeNull] IDbContextPool contextPool, [CanBeNull] Action returnToPool); /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -36,6 +37,6 @@ public interface IDbContextPoolable : IResettableService /// 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. /// - void Resurrect([NotNull] DbContextPoolConfigurationSnapshot configurationSnapshot); + void Resurrect([NotNull] DbContextPoolConfigurationSnapshot configurationSnapshot, [CanBeNull] Action returnToPool); } } diff --git a/src/EFCore/Internal/PooledDbContextFactory.cs b/src/EFCore/Internal/PooledDbContextFactory.cs new file mode 100644 index 00000000000..74246b889f6 --- /dev/null +++ b/src/EFCore/Internal/PooledDbContextFactory.cs @@ -0,0 +1,42 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class PooledDbContextFactory : IDbContextFactory + where TContext : DbContext + { + private readonly DbContextPool _pool; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public PooledDbContextFactory([NotNull] DbContextPool pool) + { + Check.NotNull(pool, nameof(pool)); + + _pool = pool; + } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual TContext CreateDbContext() + => _pool.Rent(); + } +} diff --git a/test/EFCore.Specification.Tests/TestUtilities/PoolableDbContext.cs b/test/EFCore.Specification.Tests/TestUtilities/PoolableDbContext.cs index 8e8edcda215..07ca09abd88 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/PoolableDbContext.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/PoolableDbContext.cs @@ -27,7 +27,7 @@ public override void Dispose() { if (!_contextPool.Return(this)) { - ((IDbContextPoolable)this).SetPool(null); + ((IDbContextPoolable)this).SetPool(null, null); base.Dispose(); } diff --git a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs index b61c5a109c2..b4bbd5839fd 100644 --- a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs @@ -55,6 +55,21 @@ private static IServiceProvider BuildServiceProvider(int poolSize = 32 poolSize) .BuildServiceProvider(); + private static IServiceProvider BuildServiceProviderWithFactory(int poolSize = 32) + where TContext : DbContext + => new ServiceCollection() + .AddPooledDbContextFactory( + ob => + ob.UseSqlServer(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString) + .EnableServiceProviderCaching(false), + poolSize) + .AddDbContextPool( + ob => + ob.UseSqlServer(SqlServerNorthwindTestStoreFactory.NorthwindConnectionString) + .EnableServiceProviderCaching(false), + poolSize) + .BuildServiceProvider(); + private interface IPooledContext { } @@ -133,6 +148,12 @@ public void Invalid_pool_size() Assert.Throws( () => BuildServiceProvider(poolSize: -1)); + + Assert.Throws( + () => BuildServiceProviderWithFactory(poolSize: 0)); + + Assert.Throws( + () => BuildServiceProviderWithFactory(poolSize: -1)); } [ConditionalTheory] @@ -161,6 +182,25 @@ public void Options_modified_in_on_configuring(bool useInterface) } } + [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 { } @@ -181,18 +221,28 @@ public void Throws_when_used_with_parameterless_constructor_context() Assert.Throws( () => serviceCollection.AddDbContextPool( (_, __) => { })).Message); + + Assert.Equal( + CoreStrings.DbContextMissingConstructor(nameof(BadCtorContext)), + Assert.Throws( + () => serviceCollection.AddPooledDbContextFactory( + (_, __) => { })).Message); } - [ConditionalFact] - public void Can_pool_non_derived_context() + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public void Can_pool_non_derived_context(bool useFactory) { - var serviceProvider = BuildServiceProvider(); + var serviceProvider = useFactory + ? BuildServiceProviderWithFactory() + : BuildServiceProvider(); var serviceScope1 = serviceProvider.CreateScope(); - var context1 = serviceScope1.ServiceProvider.GetService(); + var context1 = GetContext(serviceScope1); var serviceScope2 = serviceProvider.CreateScope(); - var context2 = serviceScope2.ServiceProvider.GetService(); + var context2 = GetContext(serviceScope2); Assert.NotSame(context1, context2); @@ -205,9 +255,19 @@ public void Can_pool_non_derived_context() Assert.Equal(1, id1.Lease); Assert.Equal(1, id2.Lease); + if (useFactory) + { + context1.Dispose(); + } + serviceScope1.Dispose(); serviceScope2.Dispose(); + if (useFactory) + { + context2.Dispose(); + } + var id1d = context1.ContextId; var id2d = context2.ContextId; @@ -217,7 +277,7 @@ public void Can_pool_non_derived_context() Assert.Equal(1, id2d.Lease); var serviceScope3 = serviceProvider.CreateScope(); - var context3 = serviceScope3.ServiceProvider.GetService(); + var context3 = GetContext(serviceScope3); var id1r = context3.ContextId; @@ -228,7 +288,7 @@ public void Can_pool_non_derived_context() Assert.Equal(2, id1r.Lease); var serviceScope4 = serviceProvider.CreateScope(); - var context4 = serviceScope4.ServiceProvider.GetService(); + var context4 = GetContext(serviceScope4); var id2r = context4.ContextId; @@ -237,6 +297,11 @@ public void Can_pool_non_derived_context() Assert.NotEqual(default, id2r.InstanceId); Assert.NotEqual(id2, id2r); Assert.Equal(2, id2r.Lease); + + DbContext GetContext(IServiceScope serviceScope) + => useFactory + ? serviceScope.ServiceProvider.GetService>().CreateDbContext() + : serviceScope.ServiceProvider.GetService(); } [ConditionalFact] @@ -367,6 +432,38 @@ public void Contexts_are_pooled(bool useInterface) Assert.Same(secondContext2, secondContext4); } + [ConditionalFact] + public void Contexts_are_pooled_with_factory() + { + var factory = BuildServiceProviderWithFactory().GetService>(); + + var context1 = factory.CreateDbContext(); + var secondContext1 = factory.CreateDbContext(); + + var context2 = factory.CreateDbContext(); + var secondContext2 = factory.CreateDbContext(); + + Assert.NotSame(context1, context2); + Assert.NotSame(secondContext1, secondContext2); + + context1.Dispose(); + secondContext1.Dispose(); + context2.Dispose(); + secondContext2.Dispose(); + + var context3 = factory.CreateDbContext(); + var secondContext3 = factory.CreateDbContext(); + + Assert.Same(context1, context3); + Assert.Same(secondContext1, secondContext3); + + var context4 = factory.CreateDbContext(); + var secondContext4 = factory.CreateDbContext(); + + Assert.Same(context2, context4); + Assert.Same(secondContext2, secondContext4); + } + [ConditionalTheory] [InlineData(true)] [InlineData(false)] @@ -409,6 +506,34 @@ public void Context_configuration_is_reset(bool useInterface) Assert.False(context2.Database.AutoTransactionsEnabled); } + [ConditionalFact] + public void Context_configuration_is_reset_with_factory() + { + var factory = BuildServiceProviderWithFactory().GetService>(); + + var context1 = factory.CreateDbContext(); + + 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; + + context1.Dispose(); + + var context2 = factory.CreateDbContext(); + + 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 Change_tracker_can_be_cleared_without_resetting_context_config() { @@ -486,6 +611,35 @@ public void Default_Context_configuration_is_reset() Assert.True(context2.Database.AutoTransactionsEnabled); } + [ConditionalFact] + public void Default_Context_configuration_is_reset_with_factory() + { + var factory = BuildServiceProviderWithFactory() + .GetService>(); + + var context1 = factory.CreateDbContext(); + + 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; + + context1.Dispose(); + + var context2 = 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.True(context2.Database.AutoTransactionsEnabled); + } + [ConditionalTheory] [InlineData(true)] [InlineData(false)] @@ -531,6 +685,36 @@ public void State_manager_is_reset(bool useInterface) private static T Scoper(Func getter) => getter(); + [ConditionalFact] + public void State_manager_is_reset_with_factory() + { + var weakRef = Scoper( + () => + { + var factory = BuildServiceProviderWithFactory() + .GetService>(); + + var context1 = factory.CreateDbContext(); + + var entity = context1.Customers.First(c => c.CustomerId == "ALFKI"); + + Assert.Single(context1.ChangeTracker.Entries()); + + context1.Dispose(); + + var context2 = factory.CreateDbContext(); + + Assert.Same(context1, context2); + Assert.Empty(context2.ChangeTracker.Entries()); + + return new WeakReference(entity); + }); + + GC.Collect(); + + Assert.False(weakRef.IsAlive); + } + [ConditionalTheory] [InlineData(true)] [InlineData(false)] @@ -631,6 +815,25 @@ public void Double_dispose_does_not_enter_pool_twice(bool useInterface) Assert.NotSame(context1, context2); } + [ConditionalFact] + public void Can_double_dispose_with_factory() + { + var factory = BuildServiceProviderWithFactory() + .GetService>(); + + var context = factory.CreateDbContext(); + + context.Customers.Load(); + + context.Dispose(); + + Assert.Throws(() => context.Customers.ToList()); + + context.Dispose(); + + Assert.Throws(() => context.Customers.ToList()); + } + [ConditionalTheory] [InlineData(true)] [InlineData(false)] @@ -680,6 +883,37 @@ public void Provider_services_are_reset(bool useInterface) Assert.Null(context3.Database.CurrentTransaction); } + [ConditionalFact] + public void Provider_services_are_reset_with_factory() + { + var factory = BuildServiceProviderWithFactory() + .GetService>(); + + var context1 = factory.CreateDbContext(); + + context1.Database.BeginTransaction(); + + Assert.NotNull(context1.Database.CurrentTransaction); + + context1.Dispose(); + + var context2 = factory.CreateDbContext(); + + Assert.Same(context1, context2); + Assert.Null(context2.Database.CurrentTransaction); + + context2.Database.BeginTransaction(); + + Assert.NotNull(context2.Database.CurrentTransaction); + + context2.Dispose(); + + var context3 = factory.CreateDbContext(); + + Assert.Same(context2, context3); + Assert.Null(context3.Database.CurrentTransaction); + } + [ConditionalTheory] [InlineData(true)] [InlineData(false)] diff --git a/test/EFCore.Tests/DbContextFactoryTest.cs b/test/EFCore.Tests/DbContextFactoryTest.cs index 5a8a866508f..477f28a6866 100644 --- a/test/EFCore.Tests/DbContextFactoryTest.cs +++ b/test/EFCore.Tests/DbContextFactoryTest.cs @@ -3,9 +3,9 @@ using System; using System.Linq; -using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.InMemory.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.Internal; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -40,6 +40,36 @@ public void Factory_creates_new_context_instance(ServiceLifetime lifetime) Assert.Equal(nameof(WoolacombeContext), GetStoreName(context2)); } + [ConditionalFact] + public void Factory_can_use_pool() + { + var serviceProvider = (IServiceProvider)new ServiceCollection() + .AddPooledDbContextFactory( + b => b.UseInMemoryDatabase(nameof(WoolacombeContext))) + .BuildServiceProvider(validateScopes: true); + + var contextFactory = serviceProvider.GetService>(); + + var context1 = contextFactory.CreateDbContext(); + var context2 = contextFactory.CreateDbContext(); + + Assert.NotSame(context1, context2); + Assert.Equal(nameof(WoolacombeContext), GetStoreName(context1)); + Assert.Equal(nameof(WoolacombeContext), GetStoreName(context2)); + + context1.Dispose(); + context2.Dispose(); + + using var context1b = contextFactory.CreateDbContext(); + using var context2b = contextFactory.CreateDbContext(); + + Assert.NotSame(context1b, context2b); + Assert.Same(context1, context1b); + Assert.Same(context2, context2b); + Assert.Equal(nameof(WoolacombeContext), GetStoreName(context1b)); + Assert.Equal(nameof(WoolacombeContext), GetStoreName(context2b)); + } + [ConditionalTheory] [InlineData(ServiceLifetime.Singleton)] [InlineData(ServiceLifetime.Scoped)] @@ -72,6 +102,26 @@ public void Default_lifetime_is_singleton() serviceCollection.Single(e => e.ServiceType == typeof(DbContextOptions)).Lifetime); } + [ConditionalFact] + public void Lifetime_is_singleton_when_pooling() + { + var serviceCollection = new ServiceCollection() + .AddPooledDbContextFactory( + b => b.UseInMemoryDatabase(nameof(WoolacombeContext))); + + Assert.Equal( + ServiceLifetime.Singleton, + serviceCollection.Single(e => e.ServiceType == typeof(DbContextPool)).Lifetime); + + Assert.Equal( + ServiceLifetime.Singleton, + serviceCollection.Single(e => e.ServiceType == typeof(IDbContextFactory)).Lifetime); + + Assert.Equal( + ServiceLifetime.Singleton, + serviceCollection.Single(e => e.ServiceType == typeof(DbContextOptions)).Lifetime); + } + private class WoolacombeContext : DbContext { public WoolacombeContext(DbContextOptions options) @@ -119,6 +169,18 @@ protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBu => optionsBuilder.UseInMemoryDatabase(nameof(MortehoeContext)); } + [ConditionalFact] + public void Factory_can_use_DbContext_directly() + { + var serviceProvider = (IServiceProvider)new ServiceCollection() + .AddDbContextFactory(b => b.UseInMemoryDatabase(nameof(DbContext))) + .BuildServiceProvider(validateScopes: true); + + using var context = serviceProvider.GetService>().CreateDbContext(); + + Assert.Equal(nameof(DbContext), GetStoreName(context)); + } + [ConditionalTheory] [InlineData(ServiceLifetime.Singleton)] [InlineData(ServiceLifetime.Scoped)] @@ -270,7 +332,7 @@ public void Can_resolve_from_the_service_provider_in_options_action(ServiceLifet .AddScoped() .AddTransient() .AddDbContextFactory( - optionsAction: (p, b) => + (p, b) => { Assert.NotNull(p.GetService()); Assert.NotNull(p.GetService()); @@ -300,6 +362,32 @@ public void Can_resolve_from_the_service_provider_in_options_action(ServiceLifet Assert.Equal(nameof(WoolacombeContext), GetStoreName(context2)); } + [ConditionalFact] + public void Can_resolve_from_the_service_provider_when_pooling() + { + var serviceProvider = (IServiceProvider)new ServiceCollection() + .AddSingleton() + .AddTransient() + .AddDbContextFactory( + (p, b) => + { + Assert.NotNull(p.GetService()); + Assert.NotNull(p.GetService()); + + b.UseInMemoryDatabase(nameof(WoolacombeContext)); + }) + .BuildServiceProvider(validateScopes: true); + + var contextFactory = serviceProvider.GetService>(); + + using var context1 = contextFactory.CreateDbContext(); + using var context2 = contextFactory.CreateDbContext(); + + Assert.NotSame(context1, context2); + Assert.Equal(nameof(WoolacombeContext), GetStoreName(context1)); + Assert.Equal(nameof(WoolacombeContext), GetStoreName(context2)); + } + [ConditionalFact] public void Throws_if_dependencies_are_not_in_DI() {