diff --git a/src/EFCore.Relational/Metadata/RuntimeRelationalPropertyOverrides.cs b/src/EFCore.Relational/Metadata/RuntimeRelationalPropertyOverrides.cs index adfebd3cef9..1df7f330414 100644 --- a/src/EFCore.Relational/Metadata/RuntimeRelationalPropertyOverrides.cs +++ b/src/EFCore.Relational/Metadata/RuntimeRelationalPropertyOverrides.cs @@ -16,7 +16,7 @@ public class RuntimeRelationalPropertyOverrides : AnnotatableBase, IRelationalPr /// Initializes a new instance of the class. /// /// The property for which the overrides are applied. - /// Whether the column name is overriden. + /// Whether the column name is overridden. /// The column name. public RuntimeRelationalPropertyOverrides( RuntimeProperty property, diff --git a/src/EFCore.Relational/Storage/RelationalConnection.cs b/src/EFCore.Relational/Storage/RelationalConnection.cs index 51d6c597d07..f294e38db35 100644 --- a/src/EFCore.Relational/Storage/RelationalConnection.cs +++ b/src/EFCore.Relational/Storage/RelationalConnection.cs @@ -242,7 +242,7 @@ public virtual void EnlistTransaction(Transaction? transaction) } /// - /// Template method that by default calls but can be overriden + /// Template method that by default calls but can be overridden /// by providers to make a different call instead. /// /// The transaction to be used. @@ -351,7 +351,7 @@ public virtual IDbContextTransaction BeginTransaction(IsolationLevel isolationLe } /// - /// Template method that by default calls but can be overriden + /// Template method that by default calls but can be overridden /// by providers to make a different call instead. /// /// The isolation level to use for the transaction. @@ -407,7 +407,7 @@ public virtual async Task BeginTransactionAsync( /// /// Template method that by default calls but can be - /// overriden by providers to make a different call instead. + /// overridden by providers to make a different call instead. /// /// The isolation level to use for the transaction. /// A to observe while waiting for the task to complete. @@ -728,7 +728,7 @@ private void OpenInternal(bool errorsExpected) } /// - /// Template method that by default calls but can be overriden + /// Template method that by default calls but can be overridden /// by providers to make a different call instead. /// /// Indicates if the connection errors are expected and should be logged as debug message. @@ -782,7 +782,7 @@ await logger.ConnectionErrorAsync( } /// - /// Template method that by default calls but can be overriden + /// Template method that by default calls but can be overridden /// by providers to make a different call instead. /// /// Indicates if the connection errors are expected and should be logged as debug message. @@ -899,7 +899,7 @@ public virtual bool Close() } /// - /// Template method that by default calls but can be overriden + /// Template method that by default calls but can be overridden /// by providers to make a different call instead. /// protected virtual void CloseDbConnection() @@ -975,14 +975,14 @@ await Dependencies.ConnectionLogger.ConnectionErrorAsync( } /// - /// Template method that by default calls but can be overriden + /// Template method that by default calls but can be overridden /// by providers to make a different call instead. /// protected virtual Task CloseDbConnectionAsync() => DbConnection.CloseAsync(); /// - /// Template method that by default calls but can be overriden + /// Template method that by default calls but can be overridden /// by providers to make a different call instead. /// protected virtual ConnectionState DbConnectionState => DbConnection.State; @@ -1080,14 +1080,14 @@ protected virtual async ValueTask ResetStateAsync(bool disposeDbConnection) } /// - /// Template method that by default calls but can be overriden by + /// Template method that by default calls but can be overridden by /// providers to make a different call instead. /// protected virtual void DisposeDbConnection() => DbConnection.Dispose(); /// - /// Template method that by default calls but can be overriden by + /// Template method that by default calls but can be overridden by /// providers to make a different call instead. /// protected virtual ValueTask DisposeDbConnectionAsync() diff --git a/src/EFCore/DbContext.cs b/src/EFCore/DbContext.cs index d774d657148..73e13982fb6 100644 --- a/src/EFCore/DbContext.cs +++ b/src/EFCore/DbContext.cs @@ -47,8 +47,6 @@ namespace Microsoft.EntityFrameworkCore /// /// public class DbContext : - IDisposable, - IAsyncDisposable, IInfrastructure, IDbContextDependencies, IDbSetCache, @@ -724,43 +722,46 @@ void IDbContextPoolable.ClearLease() /// [EntityFrameworkInternal] void IDbContextPoolable.SetLease(DbContextLease lease) + { + SetLeaseInternal(lease); + } + + /// + /// 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. + /// + [EntityFrameworkInternal] + Task IDbContextPoolable.SetLeaseAsync(DbContextLease lease, CancellationToken cancellationToken) + { + SetLeaseInternal(lease); + + return Task.CompletedTask; + } + + private void SetLeaseInternal(DbContextLease lease) { _lease = lease; _disposed = false; ++_leaseCount; - if (_configurationSnapshot?.AutoDetectChangesEnabled != null) - { - Check.DebugAssert( - _configurationSnapshot.QueryTrackingBehavior.HasValue, "!configurationSnapshot.QueryTrackingBehavior.HasValue"); - Check.DebugAssert(_configurationSnapshot.LazyLoadingEnabled.HasValue, "!configurationSnapshot.LazyLoadingEnabled.HasValue"); - Check.DebugAssert( - _configurationSnapshot.CascadeDeleteTiming.HasValue, "!configurationSnapshot.CascadeDeleteTiming.HasValue"); - Check.DebugAssert( - _configurationSnapshot.DeleteOrphansTiming.HasValue, "!configurationSnapshot.DeleteOrphansTiming.HasValue"); - - var changeTracker = ChangeTracker; - changeTracker.AutoDetectChangesEnabled = _configurationSnapshot.AutoDetectChangesEnabled.Value; - changeTracker.QueryTrackingBehavior = _configurationSnapshot.QueryTrackingBehavior.Value; - changeTracker.LazyLoadingEnabled = _configurationSnapshot.LazyLoadingEnabled.Value; - changeTracker.CascadeDeleteTiming = _configurationSnapshot.CascadeDeleteTiming.Value; - changeTracker.DeleteOrphansTiming = _configurationSnapshot.DeleteOrphansTiming.Value; - } - else - { - ((IResettableService?)_changeTracker)?.ResetState(); - } + Check.DebugAssert(_configurationSnapshot != null, "configurationSnapshot is null"); - if (_database != null) - { - _database.AutoTransactionsEnabled - = _configurationSnapshot?.AutoTransactionsEnabled == null - || _configurationSnapshot.AutoTransactionsEnabled.Value; + var changeTracker = ChangeTracker; + changeTracker.AutoDetectChangesEnabled = _configurationSnapshot.AutoDetectChangesEnabled; + changeTracker.QueryTrackingBehavior = _configurationSnapshot.QueryTrackingBehavior; + changeTracker.LazyLoadingEnabled = _configurationSnapshot.LazyLoadingEnabled; + changeTracker.CascadeDeleteTiming = _configurationSnapshot.CascadeDeleteTiming; + changeTracker.DeleteOrphansTiming = _configurationSnapshot.DeleteOrphansTiming; - _database.AutoSavepointsEnabled - = _configurationSnapshot?.AutoSavepointsEnabled == null - || _configurationSnapshot.AutoSavepointsEnabled.Value; - } + var database = Database; + database.AutoTransactionsEnabled = _configurationSnapshot.AutoTransactionsEnabled; + database.AutoSavepointsEnabled = _configurationSnapshot.AutoSavepointsEnabled; + + SavingChanges = _configurationSnapshot.SavingChanges; + SavedChanges = _configurationSnapshot.SavedChanges; + SaveChangesFailed = _configurationSnapshot.SaveChangesFailed; } /// @@ -771,14 +772,21 @@ void IDbContextPoolable.SetLease(DbContextLease lease) /// [EntityFrameworkInternal] void IDbContextPoolable.SnapshotConfiguration() - => _configurationSnapshot = new DbContextPoolConfigurationSnapshot( - _changeTracker?.AutoDetectChangesEnabled, - _changeTracker?.QueryTrackingBehavior, - _database?.AutoTransactionsEnabled, - _database?.AutoSavepointsEnabled, - _changeTracker?.LazyLoadingEnabled, - _changeTracker?.CascadeDeleteTiming, - _changeTracker?.DeleteOrphansTiming); + { + var changeTracker = ChangeTracker; + var database = Database; + _configurationSnapshot = new DbContextPoolConfigurationSnapshot( + changeTracker.AutoDetectChangesEnabled, + changeTracker.QueryTrackingBehavior, + database.AutoTransactionsEnabled, + database.AutoSavepointsEnabled, + changeTracker.LazyLoadingEnabled, + changeTracker.CascadeDeleteTiming, + changeTracker.DeleteOrphansTiming, + SavingChanges, + SavedChanges, + SaveChangesFailed); + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -794,8 +802,6 @@ void IResettableService.ResetState() service.ResetState(); } - ClearEvents(); - _disposed = true; } @@ -813,8 +819,6 @@ async Task IResettableService.ResetStateAsync(CancellationToken cancellationToke await service.ResetStateAsync(cancellationToken).ConfigureAwait(false); } - ClearEvents(); - _disposed = true; } @@ -851,22 +855,22 @@ private IEnumerable GetResettableServices() /// public virtual void Dispose() { - if (DisposeSync()) + var leaseActive = _lease.IsActive; + var contextDisposed = leaseActive && _lease.ContextDisposed(); + + if (DisposeSync(leaseActive, contextDisposed)) { _serviceScope?.Dispose(); } } - private bool DisposeSync() + private bool DisposeSync(bool leaseActive, bool contextDisposed) { - if (_lease.IsActive) + if (leaseActive) { - if (_lease.ContextDisposed()) + if (contextDisposed) { _disposed = true; - - ClearEvents(); - _lease = DbContextLease.InactiveLease; } } @@ -883,8 +887,11 @@ private bool DisposeSync() _dbContextDependencies = null; _changeTracker = null; _database = null; + _configurationSnapshot = null; - ClearEvents(); + SavingChanges = null; + SavedChanges = null; + SaveChangesFailed = null; return true; } @@ -895,14 +902,15 @@ private bool DisposeSync() /// /// Releases the allocated resources for this context. /// - public virtual ValueTask DisposeAsync() - => DisposeSync() ? _serviceScope.DisposeAsyncIfAvailable() : default; - - private void ClearEvents() + public virtual async ValueTask DisposeAsync() { - SavingChanges = null; - SavedChanges = null; - SaveChangesFailed = null; + var leaseActive = _lease.IsActive; + var contextDisposed = leaseActive && await _lease.ContextDisposedAsync(); + + if (DisposeSync(leaseActive, contextDisposed)) + { + await _serviceScope.DisposeAsyncIfAvailable(); + } } /// diff --git a/src/EFCore/IDbContextFactory.cs b/src/EFCore/IDbContextFactory.cs index f80f578ce1d..ea459694545 100644 --- a/src/EFCore/IDbContextFactory.cs +++ b/src/EFCore/IDbContextFactory.cs @@ -1,13 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; +using System.Threading; +using System.Threading.Tasks; + namespace Microsoft.EntityFrameworkCore { /// /// Defines a factory for creating instances. /// /// The type to create. - public interface IDbContextFactory + public interface IDbContextFactory where TContext : DbContext { /// @@ -20,5 +24,19 @@ public interface IDbContextFactory /// /// A new context instance. TContext CreateDbContext(); + + /// + /// + /// Creates a new instance in an async context. + /// + /// + /// The caller is responsible for disposing the context; it will not be disposed by any dependency injection container. + /// + /// + /// A to observe while waiting for the task to complete. + /// A task containing the created context that represents the asynchronous operation. + /// If the is canceled. + Task CreateDbContextAsync(CancellationToken cancellationToken = default) + => Task.FromResult(CreateDbContext()); } } diff --git a/src/EFCore/Infrastructure/DatabaseFacade.cs b/src/EFCore/Infrastructure/DatabaseFacade.cs index b453612528f..35bb5d5cd9e 100644 --- a/src/EFCore/Infrastructure/DatabaseFacade.cs +++ b/src/EFCore/Infrastructure/DatabaseFacade.cs @@ -17,7 +17,7 @@ namespace Microsoft.EntityFrameworkCore.Infrastructure /// Instances of this class are typically obtained from and it is not designed /// to be directly constructed in your application code. /// - public class DatabaseFacade : IInfrastructure, IDatabaseFacadeDependenciesAccessor + public class DatabaseFacade : IInfrastructure, IDatabaseFacadeDependenciesAccessor, IResettableService { private readonly DbContext _context; private IDatabaseFacadeDependencies? _dependencies; @@ -378,6 +378,20 @@ IDatabaseFacadeDependencies IDatabaseFacadeDependenciesAccessor.Dependencies DbContext IDatabaseFacadeDependenciesAccessor.Context => _context; + /// + void IResettableService.ResetState() + { + AutoTransactionsEnabled = true; + AutoSavepointsEnabled = true; + } + + Task IResettableService.ResetStateAsync(CancellationToken cancellationToken) + { + ((IResettableService)this).ResetState(); + + return Task.CompletedTask; + } + #region Hidden System.Object members /// diff --git a/src/EFCore/Infrastructure/PooledDbContextFactory.cs b/src/EFCore/Infrastructure/PooledDbContextFactory.cs index 49bcc05157a..ba5274351ed 100644 --- a/src/EFCore/Infrastructure/PooledDbContextFactory.cs +++ b/src/EFCore/Infrastructure/PooledDbContextFactory.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Threading; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Internal; namespace Microsoft.EntityFrameworkCore.Infrastructure @@ -49,6 +51,20 @@ public PooledDbContextFactory(DbContextOptions options, int poolSize = /// public virtual TContext CreateDbContext() - => (TContext)new DbContextLease(_pool, standalone: true).Context; + { + var lease = new DbContextLease(_pool, standalone: true); + lease.Context.SetLease(lease); + + return (TContext)lease.Context; + } + + /// + public virtual async Task CreateDbContextAsync(CancellationToken cancellationToken = default) + { + var lease = new DbContextLease(_pool, standalone: true); + await lease.Context.SetLeaseAsync(lease, cancellationToken); + + return (TContext)lease.Context; + } } } diff --git a/src/EFCore/Internal/DbContextLease.cs b/src/EFCore/Internal/DbContextLease.cs index 9936b4d5545..2c68687fb6d 100644 --- a/src/EFCore/Internal/DbContextLease.cs +++ b/src/EFCore/Internal/DbContextLease.cs @@ -38,8 +38,6 @@ public DbContextLease(IDbContextPool contextPool, bool standalone) var context = _contextPool.Rent(); Context = context; - - context.SetLease(this); } /// @@ -77,6 +75,24 @@ public bool ContextDisposed() return false; } + /// + /// 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 async ValueTask ContextDisposedAsync() + { + if (_standalone) + { + await ReleaseAsync(); + + return true; + } + + return false; + } + /// /// 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 @@ -98,7 +114,9 @@ public void Release() /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public ValueTask ReleaseAsync() - => Release(out var pool, out var context) ? pool.ReturnAsync(context) : default; + => Release(out var pool, out var context) + ? pool.ReturnAsync(context) + : default; private bool Release([NotNullWhen(true)] out IDbContextPool? pool, [NotNullWhen(true)] out IDbContextPoolable? context) { diff --git a/src/EFCore/Internal/DbContextPoolConfigurationSnapshot.cs b/src/EFCore/Internal/DbContextPoolConfigurationSnapshot.cs index 5139922e1dd..2891c1c547d 100644 --- a/src/EFCore/Internal/DbContextPoolConfigurationSnapshot.cs +++ b/src/EFCore/Internal/DbContextPoolConfigurationSnapshot.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using Microsoft.EntityFrameworkCore.ChangeTracking; namespace Microsoft.EntityFrameworkCore.Internal @@ -11,7 +12,7 @@ namespace Microsoft.EntityFrameworkCore.Internal /// 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 DbContextPoolConfigurationSnapshot + public sealed class DbContextPoolConfigurationSnapshot { /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -20,13 +21,16 @@ public class DbContextPoolConfigurationSnapshot /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public DbContextPoolConfigurationSnapshot( - bool? autoDetectChangesEnabled, - QueryTrackingBehavior? queryTrackingBehavior, - bool? autoTransactionsEnabled, - bool? autoSavepointsEnabled, - bool? lazyLoadingEnabled, - CascadeTiming? cascadeDeleteTiming, - CascadeTiming? deleteOrphansTiming) + bool autoDetectChangesEnabled, + QueryTrackingBehavior queryTrackingBehavior, + bool autoTransactionsEnabled, + bool autoSavepointsEnabled, + bool lazyLoadingEnabled, + CascadeTiming cascadeDeleteTiming, + CascadeTiming deleteOrphansTiming, + EventHandler? savingChanges, + EventHandler? savedChanges, + EventHandler? saveChangesFailed) { AutoDetectChangesEnabled = autoDetectChangesEnabled; QueryTrackingBehavior = queryTrackingBehavior; @@ -35,6 +39,9 @@ public DbContextPoolConfigurationSnapshot( LazyLoadingEnabled = lazyLoadingEnabled; CascadeDeleteTiming = cascadeDeleteTiming; DeleteOrphansTiming = deleteOrphansTiming; + SavingChanges = savingChanges; + SavedChanges = savedChanges; + SaveChangesFailed = saveChangesFailed; } /// @@ -43,7 +50,7 @@ public DbContextPoolConfigurationSnapshot( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual bool? AutoDetectChangesEnabled { get; } + public bool AutoDetectChangesEnabled { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -51,7 +58,7 @@ public DbContextPoolConfigurationSnapshot( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual bool? LazyLoadingEnabled { get; } + public bool LazyLoadingEnabled { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -59,7 +66,7 @@ public DbContextPoolConfigurationSnapshot( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual CascadeTiming? CascadeDeleteTiming { get; } + public CascadeTiming CascadeDeleteTiming { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -67,7 +74,7 @@ public DbContextPoolConfigurationSnapshot( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual CascadeTiming? DeleteOrphansTiming { get; } + public CascadeTiming DeleteOrphansTiming { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -75,7 +82,7 @@ public DbContextPoolConfigurationSnapshot( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual QueryTrackingBehavior? QueryTrackingBehavior { get; } + public QueryTrackingBehavior QueryTrackingBehavior { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -83,7 +90,7 @@ public DbContextPoolConfigurationSnapshot( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual bool? AutoTransactionsEnabled { get; } + public bool AutoTransactionsEnabled { get; } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -91,6 +98,30 @@ public DbContextPoolConfigurationSnapshot( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual bool? AutoSavepointsEnabled { get; } + public bool AutoSavepointsEnabled { get; } + + /// + /// 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 EventHandler? SavingChanges { get; } + + /// + /// 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 EventHandler? SavedChanges { get; } + + /// + /// 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 EventHandler? SaveChangesFailed { get; } } } diff --git a/src/EFCore/Internal/IDbContextPoolable.cs b/src/EFCore/Internal/IDbContextPoolable.cs index a93c59ee14b..6e051c2ec6b 100644 --- a/src/EFCore/Internal/IDbContextPoolable.cs +++ b/src/EFCore/Internal/IDbContextPoolable.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Infrastructure; namespace Microsoft.EntityFrameworkCore.Internal @@ -22,6 +24,14 @@ public interface IDbContextPoolable : IResettableService, IDisposable, IAsyncDis /// void SetLease(DbContextLease lease); + /// + /// 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. + /// + Task SetLeaseAsync(DbContextLease lease, CancellationToken cancellationToken); + /// /// 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 diff --git a/src/EFCore/Internal/ScopedDbContextLease.cs b/src/EFCore/Internal/ScopedDbContextLease.cs index f3093894e9d..77912cf2374 100644 --- a/src/EFCore/Internal/ScopedDbContextLease.cs +++ b/src/EFCore/Internal/ScopedDbContextLease.cs @@ -24,7 +24,10 @@ public sealed class ScopedDbContextLease : IScopedDbContextLease public ScopedDbContextLease(IDbContextPool contextPool) - => _lease = new DbContextLease(contextPool, standalone: false); + { + _lease = new DbContextLease(contextPool, standalone: false); + _lease.Context.SetLease(_lease); + } /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs index 636b30c99d3..c6ae8c730b9 100644 --- a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs @@ -13,10 +13,10 @@ 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 MethodHasAsyncOverload // ReSharper disable InconsistentNaming // ReSharper disable UnusedAutoPropertyAccessor.Local @@ -145,12 +145,25 @@ public override void Dispose() Interlocked.Increment(ref DisposedCount); } + } - public class Customer + private class PooledContextWithOverrides : DbContext, IPooledContext + { + public PooledContextWithOverrides(DbContextOptions options) + : base(options) { - public string CustomerId { get; set; } - public string CompanyName { get; set; } } + + public DbSet Customers { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity().ToTable("Customers"); + } + + public class Customer + { + public string CustomerId { get; set; } + public string CompanyName { get; set; } } private interface ISecondContext @@ -212,7 +225,7 @@ public void Validate_pool_size() scope.ServiceProvider .GetRequiredService() .GetService() - .FindExtension().MaxPoolSize); + .FindExtension()!.MaxPoolSize); } [ConditionalFact] @@ -227,7 +240,7 @@ public void Validate_pool_size_with_service_interface() ((DbContext)scope.ServiceProvider .GetRequiredService()) .GetService() - .FindExtension().MaxPoolSize); + .FindExtension()!.MaxPoolSize); } [ConditionalFact] @@ -240,7 +253,7 @@ public void Validate_pool_size_with_factory() Assert.Equal( 64, context.GetService() - .FindExtension().MaxPoolSize); + .FindExtension()!.MaxPoolSize); } [ConditionalTheory] @@ -272,7 +285,7 @@ public void Validate_pool_size_default() scope.ServiceProvider .GetRequiredService() .GetService() - .FindExtension().MaxPoolSize); + .FindExtension()!.MaxPoolSize); } [ConditionalFact] @@ -287,7 +300,7 @@ public void Validate_pool_size_with_service_interface_default() ((DbContext)scope.ServiceProvider .GetRequiredService()) .GetService() - .FindExtension().MaxPoolSize); + .FindExtension()!.MaxPoolSize); } [ConditionalFact] @@ -300,7 +313,7 @@ public void Validate_pool_size_with_factory_default() Assert.Equal( 1024, context.GetService() - .FindExtension().MaxPoolSize); + .FindExtension()!.MaxPoolSize); } [ConditionalTheory] @@ -340,7 +353,7 @@ public void Options_modified_in_on_configuring_with_factory() try { var factory = scopedProvider.GetService>(); - Assert.Throws(() => factory.CreateDbContext()); + Assert.Throws(() => factory!.CreateDbContext()); } finally { @@ -430,10 +443,10 @@ public async Task Can_pool_non_derived_context(bool useFactory, bool async) : BuildServiceProvider(); var serviceScope1 = serviceProvider.CreateScope(); - var context1 = GetContext(serviceScope1); + var context1 = await GetContextAsync(serviceScope1); var serviceScope2 = serviceProvider.CreateScope(); - var context2 = GetContext(serviceScope2); + var context2 = await GetContextAsync(serviceScope2); Assert.NotSame(context1, context2); @@ -468,7 +481,7 @@ public async Task Can_pool_non_derived_context(bool useFactory, bool async) Assert.Equal(1, id2d.Lease); var serviceScope3 = serviceProvider.CreateScope(); - var context3 = GetContext(serviceScope3); + var context3 = await GetContextAsync(serviceScope3); var id1r = context3.ContextId; @@ -479,7 +492,7 @@ public async Task Can_pool_non_derived_context(bool useFactory, bool async) Assert.Equal(2, id1r.Lease); var serviceScope4 = serviceProvider.CreateScope(); - var context4 = GetContext(serviceScope4); + var context4 = await GetContextAsync(serviceScope4); var id2r = context4.ContextId; @@ -489,9 +502,11 @@ public async Task Can_pool_non_derived_context(bool useFactory, bool async) Assert.NotEqual(id2, id2r); Assert.Equal(2, id2r.Lease); - DbContext GetContext(IServiceScope serviceScope) + async Task GetContextAsync(IServiceScope serviceScope) => useFactory - ? serviceScope.ServiceProvider.GetService>().CreateDbContext() + ? async + ? await serviceScope.ServiceProvider.GetService>()!.CreateDbContextAsync() + : serviceScope.ServiceProvider.GetService>()!.CreateDbContext() : serviceScope.ServiceProvider.GetService(); } @@ -515,8 +530,8 @@ public async Task ContextIds_make_sense_when_not_pooling(bool async) Assert.NotSame(context1, context2); - var id1 = context1.ContextId; - var id2 = context2.ContextId; + var id1 = context1!.ContextId; + var id2 = context2!.ContextId; Assert.NotEqual(default, id1.InstanceId); Assert.NotEqual(default, id2.InstanceId); @@ -596,6 +611,7 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.NotSame(secondContext1, secondContext2); await Dispose(serviceScope1, async); + await Dispose(serviceScope2, async); var serviceScope3 = serviceProvider.CreateScope(); @@ -625,6 +641,10 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Same(context2, context4); Assert.Same(secondContext2, secondContext4); + + await Dispose(serviceScope3, async); + + await Dispose(serviceScope4, async); } [ConditionalTheory] @@ -636,11 +656,11 @@ public async Task Contexts_are_pooled_with_factory(bool async, bool withDependen { var factory = BuildFactory(withDependencyInjection); - var context1 = factory.CreateDbContext(); - var secondContext1 = factory.CreateDbContext(); + var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); + var secondContext1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); - var context2 = factory.CreateDbContext(); - var secondContext2 = 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); @@ -650,17 +670,22 @@ public async Task Contexts_are_pooled_with_factory(bool async, bool withDependen await Dispose(context2, async); await Dispose(secondContext2, async); - var context3 = factory.CreateDbContext(); - var secondContext3 = factory.CreateDbContext(); + 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 = factory.CreateDbContext(); - var secondContext4 = factory.CreateDbContext(); + 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] @@ -678,10 +703,10 @@ public async Task Context_configuration_is_reset(bool useInterface, bool async) var scopedProvider = serviceScope.ServiceProvider; var context1 = useInterface - ? (DbContext)scopedProvider.GetService() + ? (PooledContext)scopedProvider.GetService() : scopedProvider.GetService(); - Assert.Null(context1.Database.GetCommandTimeout()); + Assert.Null(context1!.Database.GetCommandTimeout()); context1.ChangeTracker.AutoDetectChangesEnabled = true; context1.ChangeTracker.LazyLoadingEnabled = true; @@ -695,26 +720,22 @@ public async Task Context_configuration_is_reset(bool useInterface, bool async) context1.SavedChanges += (sender, args) => { }; context1.SaveChangesFailed += (sender, args) => { }; - Assert.NotNull(GetContextEventField(context1, nameof(DbContext.SavingChanges))); - Assert.NotNull(GetContextEventField(context1, nameof(DbContext.SavedChanges))); - Assert.NotNull(GetContextEventField(context1, nameof(DbContext.SaveChangesFailed))); - await Dispose(serviceScope, async); - Assert.Null(GetContextEventField(context1, nameof(DbContext.SavingChanges))); - Assert.Null(GetContextEventField(context1, nameof(DbContext.SavedChanges))); - Assert.Null(GetContextEventField(context1, nameof(DbContext.SaveChangesFailed))); - serviceScope = serviceProvider.CreateScope(); scopedProvider = serviceScope.ServiceProvider; var context2 = useInterface - ? (DbContext)scopedProvider.GetService() + ? (PooledContext)scopedProvider.GetService() : scopedProvider.GetService(); Assert.Same(context1, context2); - Assert.False(context2.ChangeTracker.AutoDetectChangesEnabled); + Assert.Null(GetContextEventField(context2, nameof(DbContext.SavingChanges))); + Assert.Null(GetContextEventField(context2, nameof(DbContext.SavedChanges))); + Assert.Null(GetContextEventField(context2, nameof(DbContext.SaveChangesFailed))); + + Assert.False(context2!.ChangeTracker.AutoDetectChangesEnabled); Assert.False(context2.ChangeTracker.LazyLoadingEnabled); Assert.Equal(QueryTrackingBehavior.TrackAll, context2.ChangeTracker.QueryTrackingBehavior); Assert.Equal(CascadeTiming.Never, context2.ChangeTracker.CascadeDeleteTiming); @@ -760,7 +781,7 @@ public async Task Context_configuration_is_reset_with_factory(bool async, bool w { var factory = BuildFactory(withDependencyInjection); - var context1 = factory.CreateDbContext(); + var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); context1.ChangeTracker.AutoDetectChangesEnabled = true; context1.ChangeTracker.LazyLoadingEnabled = true; @@ -773,27 +794,24 @@ public async Task Context_configuration_is_reset_with_factory(bool async, bool w context1.SavedChanges += (sender, args) => { }; context1.SaveChangesFailed += (sender, args) => { }; - Assert.NotNull(GetContextEventField(context1, nameof(DbContext.SavingChanges))); - Assert.NotNull(GetContextEventField(context1, nameof(DbContext.SavedChanges))); - Assert.NotNull(GetContextEventField(context1, nameof(DbContext.SaveChangesFailed))); - await Dispose(context1, async); - Assert.Null(GetContextEventField(context1, nameof(DbContext.SavingChanges))); - Assert.Null(GetContextEventField(context1, nameof(DbContext.SavedChanges))); - Assert.Null(GetContextEventField(context1, nameof(DbContext.SaveChangesFailed))); - - var context2 = factory.CreateDbContext(); + var context2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); Assert.Same(context1, context2); - Assert.False(context2.ChangeTracker.AutoDetectChangesEnabled); + Assert.Null(GetContextEventField(context2, nameof(DbContext.SavingChanges))); + Assert.Null(GetContextEventField(context2, nameof(DbContext.SavedChanges))); + Assert.Null(GetContextEventField(context2, nameof(DbContext.SaveChangesFailed))); + + 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); Assert.False(context2.Database.AutoSavepointsEnabled); + Assert.Null(context1.Database.GetCommandTimeout()); } [ConditionalFact] @@ -834,7 +852,7 @@ public void Change_tracker_can_be_cleared_without_resetting_context_config() Assert.False(_changeTracker_OnStateChanged); context.Customers.Attach( - new PooledContext.Customer { CustomerId = "C" }).State = EntityState.Modified; + new Customer { CustomerId = "C" }).State = EntityState.Modified; Assert.True(_changeTracker_OnTracked); Assert.True(_changeTracker_OnStateChanged); @@ -846,7 +864,7 @@ public void Change_tracker_can_be_cleared_without_resetting_context_config() private object GetContextEventField(DbContext context, string eventName) => typeof(DbContext) - .GetField(eventName, BindingFlags.GetField | BindingFlags.NonPublic | BindingFlags.Instance) + .GetField(eventName, BindingFlags.GetField | BindingFlags.NonPublic | BindingFlags.Instance)! .GetValue(context); private bool _changeTracker_OnTracked; @@ -871,7 +889,7 @@ public async Task Default_Context_configuration_is_reset(bool async) var context1 = scopedProvider.GetService(); - context1.ChangeTracker.AutoDetectChangesEnabled = false; + context1!.ChangeTracker.AutoDetectChangesEnabled = false; context1.ChangeTracker.LazyLoadingEnabled = false; context1.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; context1.Database.AutoTransactionsEnabled = false; @@ -888,7 +906,7 @@ public async Task Default_Context_configuration_is_reset(bool async) Assert.Same(context1, context2); - Assert.True(context2.ChangeTracker.AutoDetectChangesEnabled); + Assert.True(context2!.ChangeTracker.AutoDetectChangesEnabled); Assert.True(context2.ChangeTracker.LazyLoadingEnabled); Assert.Equal(QueryTrackingBehavior.TrackAll, context2.ChangeTracker.QueryTrackingBehavior); Assert.Equal(CascadeTiming.Immediate, context2.ChangeTracker.CascadeDeleteTiming); @@ -906,7 +924,7 @@ public async Task Default_Context_configuration_is_reset_with_factory(bool async { var factory = BuildFactory(withDependencyInjection); - var context1 = factory.CreateDbContext(); + var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); context1.ChangeTracker.AutoDetectChangesEnabled = false; context1.ChangeTracker.LazyLoadingEnabled = false; @@ -918,7 +936,7 @@ public async Task Default_Context_configuration_is_reset_with_factory(bool async await Dispose(context1, async); - var context2 = factory.CreateDbContext(); + var context2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); Assert.Same(context1, context2); @@ -991,7 +1009,7 @@ public async Task State_manager_is_reset_with_factory(bool async, bool withDepen { var factory = BuildFactory(withDependencyInjection); - var context1 = factory.CreateDbContext(); + var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); var entity = context1.Customers.First(c => c.CustomerId == "ALFKI"); @@ -999,7 +1017,7 @@ public async Task State_manager_is_reset_with_factory(bool async, bool withDepen await Dispose(context1, async); - var context2 = factory.CreateDbContext(); + var context2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); Assert.Same(context1, context2); Assert.Empty(context2.ChangeTracker.Entries()); @@ -1026,26 +1044,22 @@ public async Task Pool_disposes_context_when_context_not_pooled(bool useInterfac var serviceScope1 = serviceProvider.CreateScope(); var scopedProvider1 = serviceScope1.ServiceProvider; - if (useInterface) - { - scopedProvider1.GetService(); - } - else - { - scopedProvider1.GetService(); - } + var context1 = useInterface + ? (PooledContext)scopedProvider1.GetService() + : scopedProvider1.GetService(); var serviceScope2 = serviceProvider.CreateScope(); var scopedProvider2 = serviceScope2.ServiceProvider; - var context = useInterface + var context2 = useInterface ? (PooledContext)scopedProvider2.GetService() : scopedProvider2.GetService(); await Dispose(serviceScope1, async); await Dispose(serviceScope2, async); - Assert.Throws(() => context.Customers.ToList()); + Assert.Throws(() => context1.Customers.ToList()); + Assert.Throws(() => context2.Customers.ToList()); } [ConditionalTheory] @@ -1093,7 +1107,7 @@ public async Task Object_in_pool_is_disposed(bool useInterface, bool async) await Dispose(serviceScope, async); - Assert.Throws(() => context.Customers.ToList()); + Assert.Throws(() => context!.Customers.ToList()); } [ConditionalTheory] @@ -1112,6 +1126,7 @@ public async Task Double_dispose_does_not_enter_pool_twice(bool useInterface, bo var context = lease.Context; await Dispose(scope, async); + await Dispose(scope, async); using var scope1 = serviceProvider.CreateScope(); @@ -1137,9 +1152,11 @@ public async Task Double_dispose_with_standalone_lease_does_not_enter_pool_twice var pool = serviceProvider.GetRequiredService>(); var lease = new DbContextLease(pool, standalone: true); - var context = lease.Context; + 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; @@ -1158,7 +1175,7 @@ public async Task Can_double_dispose_with_factory(bool async, bool withDependenc { var factory = BuildFactory(withDependencyInjection); - var context = factory.CreateDbContext(); + var context = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); context.Customers.Load(); @@ -1189,7 +1206,7 @@ public async Task Provider_services_are_reset(bool useInterface, bool async) ? (PooledContext)scopedProvider.GetService() : scopedProvider.GetService(); - context1.Database.BeginTransaction(); + context1!.Database.BeginTransaction(); Assert.NotNull(context1.Database.CurrentTransaction); @@ -1203,7 +1220,7 @@ public async Task Provider_services_are_reset(bool useInterface, bool async) : scopedProvider.GetService(); Assert.Same(context1, context2); - Assert.Null(context2.Database.CurrentTransaction); + Assert.Null(context2!.Database.CurrentTransaction); context2.Database.BeginTransaction(); @@ -1219,7 +1236,7 @@ public async Task Provider_services_are_reset(bool useInterface, bool async) : scopedProvider.GetService(); Assert.Same(context2, context3); - Assert.Null(context3.Database.CurrentTransaction); + Assert.Null(context3!.Database.CurrentTransaction); } [ConditionalTheory] @@ -1231,7 +1248,7 @@ public async Task Provider_services_are_reset_with_factory(bool async, bool with { var factory = BuildFactory(withDependencyInjection); - var context1 = factory.CreateDbContext(); + var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); context1.Database.BeginTransaction(); @@ -1239,7 +1256,7 @@ public async Task Provider_services_are_reset_with_factory(bool async, bool with await Dispose(context1, async); - var context2 = factory.CreateDbContext(); + var context2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); Assert.Same(context1, context2); Assert.Null(context2.Database.CurrentTransaction); @@ -1250,7 +1267,7 @@ public async Task Provider_services_are_reset_with_factory(bool async, bool with await Dispose(context2, async); - var context3 = factory.CreateDbContext(); + var context3 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); Assert.Same(context2, context3); Assert.Null(context3.Database.CurrentTransaction); @@ -1308,7 +1325,7 @@ async Task ProcessRequest() ? (PooledContext)scopedProvider.GetService() : scopedProvider.GetService(); - await context.Customers.AsNoTracking().FirstAsync(c => c.CustomerId == "ALFKI"); + await context!.Customers.AsNoTracking().FirstAsync(c => c.CustomerId == "ALFKI"); Interlocked.Increment(ref _requests); }