From ab913e04c691e622e06eb21e1211aae6a7bba5d0 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Wed, 9 Jun 2021 13:18:54 -0700 Subject: [PATCH 1/4] Implements events/callbacks for leasing and returning a context to the pool. Fixes #19193 Notes: * We now no longer reset context events, as per #19193 * However, I left the code that resets options like QueryTrackingBehavior since that still seems useful, and makes this less of a break * Added events to DbContext. These are easy and decoupled, but sync only. * A derived DbContext can override the On... event methods. This allows async hooks. * DbContextFactory now has a CreateDbContextAsync method such that a context can be rented from the pool and the hook applied in an async context. --- src/EFCore/DbContext.cs | 126 +++- src/EFCore/IDbContextFactory.cs | 20 +- .../Infrastructure/PooledDbContextFactory.cs | 18 +- src/EFCore/Internal/DbContextLease.cs | 20 +- src/EFCore/Internal/IDbContextPoolable.cs | 10 + src/EFCore/Internal/ScopedDbContextLease.cs | 5 +- .../DbContextPoolingTest.cs | 714 ++++++++++++++++-- test/EFCore.Tests/DbContextTest.cs | 2 +- 8 files changed, 825 insertions(+), 90 deletions(-) diff --git a/src/EFCore/DbContext.cs b/src/EFCore/DbContext.cs index d774d657148..bd4cd5844c4 100644 --- a/src/EFCore/DbContext.cs +++ b/src/EFCore/DbContext.cs @@ -706,6 +706,70 @@ await DbContextDependencies.UpdateLogger.OptimisticConcurrencyExceptionAsync(thi /// public event EventHandler? SaveChangesFailed; + /// + /// + /// An event fired when this context instance is leased from the context pool. + /// + /// + /// This event is only fired when 'DbContext' pooling is enabled through use of + /// + /// or + /// . + /// + /// + public event EventHandler? LeasedFromPool; + + /// + /// Called to fire the event. Can be overriden in a derived context to intercept this event. + /// + protected virtual void OnLeasedFromPool() + => LeasedFromPool?.Invoke(this, EventArgs.Empty); + + /// + /// Called to fire the event. Can be overriden in a derived context to intercept this event. + /// + /// A to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. + /// If the is canceled. + protected virtual Task OnLeasedFromPoolAsync(CancellationToken cancellationToken) + { + LeasedFromPool?.Invoke(this, EventArgs.Empty); + + return Task.CompletedTask; + } + + /// + /// + /// An event fired when this context instance is returned to the context pool. + /// + /// + /// This event is only fired when 'DbContext' pooling is enabled through use of + /// + /// or + /// . + /// + /// + public event EventHandler? ReturnedToPool; + + /// + /// Called to fire the event. Can be overriden in a derived context to intercept this event. + /// + protected virtual void OnReturnedToPool() + => ReturnedToPool?.Invoke(this, EventArgs.Empty); + + /// + /// Called to fire the event. Can be overriden in a derived context to intercept this event. + /// + /// A to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. + /// If the is canceled. + protected virtual Task OnReturnedToPoolAsync(CancellationToken cancellationToken) + { + ReturnedToPool?.Invoke(this, EventArgs.Empty); + + return Task.CompletedTask; + } + /// /// 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 @@ -724,6 +788,27 @@ void IDbContextPoolable.ClearLease() /// [EntityFrameworkInternal] void IDbContextPoolable.SetLease(DbContextLease lease) + { + SetLeaseInternal(lease); + + OnLeasedFromPool(); + } + + /// + /// 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 OnLeasedFromPoolAsync(cancellationToken); + } + + private void SetLeaseInternal(DbContextLease lease) { _lease = lease; _disposed = false; @@ -789,13 +874,13 @@ void IDbContextPoolable.SnapshotConfiguration() [EntityFrameworkInternal] void IResettableService.ResetState() { + OnReturnedToPool(); + foreach (var service in GetResettableServices()) { service.ResetState(); } - ClearEvents(); - _disposed = true; } @@ -808,13 +893,13 @@ void IResettableService.ResetState() [EntityFrameworkInternal] async Task IResettableService.ResetStateAsync(CancellationToken cancellationToken) { + await OnReturnedToPoolAsync(cancellationToken); + foreach (var service in GetResettableServices()) { await service.ResetStateAsync(cancellationToken).ConfigureAwait(false); } - ClearEvents(); - _disposed = true; } @@ -851,22 +936,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; } } @@ -884,7 +969,9 @@ private bool DisposeSync() _changeTracker = null; _database = null; - ClearEvents(); + SavingChanges = null; + SavedChanges = null; + SaveChangesFailed = null; return true; } @@ -895,14 +982,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/PooledDbContextFactory.cs b/src/EFCore/Infrastructure/PooledDbContextFactory.cs index 49bcc05157a..04680e451a3 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..45b0de5a63a 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 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..1b02767b984 100644 --- a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs @@ -13,7 +13,6 @@ 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; @@ -96,6 +95,12 @@ private static IDbContextFactory BuildFactory(bool withDepen private interface IPooledContext { + int LeasedCount { get; } + int ReturnedCount { get; } + public bool? AsyncLease { get; } + public bool? SyncLease { get; } + public bool? AsyncReturn { get; } + public bool? SyncReturn { get; } } private class DefaultOptionsPooledContext : DbContext @@ -124,8 +129,24 @@ public PooledContext(DbContextOptions options) Database.AutoSavepointsEnabled = false; ChangeTracker.CascadeDeleteTiming = CascadeTiming.Never; ChangeTracker.DeleteOrphansTiming = CascadeTiming.Never; + + LeasedFromPool += OnLeasedFromPool; + ReturnedToPool += OnReturnedToPool; } + public int LeasedCount { get; private set; } + public int ReturnedCount { get; private set; } + public bool? AsyncLease { get; private set; } + public bool? SyncLease { get; private set; } + public bool? AsyncReturn { get; private set; } + public bool? SyncReturn { get; private set; } + + private void OnLeasedFromPool(object sender, EventArgs e) + => LeasedCount++; + + private void OnReturnedToPool(object sender, EventArgs e) + => ReturnedCount++; + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (ModifyOptions) @@ -145,16 +166,74 @@ 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 int LeasedCount { get; private set; } + public int ReturnedCount { get; private set; } + public bool? AsyncLease { get; private set; } + public bool? SyncLease { get; private set; } + public bool? AsyncReturn { get; private set; } + public bool? SyncReturn { get; private set; } + + public DbSet Customers { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + => modelBuilder.Entity().ToTable("Customers"); + + protected override void OnLeasedFromPool() + { + LeasedCount++; + SyncLease = true; + + base.OnLeasedFromPool(); + } + + protected override Task OnLeasedFromPoolAsync(CancellationToken cancellationToken) + { + LeasedCount++; + AsyncLease = true; + + return base.OnLeasedFromPoolAsync(cancellationToken); + } + + protected override void OnReturnedToPool() + { + ReturnedCount++; + SyncReturn = true; + + base.OnReturnedToPool(); + } + + protected override Task OnReturnedToPoolAsync(CancellationToken cancellationToken) + { + ReturnedCount++; + AsyncReturn = true; + + return base.OnReturnedToPoolAsync(cancellationToken); } } + public class Customer + { + public string CustomerId { get; set; } + public string CompanyName { get; set; } + } + private interface ISecondContext { + int LeasedCount { get; } + int ReturnedCount { get; } + public bool? AsyncLease { get; } + public bool? SyncLease { get; } + public bool? AsyncReturn { get; } + public bool? SyncReturn { get; } } private class SecondContext : DbContext, ISecondContext @@ -164,6 +243,68 @@ private class SecondContext : DbContext, ISecondContext public SecondContext(DbContextOptions options) : base(options) { + LeasedFromPool += OnLeasedFromPool; + ReturnedToPool += OnReturnedToPool; + } + + public int LeasedCount { get; private set; } + public int ReturnedCount { get; private set; } + public bool? AsyncLease { get; private set; } + public bool? SyncLease { get; private set; } + public bool? AsyncReturn { get; private set; } + public bool? SyncReturn { get; private set; } + + private void OnLeasedFromPool(object sender, EventArgs e) + => LeasedCount++; + + private void OnReturnedToPool(object sender, EventArgs e) + => ReturnedCount++; + } + + private class SecondContextWithOverrides : DbContext, ISecondContext + { + public SecondContextWithOverrides(DbContextOptions options) + : base(options) + { + } + + public int LeasedCount { get; private set; } + public int ReturnedCount { get; private set; } + public bool? AsyncLease { get; private set; } + public bool? SyncLease { get; private set; } + public bool? AsyncReturn { get; private set; } + public bool? SyncReturn { get; private set; } + + protected override void OnLeasedFromPool() + { + LeasedCount++; + SyncLease = true; + + base.OnLeasedFromPool(); + } + + protected override Task OnLeasedFromPoolAsync(CancellationToken cancellationToken) + { + LeasedCount++; + AsyncLease = true; + + return base.OnLeasedFromPoolAsync(cancellationToken); + } + + protected override void OnReturnedToPool() + { + ReturnedCount++; + SyncReturn = true; + + base.OnReturnedToPool(); + } + + protected override Task OnReturnedToPoolAsync(CancellationToken cancellationToken) + { + ReturnedCount++; + AsyncReturn = true; + + return base.OnReturnedToPoolAsync(cancellationToken); } public class Blog @@ -212,7 +353,7 @@ public void Validate_pool_size() scope.ServiceProvider .GetRequiredService() .GetService() - .FindExtension().MaxPoolSize); + .FindExtension()!.MaxPoolSize); } [ConditionalFact] @@ -227,7 +368,7 @@ public void Validate_pool_size_with_service_interface() ((DbContext)scope.ServiceProvider .GetRequiredService()) .GetService() - .FindExtension().MaxPoolSize); + .FindExtension()!.MaxPoolSize); } [ConditionalFact] @@ -240,7 +381,7 @@ public void Validate_pool_size_with_factory() Assert.Equal( 64, context.GetService() - .FindExtension().MaxPoolSize); + .FindExtension()!.MaxPoolSize); } [ConditionalTheory] @@ -272,7 +413,7 @@ public void Validate_pool_size_default() scope.ServiceProvider .GetRequiredService() .GetService() - .FindExtension().MaxPoolSize); + .FindExtension()!.MaxPoolSize); } [ConditionalFact] @@ -287,7 +428,7 @@ public void Validate_pool_size_with_service_interface_default() ((DbContext)scope.ServiceProvider .GetRequiredService()) .GetService() - .FindExtension().MaxPoolSize); + .FindExtension()!.MaxPoolSize); } [ConditionalFact] @@ -300,7 +441,7 @@ public void Validate_pool_size_with_factory_default() Assert.Equal( 1024, context.GetService() - .FindExtension().MaxPoolSize); + .FindExtension()!.MaxPoolSize); } [ConditionalTheory] @@ -340,7 +481,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 +571,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 +609,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 +620,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 +630,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 +658,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); @@ -577,10 +720,18 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) ? scopedProvider1.GetService() : scopedProvider1.GetService(); + Assert.Equal(1, context1!.LeasedCount); + Assert.Equal(0, context1.ReturnedCount); + var secondContext1 = useInterface ? scopedProvider1.GetService() : scopedProvider1.GetService(); + Assert.Equal(1, context1.LeasedCount); + Assert.Equal(1, secondContext1!.LeasedCount); + Assert.Equal(0, context1.ReturnedCount); + Assert.Equal(0, secondContext1.ReturnedCount); + var serviceScope2 = serviceProvider.CreateScope(); var scopedProvider2 = serviceScope2.ServiceProvider; @@ -588,6 +739,13 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) ? scopedProvider2.GetService() : scopedProvider2.GetService(); + Assert.Equal(1, context1.LeasedCount); + Assert.Equal(1, secondContext1.LeasedCount); + Assert.Equal(1, context2!.LeasedCount); + Assert.Equal(0, context1.ReturnedCount); + Assert.Equal(0, secondContext1.ReturnedCount); + Assert.Equal(0, context2.ReturnedCount); + var secondContext2 = useInterface ? scopedProvider2.GetService() : scopedProvider2.GetService(); @@ -595,9 +753,37 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.NotSame(context1, context2); Assert.NotSame(secondContext1, secondContext2); + Assert.Equal(1, context1.LeasedCount); + Assert.Equal(1, secondContext1.LeasedCount); + Assert.Equal(1, context2.LeasedCount); + Assert.Equal(1, secondContext2!.LeasedCount); + Assert.Equal(0, context1.ReturnedCount); + Assert.Equal(0, secondContext1.ReturnedCount); + Assert.Equal(0, context2.ReturnedCount); + Assert.Equal(0, secondContext2.ReturnedCount); + await Dispose(serviceScope1, async); + + Assert.Equal(1, context1.LeasedCount); + Assert.Equal(1, secondContext1.LeasedCount); + Assert.Equal(1, context2.LeasedCount); + Assert.Equal(1, secondContext2.LeasedCount); + Assert.Equal(1, context1.ReturnedCount); + Assert.Equal(1, secondContext1.ReturnedCount); + Assert.Equal(0, context2.ReturnedCount); + Assert.Equal(0, secondContext2.ReturnedCount); + await Dispose(serviceScope2, async); + Assert.Equal(1, context1.LeasedCount); + Assert.Equal(1, secondContext1.LeasedCount); + Assert.Equal(1, context2.LeasedCount); + Assert.Equal(1, secondContext2!.LeasedCount); + Assert.Equal(1, context1.ReturnedCount); + Assert.Equal(1, secondContext1.ReturnedCount); + Assert.Equal(1, context2.ReturnedCount); + Assert.Equal(1, secondContext2.ReturnedCount); + var serviceScope3 = serviceProvider.CreateScope(); var scopedProvider3 = serviceScope3.ServiceProvider; @@ -612,6 +798,15 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Same(context1, context3); Assert.Same(secondContext1, secondContext3); + Assert.Equal(2, context1.LeasedCount); + Assert.Equal(2, secondContext1.LeasedCount); + Assert.Equal(1, context2.LeasedCount); + Assert.Equal(1, secondContext2!.LeasedCount); + Assert.Equal(1, context1.ReturnedCount); + Assert.Equal(1, secondContext1.ReturnedCount); + Assert.Equal(1, context2.ReturnedCount); + Assert.Equal(1, secondContext2.ReturnedCount); + var serviceScope4 = serviceProvider.CreateScope(); var scopedProvider4 = serviceScope4.ServiceProvider; @@ -625,6 +820,37 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Same(context2, context4); Assert.Same(secondContext2, secondContext4); + + Assert.Equal(2, context1.LeasedCount); + Assert.Equal(2, secondContext1.LeasedCount); + Assert.Equal(2, context2.LeasedCount); + Assert.Equal(2, secondContext2!.LeasedCount); + Assert.Equal(1, context1.ReturnedCount); + Assert.Equal(1, secondContext1.ReturnedCount); + Assert.Equal(1, context2.ReturnedCount); + Assert.Equal(1, secondContext2.ReturnedCount); + + await Dispose(serviceScope3, async); + + Assert.Equal(2, context1.LeasedCount); + Assert.Equal(2, secondContext1.LeasedCount); + Assert.Equal(2, context2.LeasedCount); + Assert.Equal(2, secondContext2!.LeasedCount); + Assert.Equal(2, context1.ReturnedCount); + Assert.Equal(2, secondContext1.ReturnedCount); + Assert.Equal(1, context2.ReturnedCount); + Assert.Equal(1, secondContext2.ReturnedCount); + + await Dispose(serviceScope4, async); + + Assert.Equal(2, context1.LeasedCount); + Assert.Equal(2, secondContext1.LeasedCount); + Assert.Equal(2, context2.LeasedCount); + Assert.Equal(2, secondContext2!.LeasedCount); + Assert.Equal(2, context1.ReturnedCount); + Assert.Equal(2, secondContext1.ReturnedCount); + Assert.Equal(2, context2.ReturnedCount); + Assert.Equal(2, secondContext2.ReturnedCount); } [ConditionalTheory] @@ -636,31 +862,362 @@ 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 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); + var secondContext2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); + + Assert.NotSame(context1, context2); + Assert.NotSame(secondContext1, secondContext2); + + Assert.Equal(1, context1.LeasedCount); + Assert.Equal(1, secondContext1.LeasedCount); + Assert.Equal(1, context2.LeasedCount); + Assert.Equal(1, secondContext2!.LeasedCount); + Assert.Equal(0, context1.ReturnedCount); + Assert.Equal(0, secondContext1.ReturnedCount); + Assert.Equal(0, context2.ReturnedCount); + Assert.Equal(0, secondContext2.ReturnedCount); + + await Dispose(context1, async); + await Dispose(secondContext1, async); + await Dispose(context2, async); + await Dispose(secondContext2, async); + + Assert.Equal(1, context1.LeasedCount); + Assert.Equal(1, secondContext1.LeasedCount); + Assert.Equal(1, context2.LeasedCount); + Assert.Equal(1, secondContext2!.LeasedCount); + Assert.Equal(1, context1.ReturnedCount); + Assert.Equal(1, secondContext1.ReturnedCount); + Assert.Equal(1, context2.ReturnedCount); + Assert.Equal(1, secondContext2.ReturnedCount); + + var context3 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); + var secondContext3 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); + + Assert.Same(context1, context3); + Assert.Same(secondContext1, secondContext3); + + var context4 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); + var secondContext4 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); + + Assert.Same(context2, context4); + Assert.Same(secondContext2, secondContext4); + + Assert.Equal(2, context1.LeasedCount); + Assert.Equal(2, secondContext1.LeasedCount); + Assert.Equal(2, context2.LeasedCount); + Assert.Equal(2, secondContext2!.LeasedCount); + Assert.Equal(1, context1.ReturnedCount); + Assert.Equal(1, secondContext1.ReturnedCount); + Assert.Equal(1, context2.ReturnedCount); + Assert.Equal(1, secondContext2.ReturnedCount); + + await Dispose(context1, async); + await Dispose(secondContext1, async); + await Dispose(context2, async); + await Dispose(secondContext2, async); + + Assert.Equal(2, context1.LeasedCount); + Assert.Equal(2, secondContext1.LeasedCount); + Assert.Equal(2, context2.LeasedCount); + Assert.Equal(2, secondContext2!.LeasedCount); + Assert.Equal(2, context1.ReturnedCount); + Assert.Equal(2, secondContext1.ReturnedCount); + Assert.Equal(2, context2.ReturnedCount); + Assert.Equal(2, secondContext2.ReturnedCount); + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public async Task Contexts_are_pooled_with_event_overrides(bool useInterface, bool async) + { + var serviceProvider = useInterface + ? new ServiceCollection() + .AddDbContextPool(ob => ConfigureOptions(ob)) + .AddDbContextPool(ob => ConfigureOptions(ob)) + .BuildServiceProvider() + : new ServiceCollection() + .AddDbContextPool(ob => ConfigureOptions(ob)) + .AddDbContextPool(ob => ConfigureOptions(ob)) + .BuildServiceProvider(); + + var serviceScope1 = serviceProvider.CreateScope(); + var scopedProvider1 = serviceScope1.ServiceProvider; + + var context1 = useInterface + ? scopedProvider1.GetService() + : scopedProvider1.GetService(); + + Assert.Equal(1, context1!.LeasedCount); + Assert.Equal(0, context1.ReturnedCount); + Assert.True(context1.SyncLease); + Assert.Null(context1.AsyncLease); + Assert.Null(context1.SyncReturn); + Assert.Null(context1.AsyncReturn); + + var secondContext1 = useInterface + ? scopedProvider1.GetService() + : scopedProvider1.GetService(); + + Assert.Equal(1, context1.LeasedCount); + Assert.Equal(1, secondContext1!.LeasedCount); + Assert.Equal(0, context1.ReturnedCount); + Assert.Equal(0, secondContext1.ReturnedCount); + Assert.True(context1.SyncLease); + Assert.Null(context1.AsyncLease); + Assert.Null(context1.SyncReturn); + Assert.Null(context1.AsyncReturn); + + var serviceScope2 = serviceProvider.CreateScope(); + var scopedProvider2 = serviceScope2.ServiceProvider; + + var context2 = useInterface + ? scopedProvider2.GetService() + : scopedProvider2.GetService(); + + Assert.Equal(1, context1.LeasedCount); + Assert.Equal(1, secondContext1.LeasedCount); + Assert.Equal(1, context2!.LeasedCount); + Assert.Equal(0, context1.ReturnedCount); + Assert.Equal(0, secondContext1.ReturnedCount); + Assert.Equal(0, context2.ReturnedCount); + Assert.True(context1.SyncLease); + Assert.Null(context1.AsyncLease); + Assert.Null(context1.SyncReturn); + Assert.Null(context1.AsyncReturn); + + var secondContext2 = useInterface + ? scopedProvider2.GetService() + : scopedProvider2.GetService(); + + Assert.NotSame(context1, context2); + Assert.NotSame(secondContext1, secondContext2); + + Assert.Equal(1, context1.LeasedCount); + Assert.Equal(1, secondContext1.LeasedCount); + Assert.Equal(1, context2.LeasedCount); + Assert.Equal(1, secondContext2!.LeasedCount); + Assert.Equal(0, context1.ReturnedCount); + Assert.Equal(0, secondContext1.ReturnedCount); + Assert.Equal(0, context2.ReturnedCount); + Assert.Equal(0, secondContext2.ReturnedCount); + Assert.True(context1.SyncLease); + Assert.Null(context1.AsyncLease); + Assert.Null(context1.SyncReturn); + Assert.Null(context1.AsyncReturn); + + await Dispose(serviceScope1, async); + + Assert.Equal(1, context1.LeasedCount); + Assert.Equal(1, secondContext1.LeasedCount); + Assert.Equal(1, context2.LeasedCount); + Assert.Equal(1, secondContext2.LeasedCount); + Assert.Equal(1, context1.ReturnedCount); + Assert.Equal(1, secondContext1.ReturnedCount); + Assert.Equal(0, context2.ReturnedCount); + Assert.Equal(0, secondContext2.ReturnedCount); + Assert.True(context1.SyncLease); + Assert.Null(context1.AsyncLease); + Assert.Equal(!async ? true : null, context1.SyncReturn); + Assert.Equal(async ? true : null, context1.AsyncReturn); + + await Dispose(serviceScope2, async); + + Assert.Equal(1, context1.LeasedCount); + Assert.Equal(1, secondContext1.LeasedCount); + Assert.Equal(1, context2.LeasedCount); + Assert.Equal(1, secondContext2!.LeasedCount); + Assert.Equal(1, context1.ReturnedCount); + Assert.Equal(1, secondContext1.ReturnedCount); + Assert.Equal(1, context2.ReturnedCount); + Assert.Equal(1, secondContext2.ReturnedCount); + Assert.True(context1.SyncLease); + Assert.Null(context1.AsyncLease); + Assert.Equal(!async ? true : null, context1.SyncReturn); + Assert.Equal(async ? true : null, context1.AsyncReturn); + + 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); + + Assert.Equal(2, context1.LeasedCount); + Assert.Equal(2, secondContext1.LeasedCount); + Assert.Equal(1, context2.LeasedCount); + Assert.Equal(1, secondContext2!.LeasedCount); + Assert.Equal(1, context1.ReturnedCount); + Assert.Equal(1, secondContext1.ReturnedCount); + Assert.Equal(1, context2.ReturnedCount); + Assert.Equal(1, secondContext2.ReturnedCount); + Assert.True(context1.SyncLease); + Assert.Null(context1.AsyncLease); + Assert.Equal(!async ? true : null, context1.SyncReturn); + Assert.Equal(async ? true : null, context1.AsyncReturn); + + 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); + + Assert.Equal(2, context1.LeasedCount); + Assert.Equal(2, secondContext1.LeasedCount); + Assert.Equal(2, context2.LeasedCount); + Assert.Equal(2, secondContext2!.LeasedCount); + Assert.Equal(1, context1.ReturnedCount); + Assert.Equal(1, secondContext1.ReturnedCount); + Assert.Equal(1, context2.ReturnedCount); + Assert.Equal(1, secondContext2.ReturnedCount); + Assert.True(context1.SyncLease); + Assert.Null(context1.AsyncLease); + Assert.Equal(!async ? true : null, context1.SyncReturn); + Assert.Equal(async ? true : null, context1.AsyncReturn); + + await Dispose(serviceScope3, async); + + Assert.Equal(2, context1.LeasedCount); + Assert.Equal(2, secondContext1.LeasedCount); + Assert.Equal(2, context2.LeasedCount); + Assert.Equal(2, secondContext2!.LeasedCount); + Assert.Equal(2, context1.ReturnedCount); + Assert.Equal(2, secondContext1.ReturnedCount); + Assert.Equal(1, context2.ReturnedCount); + Assert.Equal(1, secondContext2.ReturnedCount); + Assert.True(context1.SyncLease); + Assert.Null(context1.AsyncLease); + Assert.Equal(!async ? true : null, context1.SyncReturn); + Assert.Equal(async ? true : null, context1.AsyncReturn); + + await Dispose(serviceScope4, async); + + Assert.Equal(2, context1.LeasedCount); + Assert.Equal(2, secondContext1.LeasedCount); + Assert.Equal(2, context2.LeasedCount); + Assert.Equal(2, secondContext2!.LeasedCount); + Assert.Equal(2, context1.ReturnedCount); + Assert.Equal(2, secondContext1.ReturnedCount); + Assert.Equal(2, context2.ReturnedCount); + Assert.Equal(2, secondContext2.ReturnedCount); + Assert.True(context1.SyncLease); + Assert.Null(context1.AsyncLease); + Assert.Equal(!async ? true : null, context1.SyncReturn); + Assert.Equal(async ? true : null, context1.AsyncReturn); + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public async Task Contexts_are_pooled_with_factory_with_event_overrides(bool async, bool withDependencyInjection) + { + var factory = BuildFactory(withDependencyInjection); + + var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); + var secondContext1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); - var context2 = 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); + Assert.Equal(1, context1.LeasedCount); + Assert.Equal(1, secondContext1.LeasedCount); + Assert.Equal(1, context2.LeasedCount); + Assert.Equal(1, secondContext2!.LeasedCount); + Assert.Equal(0, context1.ReturnedCount); + Assert.Equal(0, secondContext1.ReturnedCount); + Assert.Equal(0, context2.ReturnedCount); + Assert.Equal(0, secondContext2.ReturnedCount); + Assert.Equal(!async ? true : null, context1.SyncLease); + Assert.Equal(async ? true : null, context1.AsyncLease); + Assert.Null(context1.SyncReturn); + Assert.Null(context1.AsyncReturn); + await Dispose(context1, async); await Dispose(secondContext1, async); await Dispose(context2, async); await Dispose(secondContext2, async); - var context3 = factory.CreateDbContext(); - var secondContext3 = factory.CreateDbContext(); + Assert.Equal(1, context1.LeasedCount); + Assert.Equal(1, secondContext1.LeasedCount); + Assert.Equal(1, context2.LeasedCount); + Assert.Equal(1, secondContext2!.LeasedCount); + Assert.Equal(1, context1.ReturnedCount); + Assert.Equal(1, secondContext1.ReturnedCount); + Assert.Equal(1, context2.ReturnedCount); + Assert.Equal(1, secondContext2.ReturnedCount); + Assert.Equal(!async ? true : null, context1.SyncLease); + Assert.Equal(async ? true : null, context1.AsyncLease); + Assert.Equal(!async ? true : null, context1.SyncReturn); + Assert.Equal(async ? true : null, context1.AsyncReturn); + + 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); + + Assert.Equal(2, context1.LeasedCount); + Assert.Equal(2, secondContext1.LeasedCount); + Assert.Equal(2, context2.LeasedCount); + Assert.Equal(2, secondContext2!.LeasedCount); + Assert.Equal(1, context1.ReturnedCount); + Assert.Equal(1, secondContext1.ReturnedCount); + Assert.Equal(1, context2.ReturnedCount); + Assert.Equal(1, secondContext2.ReturnedCount); + Assert.Equal(!async ? true : null, context1.SyncLease); + Assert.Equal(async ? true : null, context1.AsyncLease); + Assert.Equal(!async ? true : null, context1.SyncReturn); + Assert.Equal(async ? true : null, context1.AsyncReturn); + + await Dispose(context1, async); + await Dispose(secondContext1, async); + await Dispose(context2, async); + await Dispose(secondContext2, async); + + Assert.Equal(2, context1.LeasedCount); + Assert.Equal(2, secondContext1.LeasedCount); + Assert.Equal(2, context2.LeasedCount); + Assert.Equal(2, secondContext2!.LeasedCount); + Assert.Equal(2, context1.ReturnedCount); + Assert.Equal(2, secondContext1.ReturnedCount); + Assert.Equal(2, context2.ReturnedCount); + Assert.Equal(2, secondContext2.ReturnedCount); + Assert.Equal(!async ? true : null, context1.SyncLease); + Assert.Equal(async ? true : null, context1.AsyncLease); + Assert.Equal(!async ? true : null, context1.SyncReturn); + Assert.Equal(async ? true : null, context1.AsyncReturn); } [ConditionalTheory] @@ -681,7 +1238,7 @@ public async Task Context_configuration_is_reset(bool useInterface, bool async) ? (DbContext)scopedProvider.GetService() : scopedProvider.GetService(); - Assert.Null(context1.Database.GetCommandTimeout()); + Assert.Null(context1!.Database.GetCommandTimeout()); context1.ChangeTracker.AutoDetectChangesEnabled = true; context1.ChangeTracker.LazyLoadingEnabled = true; @@ -701,9 +1258,9 @@ public async Task Context_configuration_is_reset(bool useInterface, bool async) 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))); + Assert.NotNull(GetContextEventField(context1, nameof(DbContext.SavingChanges))); + Assert.NotNull(GetContextEventField(context1, nameof(DbContext.SavedChanges))); + Assert.NotNull(GetContextEventField(context1, nameof(DbContext.SaveChangesFailed))); serviceScope = serviceProvider.CreateScope(); scopedProvider = serviceScope.ServiceProvider; @@ -714,7 +1271,7 @@ public async Task Context_configuration_is_reset(bool useInterface, bool async) Assert.Same(context1, context2); - Assert.False(context2.ChangeTracker.AutoDetectChangesEnabled); + 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 +1317,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; @@ -779,11 +1336,11 @@ public async Task Context_configuration_is_reset_with_factory(bool async, bool w 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))); + Assert.NotNull(GetContextEventField(context1, nameof(DbContext.SavingChanges))); + Assert.NotNull(GetContextEventField(context1, nameof(DbContext.SavedChanges))); + Assert.NotNull(GetContextEventField(context1, nameof(DbContext.SaveChangesFailed))); - var context2 = factory.CreateDbContext(); + var context2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); Assert.Same(context1, context2); @@ -834,7 +1391,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 +1403,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 +1428,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 +1445,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 +1463,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 +1475,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 +1548,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 +1556,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 +1583,32 @@ 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(); + Assert.Equal(1, context1!.LeasedCount); + Assert.Equal(1, context2!.LeasedCount); + Assert.Equal(0, context1.ReturnedCount); + Assert.Equal(0, context2.ReturnedCount); + await Dispose(serviceScope1, async); await Dispose(serviceScope2, async); - Assert.Throws(() => context.Customers.ToList()); + Assert.Equal(1, context1!.LeasedCount); + Assert.Equal(1, context2!.LeasedCount); + Assert.Equal(1, context1.ReturnedCount); + Assert.Equal(1, context2.ReturnedCount); + + Assert.Throws(() => context1.Customers.ToList()); + Assert.Throws(() => context2.Customers.ToList()); } [ConditionalTheory] @@ -1093,7 +1656,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] @@ -1111,9 +1674,19 @@ public async Task Double_dispose_does_not_enter_pool_twice(bool useInterface, bo var lease = scope.ServiceProvider.GetRequiredService>(); var context = lease.Context; + Assert.Equal(1, context.LeasedCount); + Assert.Equal(0, context.ReturnedCount); + await Dispose(scope, async); + + Assert.Equal(1, context.LeasedCount); + Assert.Equal(1, context.ReturnedCount); + await Dispose(scope, async); + Assert.Equal(1, context.LeasedCount); + Assert.Equal(1, context.ReturnedCount); + using var scope1 = serviceProvider.CreateScope(); var lease1 = scope1.ServiceProvider.GetRequiredService>(); @@ -1137,11 +1710,22 @@ 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); + + Assert.Equal(1, context.LeasedCount); + Assert.Equal(0, context.ReturnedCount); await Dispose(context, async); + + Assert.Equal(1, context.LeasedCount); + Assert.Equal(1, context.ReturnedCount); + await Dispose(context, async); + Assert.Equal(1, context.LeasedCount); + Assert.Equal(1, context.ReturnedCount); + using var context1 = new DbContextLease(pool, standalone: true).Context; using var context2 = new DbContextLease(pool, standalone: true).Context; @@ -1158,7 +1742,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 +1773,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 +1787,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 +1803,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 +1815,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 +1823,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 +1834,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 +1892,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); } diff --git a/test/EFCore.Tests/DbContextTest.cs b/test/EFCore.Tests/DbContextTest.cs index 1763731efd8..29f1033d896 100644 --- a/test/EFCore.Tests/DbContextTest.cs +++ b/test/EFCore.Tests/DbContextTest.cs @@ -763,7 +763,7 @@ public async Task It_throws_object_disposed_exception(bool async) (await Assert.ThrowsAsync(() => context.FindAsync(typeof(Random), 77).AsTask())).Message); var methodCount = typeof(DbContext).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).Count(); - var expectedMethodCount = 50; + var expectedMethodCount = 54; Assert.True( methodCount == expectedMethodCount, userMessage: $"Expected {expectedMethodCount} methods on DbContext but found {methodCount}. " From 3ac3716823e8171d29173cef1ccdeff27de93f88 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Tue, 27 Apr 2021 08:38:47 -0700 Subject: [PATCH 2/4] Add asynchronous events --- src/EFCore/DbContext.cs | 62 ++++++++-- .../DbContextPoolingTest.cs | 112 ++++++++++++++++-- test/EFCore.Tests/DbContextTest.cs | 2 +- 3 files changed, 154 insertions(+), 22 deletions(-) diff --git a/src/EFCore/DbContext.cs b/src/EFCore/DbContext.cs index bd4cd5844c4..19032fc992c 100644 --- a/src/EFCore/DbContext.cs +++ b/src/EFCore/DbContext.cs @@ -717,25 +717,43 @@ await DbContextDependencies.UpdateLogger.OptimisticConcurrencyExceptionAsync(thi /// . /// /// - public event EventHandler? LeasedFromPool; + public event Action? LeasedFromPool; /// /// Called to fire the event. Can be overriden in a derived context to intercept this event. /// protected virtual void OnLeasedFromPool() - => LeasedFromPool?.Invoke(this, EventArgs.Empty); + => LeasedFromPool?.Invoke(this); /// - /// Called to fire the event. Can be overriden in a derived context to intercept this event. + /// + /// An event fired when this context instance is leased from the context pool. + /// + /// + /// This event is only fired when 'DbContext' pooling is enabled through use of + /// + /// or + /// . + /// + /// + public event Func? LeasedFromPoolAsync; + + /// + /// Called to fire the event. Can be overriden in a derived context to intercept this event. /// /// A to observe while waiting for the task to complete. /// A task that represents the asynchronous operation. /// If the is canceled. - protected virtual Task OnLeasedFromPoolAsync(CancellationToken cancellationToken) + protected virtual async Task OnLeasedFromPoolAsync(CancellationToken cancellationToken) { - LeasedFromPool?.Invoke(this, EventArgs.Empty); - - return Task.CompletedTask; + var handler = LeasedFromPoolAsync; + if (handler != null) + { + foreach (var func in handler.GetInvocationList()) + { + await ((Func)func)(this, cancellationToken); + } + } } /// @@ -749,13 +767,26 @@ protected virtual Task OnLeasedFromPoolAsync(CancellationToken cancellationToken /// . /// /// - public event EventHandler? ReturnedToPool; + public event Action? ReturnedToPool; /// /// Called to fire the event. Can be overriden in a derived context to intercept this event. /// protected virtual void OnReturnedToPool() - => ReturnedToPool?.Invoke(this, EventArgs.Empty); + => ReturnedToPool?.Invoke(this); + + /// + /// + /// An event fired when this context instance is returned to the context pool. + /// + /// + /// This event is only fired when 'DbContext' pooling is enabled through use of + /// + /// or + /// . + /// + /// + public event Func? ReturnedToPoolAsync; /// /// Called to fire the event. Can be overriden in a derived context to intercept this event. @@ -763,11 +794,16 @@ protected virtual void OnReturnedToPool() /// A to observe while waiting for the task to complete. /// A task that represents the asynchronous operation. /// If the is canceled. - protected virtual Task OnReturnedToPoolAsync(CancellationToken cancellationToken) + protected virtual async Task OnReturnedToPoolAsync(CancellationToken cancellationToken) { - ReturnedToPool?.Invoke(this, EventArgs.Empty); - - return Task.CompletedTask; + var handler = ReturnedToPoolAsync; + if (handler != null) + { + foreach (var func in handler.GetInvocationList()) + { + await ((Func)func)(this, cancellationToken); + } + } } /// diff --git a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs index 1b02767b984..26cddbc726f 100644 --- a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs @@ -132,6 +132,8 @@ public PooledContext(DbContextOptions options) LeasedFromPool += OnLeasedFromPool; ReturnedToPool += OnReturnedToPool; + LeasedFromPoolAsync += OnLeasedFromPoolAsync; + ReturnedToPoolAsync += OnReturnedToPoolAsync; } public int LeasedCount { get; private set; } @@ -141,11 +143,33 @@ public PooledContext(DbContextOptions options) public bool? AsyncReturn { get; private set; } public bool? SyncReturn { get; private set; } - private void OnLeasedFromPool(object sender, EventArgs e) - => LeasedCount++; + private void OnLeasedFromPool(object sender) + { + SyncLease = true; + LeasedCount++; + } + + private async Task OnLeasedFromPoolAsync(DbContext sender, CancellationToken cancellationToken) + { + await sender.Database.CanConnectAsync(cancellationToken); // Just something async + + AsyncLease = true; + LeasedCount++; + } + + private void OnReturnedToPool(object sender) + { + SyncReturn = true; + ReturnedCount++; + } - private void OnReturnedToPool(object sender, EventArgs e) - => ReturnedCount++; + private async Task OnReturnedToPoolAsync(DbContext sender, CancellationToken cancellationToken) + { + await sender.Database.CanConnectAsync(cancellationToken); // Just something async + + AsyncReturn = true; + ReturnedCount++; + } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { @@ -245,6 +269,8 @@ public SecondContext(DbContextOptions options) { LeasedFromPool += OnLeasedFromPool; ReturnedToPool += OnReturnedToPool; + LeasedFromPoolAsync += OnLeasedFromPoolAsync; + ReturnedToPoolAsync += OnReturnedToPoolAsync; } public int LeasedCount { get; private set; } @@ -254,11 +280,33 @@ public SecondContext(DbContextOptions options) public bool? AsyncReturn { get; private set; } public bool? SyncReturn { get; private set; } - private void OnLeasedFromPool(object sender, EventArgs e) - => LeasedCount++; + private void OnLeasedFromPool(object sender) + { + SyncLease = true; + LeasedCount++; + } + + private async Task OnLeasedFromPoolAsync(DbContext sender, CancellationToken cancellationToken) + { + await sender.Database.CanConnectAsync(cancellationToken); // Just something async + + AsyncLease = true; + LeasedCount++; + } + + private void OnReturnedToPool(object sender) + { + SyncReturn = true; + ReturnedCount++; + } + + private async Task OnReturnedToPoolAsync(DbContext sender, CancellationToken cancellationToken) + { + await sender.Database.CanConnectAsync(cancellationToken); // Just something async - private void OnReturnedToPool(object sender, EventArgs e) - => ReturnedCount++; + AsyncReturn = true; + ReturnedCount++; + } } private class SecondContextWithOverrides : DbContext, ISecondContext @@ -731,6 +779,10 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Equal(1, secondContext1!.LeasedCount); Assert.Equal(0, context1.ReturnedCount); Assert.Equal(0, secondContext1.ReturnedCount); + Assert.True(context1.SyncLease); + Assert.Null(context1.AsyncLease); + Assert.Null(context1.SyncReturn); + Assert.Null(context1.AsyncReturn); var serviceScope2 = serviceProvider.CreateScope(); var scopedProvider2 = serviceScope2.ServiceProvider; @@ -745,6 +797,10 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Equal(0, context1.ReturnedCount); Assert.Equal(0, secondContext1.ReturnedCount); Assert.Equal(0, context2.ReturnedCount); + Assert.True(context1.SyncLease); + Assert.Null(context1.AsyncLease); + Assert.Null(context1.SyncReturn); + Assert.Null(context1.AsyncReturn); var secondContext2 = useInterface ? scopedProvider2.GetService() @@ -761,6 +817,10 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Equal(0, secondContext1.ReturnedCount); Assert.Equal(0, context2.ReturnedCount); Assert.Equal(0, secondContext2.ReturnedCount); + Assert.True(context1.SyncLease); + Assert.Null(context1.AsyncLease); + Assert.Null(context1.SyncReturn); + Assert.Null(context1.AsyncReturn); await Dispose(serviceScope1, async); @@ -772,6 +832,10 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Equal(1, secondContext1.ReturnedCount); Assert.Equal(0, context2.ReturnedCount); Assert.Equal(0, secondContext2.ReturnedCount); + Assert.True(context1.SyncLease); + Assert.Null(context1.AsyncLease); + Assert.Equal(!async ? true : null, context1.SyncReturn); + Assert.Equal(async ? true : null, context1.AsyncReturn); await Dispose(serviceScope2, async); @@ -783,6 +847,10 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Equal(1, secondContext1.ReturnedCount); Assert.Equal(1, context2.ReturnedCount); Assert.Equal(1, secondContext2.ReturnedCount); + Assert.True(context1.SyncLease); + Assert.Null(context1.AsyncLease); + Assert.Equal(!async ? true : null, context1.SyncReturn); + Assert.Equal(async ? true : null, context1.AsyncReturn); var serviceScope3 = serviceProvider.CreateScope(); var scopedProvider3 = serviceScope3.ServiceProvider; @@ -806,6 +874,10 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Equal(1, secondContext1.ReturnedCount); Assert.Equal(1, context2.ReturnedCount); Assert.Equal(1, secondContext2.ReturnedCount); + Assert.True(context1.SyncLease); + Assert.Null(context1.AsyncLease); + Assert.Equal(!async ? true : null, context1.SyncReturn); + Assert.Equal(async ? true : null, context1.AsyncReturn); var serviceScope4 = serviceProvider.CreateScope(); var scopedProvider4 = serviceScope4.ServiceProvider; @@ -829,6 +901,10 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Equal(1, secondContext1.ReturnedCount); Assert.Equal(1, context2.ReturnedCount); Assert.Equal(1, secondContext2.ReturnedCount); + Assert.True(context1.SyncLease); + Assert.Null(context1.AsyncLease); + Assert.Equal(!async ? true : null, context1.SyncReturn); + Assert.Equal(async ? true : null, context1.AsyncReturn); await Dispose(serviceScope3, async); @@ -840,6 +916,10 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Equal(2, secondContext1.ReturnedCount); Assert.Equal(1, context2.ReturnedCount); Assert.Equal(1, secondContext2.ReturnedCount); + Assert.True(context1.SyncLease); + Assert.Null(context1.AsyncLease); + Assert.Equal(!async ? true : null, context1.SyncReturn); + Assert.Equal(async ? true : null, context1.AsyncReturn); await Dispose(serviceScope4, async); @@ -851,6 +931,10 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Equal(2, secondContext1.ReturnedCount); Assert.Equal(2, context2.ReturnedCount); Assert.Equal(2, secondContext2.ReturnedCount); + Assert.True(context1.SyncLease); + Assert.Null(context1.AsyncLease); + Assert.Equal(!async ? true : null, context1.SyncReturn); + Assert.Equal(async ? true : null, context1.AsyncReturn); } [ConditionalTheory] @@ -879,6 +963,10 @@ public async Task Contexts_are_pooled_with_factory(bool async, bool withDependen Assert.Equal(0, secondContext1.ReturnedCount); Assert.Equal(0, context2.ReturnedCount); Assert.Equal(0, secondContext2.ReturnedCount); + Assert.Equal(!async ? true : null, context1.SyncLease); + Assert.Equal(async ? true : null, context1.AsyncLease); + Assert.Null(context1.SyncReturn); + Assert.Null(context1.AsyncReturn); await Dispose(context1, async); await Dispose(secondContext1, async); @@ -893,6 +981,10 @@ public async Task Contexts_are_pooled_with_factory(bool async, bool withDependen Assert.Equal(1, secondContext1.ReturnedCount); Assert.Equal(1, context2.ReturnedCount); Assert.Equal(1, secondContext2.ReturnedCount); + Assert.Equal(!async ? true : null, context1.SyncLease); + Assert.Equal(async ? true : null, context1.AsyncLease); + Assert.Equal(!async ? true : null, context1.SyncReturn); + Assert.Equal(async ? true : null, context1.AsyncReturn); var context3 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); var secondContext3 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); @@ -928,6 +1020,10 @@ public async Task Contexts_are_pooled_with_factory(bool async, bool withDependen Assert.Equal(2, secondContext1.ReturnedCount); Assert.Equal(2, context2.ReturnedCount); Assert.Equal(2, secondContext2.ReturnedCount); + Assert.Equal(!async ? true : null, context1.SyncLease); + Assert.Equal(async ? true : null, context1.AsyncLease); + Assert.Equal(!async ? true : null, context1.SyncReturn); + Assert.Equal(async ? true : null, context1.AsyncReturn); } [ConditionalTheory] diff --git a/test/EFCore.Tests/DbContextTest.cs b/test/EFCore.Tests/DbContextTest.cs index 29f1033d896..95c43f4e171 100644 --- a/test/EFCore.Tests/DbContextTest.cs +++ b/test/EFCore.Tests/DbContextTest.cs @@ -763,7 +763,7 @@ public async Task It_throws_object_disposed_exception(bool async) (await Assert.ThrowsAsync(() => context.FindAsync(typeof(Random), 77).AsTask())).Message); var methodCount = typeof(DbContext).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).Count(); - var expectedMethodCount = 54; + var expectedMethodCount = 58; Assert.True( methodCount == expectedMethodCount, userMessage: $"Expected {expectedMethodCount} methods on DbContext but found {methodCount}. " From 609398b3c3623000ce01b64bfb0a78a4e2b55faf Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Wed, 9 Jun 2021 13:19:39 -0700 Subject: [PATCH 3/4] Revert "Add asynchronous events" This reverts commit 24a3b223ffc045749d21a2912b5764f5f6464579. --- src/EFCore/DbContext.cs | 62 ++-------- .../DbContextPoolingTest.cs | 112 ++---------------- test/EFCore.Tests/DbContextTest.cs | 2 +- 3 files changed, 22 insertions(+), 154 deletions(-) diff --git a/src/EFCore/DbContext.cs b/src/EFCore/DbContext.cs index 19032fc992c..bd4cd5844c4 100644 --- a/src/EFCore/DbContext.cs +++ b/src/EFCore/DbContext.cs @@ -717,43 +717,25 @@ await DbContextDependencies.UpdateLogger.OptimisticConcurrencyExceptionAsync(thi /// . /// /// - public event Action? LeasedFromPool; + public event EventHandler? LeasedFromPool; /// /// Called to fire the event. Can be overriden in a derived context to intercept this event. /// protected virtual void OnLeasedFromPool() - => LeasedFromPool?.Invoke(this); + => LeasedFromPool?.Invoke(this, EventArgs.Empty); /// - /// - /// An event fired when this context instance is leased from the context pool. - /// - /// - /// This event is only fired when 'DbContext' pooling is enabled through use of - /// - /// or - /// . - /// - /// - public event Func? LeasedFromPoolAsync; - - /// - /// Called to fire the event. Can be overriden in a derived context to intercept this event. + /// Called to fire the event. Can be overriden in a derived context to intercept this event. /// /// A to observe while waiting for the task to complete. /// A task that represents the asynchronous operation. /// If the is canceled. - protected virtual async Task OnLeasedFromPoolAsync(CancellationToken cancellationToken) + protected virtual Task OnLeasedFromPoolAsync(CancellationToken cancellationToken) { - var handler = LeasedFromPoolAsync; - if (handler != null) - { - foreach (var func in handler.GetInvocationList()) - { - await ((Func)func)(this, cancellationToken); - } - } + LeasedFromPool?.Invoke(this, EventArgs.Empty); + + return Task.CompletedTask; } /// @@ -767,26 +749,13 @@ protected virtual async Task OnLeasedFromPoolAsync(CancellationToken cancellatio /// . /// /// - public event Action? ReturnedToPool; + public event EventHandler? ReturnedToPool; /// /// Called to fire the event. Can be overriden in a derived context to intercept this event. /// protected virtual void OnReturnedToPool() - => ReturnedToPool?.Invoke(this); - - /// - /// - /// An event fired when this context instance is returned to the context pool. - /// - /// - /// This event is only fired when 'DbContext' pooling is enabled through use of - /// - /// or - /// . - /// - /// - public event Func? ReturnedToPoolAsync; + => ReturnedToPool?.Invoke(this, EventArgs.Empty); /// /// Called to fire the event. Can be overriden in a derived context to intercept this event. @@ -794,16 +763,11 @@ protected virtual void OnReturnedToPool() /// A to observe while waiting for the task to complete. /// A task that represents the asynchronous operation. /// If the is canceled. - protected virtual async Task OnReturnedToPoolAsync(CancellationToken cancellationToken) + protected virtual Task OnReturnedToPoolAsync(CancellationToken cancellationToken) { - var handler = ReturnedToPoolAsync; - if (handler != null) - { - foreach (var func in handler.GetInvocationList()) - { - await ((Func)func)(this, cancellationToken); - } - } + ReturnedToPool?.Invoke(this, EventArgs.Empty); + + return Task.CompletedTask; } /// diff --git a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs index 26cddbc726f..1b02767b984 100644 --- a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs @@ -132,8 +132,6 @@ public PooledContext(DbContextOptions options) LeasedFromPool += OnLeasedFromPool; ReturnedToPool += OnReturnedToPool; - LeasedFromPoolAsync += OnLeasedFromPoolAsync; - ReturnedToPoolAsync += OnReturnedToPoolAsync; } public int LeasedCount { get; private set; } @@ -143,33 +141,11 @@ public PooledContext(DbContextOptions options) public bool? AsyncReturn { get; private set; } public bool? SyncReturn { get; private set; } - private void OnLeasedFromPool(object sender) - { - SyncLease = true; - LeasedCount++; - } - - private async Task OnLeasedFromPoolAsync(DbContext sender, CancellationToken cancellationToken) - { - await sender.Database.CanConnectAsync(cancellationToken); // Just something async - - AsyncLease = true; - LeasedCount++; - } - - private void OnReturnedToPool(object sender) - { - SyncReturn = true; - ReturnedCount++; - } + private void OnLeasedFromPool(object sender, EventArgs e) + => LeasedCount++; - private async Task OnReturnedToPoolAsync(DbContext sender, CancellationToken cancellationToken) - { - await sender.Database.CanConnectAsync(cancellationToken); // Just something async - - AsyncReturn = true; - ReturnedCount++; - } + private void OnReturnedToPool(object sender, EventArgs e) + => ReturnedCount++; protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { @@ -269,8 +245,6 @@ public SecondContext(DbContextOptions options) { LeasedFromPool += OnLeasedFromPool; ReturnedToPool += OnReturnedToPool; - LeasedFromPoolAsync += OnLeasedFromPoolAsync; - ReturnedToPoolAsync += OnReturnedToPoolAsync; } public int LeasedCount { get; private set; } @@ -280,33 +254,11 @@ public SecondContext(DbContextOptions options) public bool? AsyncReturn { get; private set; } public bool? SyncReturn { get; private set; } - private void OnLeasedFromPool(object sender) - { - SyncLease = true; - LeasedCount++; - } - - private async Task OnLeasedFromPoolAsync(DbContext sender, CancellationToken cancellationToken) - { - await sender.Database.CanConnectAsync(cancellationToken); // Just something async - - AsyncLease = true; - LeasedCount++; - } - - private void OnReturnedToPool(object sender) - { - SyncReturn = true; - ReturnedCount++; - } - - private async Task OnReturnedToPoolAsync(DbContext sender, CancellationToken cancellationToken) - { - await sender.Database.CanConnectAsync(cancellationToken); // Just something async + private void OnLeasedFromPool(object sender, EventArgs e) + => LeasedCount++; - AsyncReturn = true; - ReturnedCount++; - } + private void OnReturnedToPool(object sender, EventArgs e) + => ReturnedCount++; } private class SecondContextWithOverrides : DbContext, ISecondContext @@ -779,10 +731,6 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Equal(1, secondContext1!.LeasedCount); Assert.Equal(0, context1.ReturnedCount); Assert.Equal(0, secondContext1.ReturnedCount); - Assert.True(context1.SyncLease); - Assert.Null(context1.AsyncLease); - Assert.Null(context1.SyncReturn); - Assert.Null(context1.AsyncReturn); var serviceScope2 = serviceProvider.CreateScope(); var scopedProvider2 = serviceScope2.ServiceProvider; @@ -797,10 +745,6 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Equal(0, context1.ReturnedCount); Assert.Equal(0, secondContext1.ReturnedCount); Assert.Equal(0, context2.ReturnedCount); - Assert.True(context1.SyncLease); - Assert.Null(context1.AsyncLease); - Assert.Null(context1.SyncReturn); - Assert.Null(context1.AsyncReturn); var secondContext2 = useInterface ? scopedProvider2.GetService() @@ -817,10 +761,6 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Equal(0, secondContext1.ReturnedCount); Assert.Equal(0, context2.ReturnedCount); Assert.Equal(0, secondContext2.ReturnedCount); - Assert.True(context1.SyncLease); - Assert.Null(context1.AsyncLease); - Assert.Null(context1.SyncReturn); - Assert.Null(context1.AsyncReturn); await Dispose(serviceScope1, async); @@ -832,10 +772,6 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Equal(1, secondContext1.ReturnedCount); Assert.Equal(0, context2.ReturnedCount); Assert.Equal(0, secondContext2.ReturnedCount); - Assert.True(context1.SyncLease); - Assert.Null(context1.AsyncLease); - Assert.Equal(!async ? true : null, context1.SyncReturn); - Assert.Equal(async ? true : null, context1.AsyncReturn); await Dispose(serviceScope2, async); @@ -847,10 +783,6 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Equal(1, secondContext1.ReturnedCount); Assert.Equal(1, context2.ReturnedCount); Assert.Equal(1, secondContext2.ReturnedCount); - Assert.True(context1.SyncLease); - Assert.Null(context1.AsyncLease); - Assert.Equal(!async ? true : null, context1.SyncReturn); - Assert.Equal(async ? true : null, context1.AsyncReturn); var serviceScope3 = serviceProvider.CreateScope(); var scopedProvider3 = serviceScope3.ServiceProvider; @@ -874,10 +806,6 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Equal(1, secondContext1.ReturnedCount); Assert.Equal(1, context2.ReturnedCount); Assert.Equal(1, secondContext2.ReturnedCount); - Assert.True(context1.SyncLease); - Assert.Null(context1.AsyncLease); - Assert.Equal(!async ? true : null, context1.SyncReturn); - Assert.Equal(async ? true : null, context1.AsyncReturn); var serviceScope4 = serviceProvider.CreateScope(); var scopedProvider4 = serviceScope4.ServiceProvider; @@ -901,10 +829,6 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Equal(1, secondContext1.ReturnedCount); Assert.Equal(1, context2.ReturnedCount); Assert.Equal(1, secondContext2.ReturnedCount); - Assert.True(context1.SyncLease); - Assert.Null(context1.AsyncLease); - Assert.Equal(!async ? true : null, context1.SyncReturn); - Assert.Equal(async ? true : null, context1.AsyncReturn); await Dispose(serviceScope3, async); @@ -916,10 +840,6 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Equal(2, secondContext1.ReturnedCount); Assert.Equal(1, context2.ReturnedCount); Assert.Equal(1, secondContext2.ReturnedCount); - Assert.True(context1.SyncLease); - Assert.Null(context1.AsyncLease); - Assert.Equal(!async ? true : null, context1.SyncReturn); - Assert.Equal(async ? true : null, context1.AsyncReturn); await Dispose(serviceScope4, async); @@ -931,10 +851,6 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Equal(2, secondContext1.ReturnedCount); Assert.Equal(2, context2.ReturnedCount); Assert.Equal(2, secondContext2.ReturnedCount); - Assert.True(context1.SyncLease); - Assert.Null(context1.AsyncLease); - Assert.Equal(!async ? true : null, context1.SyncReturn); - Assert.Equal(async ? true : null, context1.AsyncReturn); } [ConditionalTheory] @@ -963,10 +879,6 @@ public async Task Contexts_are_pooled_with_factory(bool async, bool withDependen Assert.Equal(0, secondContext1.ReturnedCount); Assert.Equal(0, context2.ReturnedCount); Assert.Equal(0, secondContext2.ReturnedCount); - Assert.Equal(!async ? true : null, context1.SyncLease); - Assert.Equal(async ? true : null, context1.AsyncLease); - Assert.Null(context1.SyncReturn); - Assert.Null(context1.AsyncReturn); await Dispose(context1, async); await Dispose(secondContext1, async); @@ -981,10 +893,6 @@ public async Task Contexts_are_pooled_with_factory(bool async, bool withDependen Assert.Equal(1, secondContext1.ReturnedCount); Assert.Equal(1, context2.ReturnedCount); Assert.Equal(1, secondContext2.ReturnedCount); - Assert.Equal(!async ? true : null, context1.SyncLease); - Assert.Equal(async ? true : null, context1.AsyncLease); - Assert.Equal(!async ? true : null, context1.SyncReturn); - Assert.Equal(async ? true : null, context1.AsyncReturn); var context3 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); var secondContext3 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); @@ -1020,10 +928,6 @@ public async Task Contexts_are_pooled_with_factory(bool async, bool withDependen Assert.Equal(2, secondContext1.ReturnedCount); Assert.Equal(2, context2.ReturnedCount); Assert.Equal(2, secondContext2.ReturnedCount); - Assert.Equal(!async ? true : null, context1.SyncLease); - Assert.Equal(async ? true : null, context1.AsyncLease); - Assert.Equal(!async ? true : null, context1.SyncReturn); - Assert.Equal(async ? true : null, context1.AsyncReturn); } [ConditionalTheory] diff --git a/test/EFCore.Tests/DbContextTest.cs b/test/EFCore.Tests/DbContextTest.cs index 95c43f4e171..29f1033d896 100644 --- a/test/EFCore.Tests/DbContextTest.cs +++ b/test/EFCore.Tests/DbContextTest.cs @@ -763,7 +763,7 @@ public async Task It_throws_object_disposed_exception(bool async) (await Assert.ThrowsAsync(() => context.FindAsync(typeof(Random), 77).AsTask())).Message); var methodCount = typeof(DbContext).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).Count(); - var expectedMethodCount = 58; + var expectedMethodCount = 54; Assert.True( methodCount == expectedMethodCount, userMessage: $"Expected {expectedMethodCount} methods on DbContext but found {methodCount}. " From 215dee78ac1333bdd62299f59414e7e62bf58b62 Mon Sep 17 00:00:00 2001 From: Andriy Svyryd Date: Wed, 9 Jun 2021 16:33:27 -0700 Subject: [PATCH 4/4] Restore events set in the constructor when leasing from pool Remove LeasedFromPool and ReturnedToPool events. --- .../RuntimeRelationalPropertyOverrides.cs | 2 +- .../Storage/RelationalConnection.cs | 20 +- src/EFCore/DbContext.cs | 148 +---- src/EFCore/Infrastructure/DatabaseFacade.cs | 16 +- .../Infrastructure/PooledDbContextFactory.cs | 2 +- src/EFCore/Internal/DbContextLease.cs | 4 +- .../DbContextPoolConfigurationSnapshot.cs | 61 +- .../DbContextPoolingTest.cs | 593 +----------------- test/EFCore.Tests/DbContextTest.cs | 2 +- 9 files changed, 124 insertions(+), 724 deletions(-) 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 bd4cd5844c4..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, @@ -706,70 +704,6 @@ await DbContextDependencies.UpdateLogger.OptimisticConcurrencyExceptionAsync(thi /// public event EventHandler? SaveChangesFailed; - /// - /// - /// An event fired when this context instance is leased from the context pool. - /// - /// - /// This event is only fired when 'DbContext' pooling is enabled through use of - /// - /// or - /// . - /// - /// - public event EventHandler? LeasedFromPool; - - /// - /// Called to fire the event. Can be overriden in a derived context to intercept this event. - /// - protected virtual void OnLeasedFromPool() - => LeasedFromPool?.Invoke(this, EventArgs.Empty); - - /// - /// Called to fire the event. Can be overriden in a derived context to intercept this event. - /// - /// A to observe while waiting for the task to complete. - /// A task that represents the asynchronous operation. - /// If the is canceled. - protected virtual Task OnLeasedFromPoolAsync(CancellationToken cancellationToken) - { - LeasedFromPool?.Invoke(this, EventArgs.Empty); - - return Task.CompletedTask; - } - - /// - /// - /// An event fired when this context instance is returned to the context pool. - /// - /// - /// This event is only fired when 'DbContext' pooling is enabled through use of - /// - /// or - /// . - /// - /// - public event EventHandler? ReturnedToPool; - - /// - /// Called to fire the event. Can be overriden in a derived context to intercept this event. - /// - protected virtual void OnReturnedToPool() - => ReturnedToPool?.Invoke(this, EventArgs.Empty); - - /// - /// Called to fire the event. Can be overriden in a derived context to intercept this event. - /// - /// A to observe while waiting for the task to complete. - /// A task that represents the asynchronous operation. - /// If the is canceled. - protected virtual Task OnReturnedToPoolAsync(CancellationToken cancellationToken) - { - ReturnedToPool?.Invoke(this, EventArgs.Empty); - - return Task.CompletedTask; - } - /// /// 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 @@ -790,8 +724,6 @@ void IDbContextPoolable.ClearLease() void IDbContextPoolable.SetLease(DbContextLease lease) { SetLeaseInternal(lease); - - OnLeasedFromPool(); } /// @@ -804,48 +736,32 @@ void IDbContextPoolable.SetLease(DbContextLease lease) Task IDbContextPoolable.SetLeaseAsync(DbContextLease lease, CancellationToken cancellationToken) { SetLeaseInternal(lease); - - return OnLeasedFromPoolAsync(cancellationToken); + + 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; } /// @@ -856,14 +772,21 @@ private void SetLeaseInternal(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 @@ -874,8 +797,6 @@ void IDbContextPoolable.SnapshotConfiguration() [EntityFrameworkInternal] void IResettableService.ResetState() { - OnReturnedToPool(); - foreach (var service in GetResettableServices()) { service.ResetState(); @@ -893,8 +814,6 @@ void IResettableService.ResetState() [EntityFrameworkInternal] async Task IResettableService.ResetStateAsync(CancellationToken cancellationToken) { - await OnReturnedToPoolAsync(cancellationToken); - foreach (var service in GetResettableServices()) { await service.ResetStateAsync(cancellationToken).ConfigureAwait(false); @@ -968,6 +887,7 @@ private bool DisposeSync(bool leaseActive, bool contextDisposed) _dbContextDependencies = null; _changeTracker = null; _database = null; + _configurationSnapshot = null; SavingChanges = null; SavedChanges = null; @@ -985,7 +905,7 @@ private bool DisposeSync(bool leaseActive, bool contextDisposed) public virtual async ValueTask DisposeAsync() { var leaseActive = _lease.IsActive; - var contextDisposed = leaseActive && (await _lease.ContextDisposedAsync()); + var contextDisposed = leaseActive && await _lease.ContextDisposedAsync(); if (DisposeSync(leaseActive, contextDisposed)) { 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 04680e451a3..ba5274351ed 100644 --- a/src/EFCore/Infrastructure/PooledDbContextFactory.cs +++ b/src/EFCore/Infrastructure/PooledDbContextFactory.cs @@ -57,7 +57,7 @@ public virtual TContext CreateDbContext() return (TContext)lease.Context; } - + /// public virtual async Task CreateDbContextAsync(CancellationToken cancellationToken = default) { diff --git a/src/EFCore/Internal/DbContextLease.cs b/src/EFCore/Internal/DbContextLease.cs index 45b0de5a63a..2c68687fb6d 100644 --- a/src/EFCore/Internal/DbContextLease.cs +++ b/src/EFCore/Internal/DbContextLease.cs @@ -114,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/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs index 1b02767b984..c6ae8c730b9 100644 --- a/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/DbContextPoolingTest.cs @@ -16,6 +16,7 @@ using Microsoft.Extensions.DependencyInjection; using Xunit; using Xunit.Abstractions; +// ReSharper disable MethodHasAsyncOverload // ReSharper disable InconsistentNaming // ReSharper disable UnusedAutoPropertyAccessor.Local @@ -95,12 +96,6 @@ private static IDbContextFactory BuildFactory(bool withDepen private interface IPooledContext { - int LeasedCount { get; } - int ReturnedCount { get; } - public bool? AsyncLease { get; } - public bool? SyncLease { get; } - public bool? AsyncReturn { get; } - public bool? SyncReturn { get; } } private class DefaultOptionsPooledContext : DbContext @@ -129,24 +124,8 @@ public PooledContext(DbContextOptions options) Database.AutoSavepointsEnabled = false; ChangeTracker.CascadeDeleteTiming = CascadeTiming.Never; ChangeTracker.DeleteOrphansTiming = CascadeTiming.Never; - - LeasedFromPool += OnLeasedFromPool; - ReturnedToPool += OnReturnedToPool; } - public int LeasedCount { get; private set; } - public int ReturnedCount { get; private set; } - public bool? AsyncLease { get; private set; } - public bool? SyncLease { get; private set; } - public bool? AsyncReturn { get; private set; } - public bool? SyncReturn { get; private set; } - - private void OnLeasedFromPool(object sender, EventArgs e) - => LeasedCount++; - - private void OnReturnedToPool(object sender, EventArgs e) - => ReturnedCount++; - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (ModifyOptions) @@ -175,49 +154,10 @@ public PooledContextWithOverrides(DbContextOptions options) { } - public int LeasedCount { get; private set; } - public int ReturnedCount { get; private set; } - public bool? AsyncLease { get; private set; } - public bool? SyncLease { get; private set; } - public bool? AsyncReturn { get; private set; } - public bool? SyncReturn { get; private set; } - public DbSet Customers { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) => modelBuilder.Entity().ToTable("Customers"); - - protected override void OnLeasedFromPool() - { - LeasedCount++; - SyncLease = true; - - base.OnLeasedFromPool(); - } - - protected override Task OnLeasedFromPoolAsync(CancellationToken cancellationToken) - { - LeasedCount++; - AsyncLease = true; - - return base.OnLeasedFromPoolAsync(cancellationToken); - } - - protected override void OnReturnedToPool() - { - ReturnedCount++; - SyncReturn = true; - - base.OnReturnedToPool(); - } - - protected override Task OnReturnedToPoolAsync(CancellationToken cancellationToken) - { - ReturnedCount++; - AsyncReturn = true; - - return base.OnReturnedToPoolAsync(cancellationToken); - } } public class Customer @@ -228,12 +168,6 @@ public class Customer private interface ISecondContext { - int LeasedCount { get; } - int ReturnedCount { get; } - public bool? AsyncLease { get; } - public bool? SyncLease { get; } - public bool? AsyncReturn { get; } - public bool? SyncReturn { get; } } private class SecondContext : DbContext, ISecondContext @@ -243,68 +177,6 @@ private class SecondContext : DbContext, ISecondContext public SecondContext(DbContextOptions options) : base(options) { - LeasedFromPool += OnLeasedFromPool; - ReturnedToPool += OnReturnedToPool; - } - - public int LeasedCount { get; private set; } - public int ReturnedCount { get; private set; } - public bool? AsyncLease { get; private set; } - public bool? SyncLease { get; private set; } - public bool? AsyncReturn { get; private set; } - public bool? SyncReturn { get; private set; } - - private void OnLeasedFromPool(object sender, EventArgs e) - => LeasedCount++; - - private void OnReturnedToPool(object sender, EventArgs e) - => ReturnedCount++; - } - - private class SecondContextWithOverrides : DbContext, ISecondContext - { - public SecondContextWithOverrides(DbContextOptions options) - : base(options) - { - } - - public int LeasedCount { get; private set; } - public int ReturnedCount { get; private set; } - public bool? AsyncLease { get; private set; } - public bool? SyncLease { get; private set; } - public bool? AsyncReturn { get; private set; } - public bool? SyncReturn { get; private set; } - - protected override void OnLeasedFromPool() - { - LeasedCount++; - SyncLease = true; - - base.OnLeasedFromPool(); - } - - protected override Task OnLeasedFromPoolAsync(CancellationToken cancellationToken) - { - LeasedCount++; - AsyncLease = true; - - return base.OnLeasedFromPoolAsync(cancellationToken); - } - - protected override void OnReturnedToPool() - { - ReturnedCount++; - SyncReturn = true; - - base.OnReturnedToPool(); - } - - protected override Task OnReturnedToPoolAsync(CancellationToken cancellationToken) - { - ReturnedCount++; - AsyncReturn = true; - - return base.OnReturnedToPoolAsync(cancellationToken); } public class Blog @@ -720,18 +592,10 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) ? scopedProvider1.GetService() : scopedProvider1.GetService(); - Assert.Equal(1, context1!.LeasedCount); - Assert.Equal(0, context1.ReturnedCount); - var secondContext1 = useInterface ? scopedProvider1.GetService() : scopedProvider1.GetService(); - Assert.Equal(1, context1.LeasedCount); - Assert.Equal(1, secondContext1!.LeasedCount); - Assert.Equal(0, context1.ReturnedCount); - Assert.Equal(0, secondContext1.ReturnedCount); - var serviceScope2 = serviceProvider.CreateScope(); var scopedProvider2 = serviceScope2.ServiceProvider; @@ -739,13 +603,6 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) ? scopedProvider2.GetService() : scopedProvider2.GetService(); - Assert.Equal(1, context1.LeasedCount); - Assert.Equal(1, secondContext1.LeasedCount); - Assert.Equal(1, context2!.LeasedCount); - Assert.Equal(0, context1.ReturnedCount); - Assert.Equal(0, secondContext1.ReturnedCount); - Assert.Equal(0, context2.ReturnedCount); - var secondContext2 = useInterface ? scopedProvider2.GetService() : scopedProvider2.GetService(); @@ -753,37 +610,10 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.NotSame(context1, context2); Assert.NotSame(secondContext1, secondContext2); - Assert.Equal(1, context1.LeasedCount); - Assert.Equal(1, secondContext1.LeasedCount); - Assert.Equal(1, context2.LeasedCount); - Assert.Equal(1, secondContext2!.LeasedCount); - Assert.Equal(0, context1.ReturnedCount); - Assert.Equal(0, secondContext1.ReturnedCount); - Assert.Equal(0, context2.ReturnedCount); - Assert.Equal(0, secondContext2.ReturnedCount); - await Dispose(serviceScope1, async); - Assert.Equal(1, context1.LeasedCount); - Assert.Equal(1, secondContext1.LeasedCount); - Assert.Equal(1, context2.LeasedCount); - Assert.Equal(1, secondContext2.LeasedCount); - Assert.Equal(1, context1.ReturnedCount); - Assert.Equal(1, secondContext1.ReturnedCount); - Assert.Equal(0, context2.ReturnedCount); - Assert.Equal(0, secondContext2.ReturnedCount); - await Dispose(serviceScope2, async); - Assert.Equal(1, context1.LeasedCount); - Assert.Equal(1, secondContext1.LeasedCount); - Assert.Equal(1, context2.LeasedCount); - Assert.Equal(1, secondContext2!.LeasedCount); - Assert.Equal(1, context1.ReturnedCount); - Assert.Equal(1, secondContext1.ReturnedCount); - Assert.Equal(1, context2.ReturnedCount); - Assert.Equal(1, secondContext2.ReturnedCount); - var serviceScope3 = serviceProvider.CreateScope(); var scopedProvider3 = serviceScope3.ServiceProvider; @@ -798,15 +628,6 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Same(context1, context3); Assert.Same(secondContext1, secondContext3); - Assert.Equal(2, context1.LeasedCount); - Assert.Equal(2, secondContext1.LeasedCount); - Assert.Equal(1, context2.LeasedCount); - Assert.Equal(1, secondContext2!.LeasedCount); - Assert.Equal(1, context1.ReturnedCount); - Assert.Equal(1, secondContext1.ReturnedCount); - Assert.Equal(1, context2.ReturnedCount); - Assert.Equal(1, secondContext2.ReturnedCount); - var serviceScope4 = serviceProvider.CreateScope(); var scopedProvider4 = serviceScope4.ServiceProvider; @@ -821,36 +642,9 @@ public async Task Contexts_are_pooled(bool useInterface, bool async) Assert.Same(context2, context4); Assert.Same(secondContext2, secondContext4); - Assert.Equal(2, context1.LeasedCount); - Assert.Equal(2, secondContext1.LeasedCount); - Assert.Equal(2, context2.LeasedCount); - Assert.Equal(2, secondContext2!.LeasedCount); - Assert.Equal(1, context1.ReturnedCount); - Assert.Equal(1, secondContext1.ReturnedCount); - Assert.Equal(1, context2.ReturnedCount); - Assert.Equal(1, secondContext2.ReturnedCount); - await Dispose(serviceScope3, async); - Assert.Equal(2, context1.LeasedCount); - Assert.Equal(2, secondContext1.LeasedCount); - Assert.Equal(2, context2.LeasedCount); - Assert.Equal(2, secondContext2!.LeasedCount); - Assert.Equal(2, context1.ReturnedCount); - Assert.Equal(2, secondContext1.ReturnedCount); - Assert.Equal(1, context2.ReturnedCount); - Assert.Equal(1, secondContext2.ReturnedCount); - await Dispose(serviceScope4, async); - - Assert.Equal(2, context1.LeasedCount); - Assert.Equal(2, secondContext1.LeasedCount); - Assert.Equal(2, context2.LeasedCount); - Assert.Equal(2, secondContext2!.LeasedCount); - Assert.Equal(2, context1.ReturnedCount); - Assert.Equal(2, secondContext1.ReturnedCount); - Assert.Equal(2, context2.ReturnedCount); - Assert.Equal(2, secondContext2.ReturnedCount); } [ConditionalTheory] @@ -871,29 +665,11 @@ public async Task Contexts_are_pooled_with_factory(bool async, bool withDependen Assert.NotSame(context1, context2); Assert.NotSame(secondContext1, secondContext2); - Assert.Equal(1, context1.LeasedCount); - Assert.Equal(1, secondContext1.LeasedCount); - Assert.Equal(1, context2.LeasedCount); - Assert.Equal(1, secondContext2!.LeasedCount); - Assert.Equal(0, context1.ReturnedCount); - Assert.Equal(0, secondContext1.ReturnedCount); - Assert.Equal(0, context2.ReturnedCount); - Assert.Equal(0, secondContext2.ReturnedCount); - await Dispose(context1, async); await Dispose(secondContext1, async); await Dispose(context2, async); await Dispose(secondContext2, async); - Assert.Equal(1, context1.LeasedCount); - Assert.Equal(1, secondContext1.LeasedCount); - Assert.Equal(1, context2.LeasedCount); - Assert.Equal(1, secondContext2!.LeasedCount); - Assert.Equal(1, context1.ReturnedCount); - Assert.Equal(1, secondContext1.ReturnedCount); - Assert.Equal(1, context2.ReturnedCount); - Assert.Equal(1, secondContext2.ReturnedCount); - var context3 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); var secondContext3 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); @@ -906,318 +682,10 @@ public async Task Contexts_are_pooled_with_factory(bool async, bool withDependen Assert.Same(context2, context4); Assert.Same(secondContext2, secondContext4); - Assert.Equal(2, context1.LeasedCount); - Assert.Equal(2, secondContext1.LeasedCount); - Assert.Equal(2, context2.LeasedCount); - Assert.Equal(2, secondContext2!.LeasedCount); - Assert.Equal(1, context1.ReturnedCount); - Assert.Equal(1, secondContext1.ReturnedCount); - Assert.Equal(1, context2.ReturnedCount); - Assert.Equal(1, secondContext2.ReturnedCount); - await Dispose(context1, async); await Dispose(secondContext1, async); await Dispose(context2, async); await Dispose(secondContext2, async); - - Assert.Equal(2, context1.LeasedCount); - Assert.Equal(2, secondContext1.LeasedCount); - Assert.Equal(2, context2.LeasedCount); - Assert.Equal(2, secondContext2!.LeasedCount); - Assert.Equal(2, context1.ReturnedCount); - Assert.Equal(2, secondContext1.ReturnedCount); - Assert.Equal(2, context2.ReturnedCount); - Assert.Equal(2, secondContext2.ReturnedCount); - } - - [ConditionalTheory] - [InlineData(false, false)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(true, true)] - public async Task Contexts_are_pooled_with_event_overrides(bool useInterface, bool async) - { - var serviceProvider = useInterface - ? new ServiceCollection() - .AddDbContextPool(ob => ConfigureOptions(ob)) - .AddDbContextPool(ob => ConfigureOptions(ob)) - .BuildServiceProvider() - : new ServiceCollection() - .AddDbContextPool(ob => ConfigureOptions(ob)) - .AddDbContextPool(ob => ConfigureOptions(ob)) - .BuildServiceProvider(); - - var serviceScope1 = serviceProvider.CreateScope(); - var scopedProvider1 = serviceScope1.ServiceProvider; - - var context1 = useInterface - ? scopedProvider1.GetService() - : scopedProvider1.GetService(); - - Assert.Equal(1, context1!.LeasedCount); - Assert.Equal(0, context1.ReturnedCount); - Assert.True(context1.SyncLease); - Assert.Null(context1.AsyncLease); - Assert.Null(context1.SyncReturn); - Assert.Null(context1.AsyncReturn); - - var secondContext1 = useInterface - ? scopedProvider1.GetService() - : scopedProvider1.GetService(); - - Assert.Equal(1, context1.LeasedCount); - Assert.Equal(1, secondContext1!.LeasedCount); - Assert.Equal(0, context1.ReturnedCount); - Assert.Equal(0, secondContext1.ReturnedCount); - Assert.True(context1.SyncLease); - Assert.Null(context1.AsyncLease); - Assert.Null(context1.SyncReturn); - Assert.Null(context1.AsyncReturn); - - var serviceScope2 = serviceProvider.CreateScope(); - var scopedProvider2 = serviceScope2.ServiceProvider; - - var context2 = useInterface - ? scopedProvider2.GetService() - : scopedProvider2.GetService(); - - Assert.Equal(1, context1.LeasedCount); - Assert.Equal(1, secondContext1.LeasedCount); - Assert.Equal(1, context2!.LeasedCount); - Assert.Equal(0, context1.ReturnedCount); - Assert.Equal(0, secondContext1.ReturnedCount); - Assert.Equal(0, context2.ReturnedCount); - Assert.True(context1.SyncLease); - Assert.Null(context1.AsyncLease); - Assert.Null(context1.SyncReturn); - Assert.Null(context1.AsyncReturn); - - var secondContext2 = useInterface - ? scopedProvider2.GetService() - : scopedProvider2.GetService(); - - Assert.NotSame(context1, context2); - Assert.NotSame(secondContext1, secondContext2); - - Assert.Equal(1, context1.LeasedCount); - Assert.Equal(1, secondContext1.LeasedCount); - Assert.Equal(1, context2.LeasedCount); - Assert.Equal(1, secondContext2!.LeasedCount); - Assert.Equal(0, context1.ReturnedCount); - Assert.Equal(0, secondContext1.ReturnedCount); - Assert.Equal(0, context2.ReturnedCount); - Assert.Equal(0, secondContext2.ReturnedCount); - Assert.True(context1.SyncLease); - Assert.Null(context1.AsyncLease); - Assert.Null(context1.SyncReturn); - Assert.Null(context1.AsyncReturn); - - await Dispose(serviceScope1, async); - - Assert.Equal(1, context1.LeasedCount); - Assert.Equal(1, secondContext1.LeasedCount); - Assert.Equal(1, context2.LeasedCount); - Assert.Equal(1, secondContext2.LeasedCount); - Assert.Equal(1, context1.ReturnedCount); - Assert.Equal(1, secondContext1.ReturnedCount); - Assert.Equal(0, context2.ReturnedCount); - Assert.Equal(0, secondContext2.ReturnedCount); - Assert.True(context1.SyncLease); - Assert.Null(context1.AsyncLease); - Assert.Equal(!async ? true : null, context1.SyncReturn); - Assert.Equal(async ? true : null, context1.AsyncReturn); - - await Dispose(serviceScope2, async); - - Assert.Equal(1, context1.LeasedCount); - Assert.Equal(1, secondContext1.LeasedCount); - Assert.Equal(1, context2.LeasedCount); - Assert.Equal(1, secondContext2!.LeasedCount); - Assert.Equal(1, context1.ReturnedCount); - Assert.Equal(1, secondContext1.ReturnedCount); - Assert.Equal(1, context2.ReturnedCount); - Assert.Equal(1, secondContext2.ReturnedCount); - Assert.True(context1.SyncLease); - Assert.Null(context1.AsyncLease); - Assert.Equal(!async ? true : null, context1.SyncReturn); - Assert.Equal(async ? true : null, context1.AsyncReturn); - - 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); - - Assert.Equal(2, context1.LeasedCount); - Assert.Equal(2, secondContext1.LeasedCount); - Assert.Equal(1, context2.LeasedCount); - Assert.Equal(1, secondContext2!.LeasedCount); - Assert.Equal(1, context1.ReturnedCount); - Assert.Equal(1, secondContext1.ReturnedCount); - Assert.Equal(1, context2.ReturnedCount); - Assert.Equal(1, secondContext2.ReturnedCount); - Assert.True(context1.SyncLease); - Assert.Null(context1.AsyncLease); - Assert.Equal(!async ? true : null, context1.SyncReturn); - Assert.Equal(async ? true : null, context1.AsyncReturn); - - 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); - - Assert.Equal(2, context1.LeasedCount); - Assert.Equal(2, secondContext1.LeasedCount); - Assert.Equal(2, context2.LeasedCount); - Assert.Equal(2, secondContext2!.LeasedCount); - Assert.Equal(1, context1.ReturnedCount); - Assert.Equal(1, secondContext1.ReturnedCount); - Assert.Equal(1, context2.ReturnedCount); - Assert.Equal(1, secondContext2.ReturnedCount); - Assert.True(context1.SyncLease); - Assert.Null(context1.AsyncLease); - Assert.Equal(!async ? true : null, context1.SyncReturn); - Assert.Equal(async ? true : null, context1.AsyncReturn); - - await Dispose(serviceScope3, async); - - Assert.Equal(2, context1.LeasedCount); - Assert.Equal(2, secondContext1.LeasedCount); - Assert.Equal(2, context2.LeasedCount); - Assert.Equal(2, secondContext2!.LeasedCount); - Assert.Equal(2, context1.ReturnedCount); - Assert.Equal(2, secondContext1.ReturnedCount); - Assert.Equal(1, context2.ReturnedCount); - Assert.Equal(1, secondContext2.ReturnedCount); - Assert.True(context1.SyncLease); - Assert.Null(context1.AsyncLease); - Assert.Equal(!async ? true : null, context1.SyncReturn); - Assert.Equal(async ? true : null, context1.AsyncReturn); - - await Dispose(serviceScope4, async); - - Assert.Equal(2, context1.LeasedCount); - Assert.Equal(2, secondContext1.LeasedCount); - Assert.Equal(2, context2.LeasedCount); - Assert.Equal(2, secondContext2!.LeasedCount); - Assert.Equal(2, context1.ReturnedCount); - Assert.Equal(2, secondContext1.ReturnedCount); - Assert.Equal(2, context2.ReturnedCount); - Assert.Equal(2, secondContext2.ReturnedCount); - Assert.True(context1.SyncLease); - Assert.Null(context1.AsyncLease); - Assert.Equal(!async ? true : null, context1.SyncReturn); - Assert.Equal(async ? true : null, context1.AsyncReturn); - } - - [ConditionalTheory] - [InlineData(false, false)] - [InlineData(true, false)] - [InlineData(false, true)] - [InlineData(true, true)] - public async Task Contexts_are_pooled_with_factory_with_event_overrides(bool async, bool withDependencyInjection) - { - var factory = BuildFactory(withDependencyInjection); - - var context1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); - var secondContext1 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); - - var context2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); - var secondContext2 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); - - Assert.NotSame(context1, context2); - Assert.NotSame(secondContext1, secondContext2); - - Assert.Equal(1, context1.LeasedCount); - Assert.Equal(1, secondContext1.LeasedCount); - Assert.Equal(1, context2.LeasedCount); - Assert.Equal(1, secondContext2!.LeasedCount); - Assert.Equal(0, context1.ReturnedCount); - Assert.Equal(0, secondContext1.ReturnedCount); - Assert.Equal(0, context2.ReturnedCount); - Assert.Equal(0, secondContext2.ReturnedCount); - Assert.Equal(!async ? true : null, context1.SyncLease); - Assert.Equal(async ? true : null, context1.AsyncLease); - Assert.Null(context1.SyncReturn); - Assert.Null(context1.AsyncReturn); - - await Dispose(context1, async); - await Dispose(secondContext1, async); - await Dispose(context2, async); - await Dispose(secondContext2, async); - - Assert.Equal(1, context1.LeasedCount); - Assert.Equal(1, secondContext1.LeasedCount); - Assert.Equal(1, context2.LeasedCount); - Assert.Equal(1, secondContext2!.LeasedCount); - Assert.Equal(1, context1.ReturnedCount); - Assert.Equal(1, secondContext1.ReturnedCount); - Assert.Equal(1, context2.ReturnedCount); - Assert.Equal(1, secondContext2.ReturnedCount); - Assert.Equal(!async ? true : null, context1.SyncLease); - Assert.Equal(async ? true : null, context1.AsyncLease); - Assert.Equal(!async ? true : null, context1.SyncReturn); - Assert.Equal(async ? true : null, context1.AsyncReturn); - - var context3 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); - var secondContext3 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); - - Assert.Same(context1, context3); - Assert.Same(secondContext1, secondContext3); - - var context4 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); - var secondContext4 = async ? await factory.CreateDbContextAsync() : factory.CreateDbContext(); - - Assert.Same(context2, context4); - Assert.Same(secondContext2, secondContext4); - - Assert.Equal(2, context1.LeasedCount); - Assert.Equal(2, secondContext1.LeasedCount); - Assert.Equal(2, context2.LeasedCount); - Assert.Equal(2, secondContext2!.LeasedCount); - Assert.Equal(1, context1.ReturnedCount); - Assert.Equal(1, secondContext1.ReturnedCount); - Assert.Equal(1, context2.ReturnedCount); - Assert.Equal(1, secondContext2.ReturnedCount); - Assert.Equal(!async ? true : null, context1.SyncLease); - Assert.Equal(async ? true : null, context1.AsyncLease); - Assert.Equal(!async ? true : null, context1.SyncReturn); - Assert.Equal(async ? true : null, context1.AsyncReturn); - - await Dispose(context1, async); - await Dispose(secondContext1, async); - await Dispose(context2, async); - await Dispose(secondContext2, async); - - Assert.Equal(2, context1.LeasedCount); - Assert.Equal(2, secondContext1.LeasedCount); - Assert.Equal(2, context2.LeasedCount); - Assert.Equal(2, secondContext2!.LeasedCount); - Assert.Equal(2, context1.ReturnedCount); - Assert.Equal(2, secondContext1.ReturnedCount); - Assert.Equal(2, context2.ReturnedCount); - Assert.Equal(2, secondContext2.ReturnedCount); - Assert.Equal(!async ? true : null, context1.SyncLease); - Assert.Equal(async ? true : null, context1.AsyncLease); - Assert.Equal(!async ? true : null, context1.SyncReturn); - Assert.Equal(async ? true : null, context1.AsyncReturn); } [ConditionalTheory] @@ -1235,7 +703,7 @@ 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()); @@ -1252,25 +720,21 @@ 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.NotNull(GetContextEventField(context1, nameof(DbContext.SavingChanges))); - Assert.NotNull(GetContextEventField(context1, nameof(DbContext.SavedChanges))); - Assert.NotNull(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.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); @@ -1330,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.NotNull(GetContextEventField(context1, nameof(DbContext.SavingChanges))); - Assert.NotNull(GetContextEventField(context1, nameof(DbContext.SavedChanges))); - Assert.NotNull(GetContextEventField(context1, nameof(DbContext.SaveChangesFailed))); - 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] @@ -1594,19 +1055,9 @@ public async Task Pool_disposes_context_when_context_not_pooled(bool useInterfac ? (PooledContext)scopedProvider2.GetService() : scopedProvider2.GetService(); - Assert.Equal(1, context1!.LeasedCount); - Assert.Equal(1, context2!.LeasedCount); - Assert.Equal(0, context1.ReturnedCount); - Assert.Equal(0, context2.ReturnedCount); - await Dispose(serviceScope1, async); await Dispose(serviceScope2, async); - Assert.Equal(1, context1!.LeasedCount); - Assert.Equal(1, context2!.LeasedCount); - Assert.Equal(1, context1.ReturnedCount); - Assert.Equal(1, context2.ReturnedCount); - Assert.Throws(() => context1.Customers.ToList()); Assert.Throws(() => context2.Customers.ToList()); } @@ -1674,19 +1125,10 @@ public async Task Double_dispose_does_not_enter_pool_twice(bool useInterface, bo var lease = scope.ServiceProvider.GetRequiredService>(); var context = lease.Context; - Assert.Equal(1, context.LeasedCount); - Assert.Equal(0, context.ReturnedCount); - await Dispose(scope, async); - Assert.Equal(1, context.LeasedCount); - Assert.Equal(1, context.ReturnedCount); - await Dispose(scope, async); - Assert.Equal(1, context.LeasedCount); - Assert.Equal(1, context.ReturnedCount); - using var scope1 = serviceProvider.CreateScope(); var lease1 = scope1.ServiceProvider.GetRequiredService>(); @@ -1713,19 +1155,10 @@ public async Task Double_dispose_with_standalone_lease_does_not_enter_pool_twice var context = (PooledContext)lease.Context; ((IDbContextPoolable)context).SetLease(lease); - Assert.Equal(1, context.LeasedCount); - Assert.Equal(0, context.ReturnedCount); - await Dispose(context, async); - Assert.Equal(1, context.LeasedCount); - Assert.Equal(1, context.ReturnedCount); - await Dispose(context, async); - Assert.Equal(1, context.LeasedCount); - Assert.Equal(1, context.ReturnedCount); - using var context1 = new DbContextLease(pool, standalone: true).Context; using var context2 = new DbContextLease(pool, standalone: true).Context; diff --git a/test/EFCore.Tests/DbContextTest.cs b/test/EFCore.Tests/DbContextTest.cs index 29f1033d896..1763731efd8 100644 --- a/test/EFCore.Tests/DbContextTest.cs +++ b/test/EFCore.Tests/DbContextTest.cs @@ -763,7 +763,7 @@ public async Task It_throws_object_disposed_exception(bool async) (await Assert.ThrowsAsync(() => context.FindAsync(typeof(Random), 77).AsTask())).Message); var methodCount = typeof(DbContext).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly).Count(); - var expectedMethodCount = 54; + var expectedMethodCount = 50; Assert.True( methodCount == expectedMethodCount, userMessage: $"Expected {expectedMethodCount} methods on DbContext but found {methodCount}. "