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