diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs index 37fbc34aa32..bbd63a076b1 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs @@ -35,6 +35,7 @@ private sealed class QueryingEnumerable : IEnumerable, IAsyncEnumerable private readonly string _partitionKey; private readonly IDiagnosticsLogger _queryLogger; private readonly bool _standAloneStateManager; + private readonly bool _concurrencyDetectionEnabled; public QueryingEnumerable( CosmosQueryContext cosmosQueryContext, @@ -44,7 +45,8 @@ public QueryingEnumerable( Func shaper, Type contextType, string partitionKeyFromExtension, - bool standAloneStateManager) + bool standAloneStateManager, + bool concurrencyDetectionEnabled) { _cosmosQueryContext = cosmosQueryContext; _sqlExpressionFactory = sqlExpressionFactory; @@ -54,6 +56,7 @@ public QueryingEnumerable( _contextType = contextType; _queryLogger = cosmosQueryContext.QueryLogger; _standAloneStateManager = standAloneStateManager; + _concurrencyDetectionEnabled = concurrencyDetectionEnabled; var partitionKey = selectExpression.GetPartitionKey(cosmosQueryContext.ParameterValues); if (partitionKey != null && partitionKeyFromExtension != null && partitionKeyFromExtension != partitionKey) @@ -113,6 +116,7 @@ private sealed class Enumerator : IEnumerator private readonly string _partitionKey; private readonly IDiagnosticsLogger _queryLogger; private readonly bool _standAloneStateManager; + private readonly IConcurrencyDetector _concurrencyDetector; private IEnumerator _enumerator; @@ -126,6 +130,10 @@ public Enumerator(QueryingEnumerable queryingEnumerable) _partitionKey = queryingEnumerable._partitionKey; _queryLogger = queryingEnumerable._queryLogger; _standAloneStateManager = queryingEnumerable._standAloneStateManager; + + _concurrencyDetector = queryingEnumerable._concurrencyDetectionEnabled + ? _cosmosQueryContext.ConcurrencyDetector + : null; } public T Current { get; private set; } @@ -137,7 +145,9 @@ public bool MoveNext() { try { - using (_cosmosQueryContext.ConcurrencyDetector.EnterCriticalSection()) + _concurrencyDetector?.EnterCriticalSection(); + + try { if (_enumerator == null) { @@ -163,6 +173,10 @@ public bool MoveNext() return hasNext; } + finally + { + _concurrencyDetector?.ExitCriticalSection(); + } } catch (Exception exception) { @@ -193,6 +207,7 @@ private sealed class AsyncEnumerator : IAsyncEnumerator private readonly IDiagnosticsLogger _queryLogger; private readonly bool _standAloneStateManager; private readonly CancellationToken _cancellationToken; + private readonly IConcurrencyDetector _concurrencyDetector; private IAsyncEnumerator _enumerator; @@ -207,6 +222,10 @@ public AsyncEnumerator(QueryingEnumerable queryingEnumerable, CancellationTok _queryLogger = queryingEnumerable._queryLogger; _standAloneStateManager = queryingEnumerable._standAloneStateManager; _cancellationToken = cancellationToken; + + _concurrencyDetector = queryingEnumerable._concurrencyDetectionEnabled + ? _cosmosQueryContext.ConcurrencyDetector + : null; } public T Current { get; private set; } @@ -215,7 +234,9 @@ public async ValueTask MoveNextAsync() { try { - using (_cosmosQueryContext.ConcurrencyDetector.EnterCriticalSection()) + _concurrencyDetector?.EnterCriticalSection(); + + try { if (_enumerator == null) { @@ -241,6 +262,10 @@ public async ValueTask MoveNextAsync() return hasNext; } + finally + { + _concurrencyDetector?.ExitCriticalSection(); + } } catch (Exception exception) { diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs index 08234303fb5..4110db254e9 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.ReadItemQueryingEnumerable.cs @@ -35,13 +35,15 @@ private sealed class ReadItemQueryingEnumerable : IEnumerable, IAsyncEnume private readonly Type _contextType; private readonly IDiagnosticsLogger _queryLogger; private readonly bool _standAloneStateManager; + private readonly bool _concurrencyDetectionEnabled; public ReadItemQueryingEnumerable( CosmosQueryContext cosmosQueryContext, ReadItemExpression readItemExpression, Func shaper, Type contextType, - bool standAloneStateManager) + bool standAloneStateManager, + bool concurrencyDetectionEnabled) { _cosmosQueryContext = cosmosQueryContext; _readItemExpression = readItemExpression; @@ -49,6 +51,7 @@ public ReadItemQueryingEnumerable( _contextType = contextType; _queryLogger = _cosmosQueryContext.QueryLogger; _standAloneStateManager = standAloneStateManager; + _concurrencyDetectionEnabled = concurrencyDetectionEnabled; } public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) @@ -173,6 +176,7 @@ private sealed class Enumerator : IEnumerator, IAsyncEnumerator private readonly Type _contextType; private readonly IDiagnosticsLogger _queryLogger; private readonly bool _standAloneStateManager; + private readonly IConcurrencyDetector _concurrencyDetector; private readonly ReadItemQueryingEnumerable _readItemEnumerable; private readonly CancellationToken _cancellationToken; @@ -189,6 +193,10 @@ public Enumerator(ReadItemQueryingEnumerable readItemEnumerable, Cancellation _standAloneStateManager = readItemEnumerable._standAloneStateManager; _readItemEnumerable = readItemEnumerable; _cancellationToken = cancellationToken; + + _concurrencyDetector = readItemEnumerable._concurrencyDetectionEnabled + ? _cosmosQueryContext.ConcurrencyDetector + : null; } object IEnumerator.Current @@ -200,7 +208,9 @@ public bool MoveNext() { try { - using (_cosmosQueryContext.ConcurrencyDetector.EnterCriticalSection()) + _concurrencyDetector?.EnterCriticalSection(); + + try { if (!_hasExecuted) { @@ -226,6 +236,10 @@ public bool MoveNext() return false; } + finally + { + _concurrencyDetector?.ExitCriticalSection(); + } } catch (Exception exception) { @@ -239,7 +253,9 @@ public async ValueTask MoveNextAsync() { try { - using (_cosmosQueryContext.ConcurrencyDetector.EnterCriticalSection()) + _concurrencyDetector?.EnterCriticalSection(); + + try { if (!_hasExecuted) { @@ -267,6 +283,10 @@ public async ValueTask MoveNextAsync() return false; } + finally + { + _concurrencyDetector?.ExitCriticalSection(); + } } catch (Exception exception) { diff --git a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs index a46dd21dcd7..03069077352 100644 --- a/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Cosmos/Query/Internal/CosmosShapedQueryCompilingExpressionVisitor.cs @@ -22,6 +22,7 @@ public partial class CosmosShapedQueryCompilingExpressionVisitor : ShapedQueryCo private readonly ISqlExpressionFactory _sqlExpressionFactory; private readonly IQuerySqlGeneratorFactory _querySqlGeneratorFactory; private readonly Type _contextType; + private readonly bool _concurrencyDetectionEnabled; private readonly string _partitionKeyFromExtension; /// @@ -40,6 +41,7 @@ public CosmosShapedQueryCompilingExpressionVisitor( _sqlExpressionFactory = sqlExpressionFactory; _querySqlGeneratorFactory = querySqlGeneratorFactory; _contextType = cosmosQueryCompilationContext.ContextType; + _concurrencyDetectionEnabled = dependencies.CoreSingletonOptions.IsConcurrencyDetectionEnabled; _partitionKeyFromExtension = cosmosQueryCompilationContext.PartitionKeyFromExtension; } @@ -85,7 +87,8 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery Expression.Constant(_contextType), Expression.Constant(_partitionKeyFromExtension, typeof(string)), Expression.Constant( - QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution)); + QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution), + Expression.Constant(_concurrencyDetectionEnabled)); case ReadItemExpression readItemExpression: @@ -108,7 +111,8 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery Expression.Constant(shaperReadItemLambda.Compile()), Expression.Constant(_contextType), Expression.Constant( - QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution)); + QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution), + Expression.Constant(_concurrencyDetectionEnabled)); default: throw new NotSupportedException(CoreStrings.UnhandledExpressionNode(shapedQueryExpression.QueryExpression)); diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs index 6daac465530..399705be080 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.QueryingEnumerable.cs @@ -33,13 +33,15 @@ private sealed class QueryingEnumerable : IAsyncEnumerable, IEnumerable private readonly Type _contextType; private readonly IDiagnosticsLogger _queryLogger; private readonly bool _standAloneStateManager; + private readonly bool _concurrencyDetectionEnabled; public QueryingEnumerable( QueryContext queryContext, IEnumerable innerEnumerable, Func shaper, Type contextType, - bool standAloneStateManager) + bool standAloneStateManager, + bool concurrencyDetectionEnabled) { _queryContext = queryContext; _innerEnumerable = innerEnumerable; @@ -47,6 +49,7 @@ public QueryingEnumerable( _contextType = contextType; _queryLogger = queryContext.QueryLogger; _standAloneStateManager = standAloneStateManager; + _concurrencyDetectionEnabled = concurrencyDetectionEnabled; } public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) @@ -70,6 +73,7 @@ private sealed class Enumerator : IEnumerator, IAsyncEnumerator private readonly IDiagnosticsLogger _queryLogger; private readonly bool _standAloneStateManager; private readonly CancellationToken _cancellationToken; + private readonly IConcurrencyDetector? _concurrencyDetector; private IEnumerator? _enumerator; @@ -83,6 +87,10 @@ public Enumerator(QueryingEnumerable queryingEnumerable, CancellationToken ca _standAloneStateManager = queryingEnumerable._standAloneStateManager; _cancellationToken = cancellationToken; Current = default!; + + _concurrencyDetector = queryingEnumerable._concurrencyDetectionEnabled + ? _queryContext.ConcurrencyDetector + : null; } public T Current { get; private set; } @@ -94,10 +102,16 @@ public bool MoveNext() { try { - using (_queryContext.ConcurrencyDetector.EnterCriticalSection()) + _concurrencyDetector?.EnterCriticalSection(); + + try { return MoveNextHelper(); } + finally + { + _concurrencyDetector?.ExitCriticalSection(); + } } catch (Exception exception) { @@ -111,12 +125,18 @@ public ValueTask MoveNextAsync() { try { - using (_queryContext.ConcurrencyDetector.EnterCriticalSection()) + _concurrencyDetector?.EnterCriticalSection(); + + try { _cancellationToken.ThrowIfCancellationRequested(); return new ValueTask(MoveNextHelper()); } + finally + { + _concurrencyDetector?.ExitCriticalSection(); + } } catch (Exception exception) { diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.cs index 3165dcc2bf0..2162a17c8dd 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.cs @@ -18,6 +18,7 @@ namespace Microsoft.EntityFrameworkCore.InMemory.Query.Internal public partial class InMemoryShapedQueryCompilingExpressionVisitor : ShapedQueryCompilingExpressionVisitor { private readonly Type _contextType; + private readonly bool _concurrencyDetectionEnabled; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -31,6 +32,7 @@ public InMemoryShapedQueryCompilingExpressionVisitor( : base(dependencies, queryCompilationContext) { _contextType = queryCompilationContext.ContextType; + _concurrencyDetectionEnabled = dependencies.CoreSingletonOptions.IsConcurrencyDetectionEnabled; } /// @@ -93,7 +95,8 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery Expression.Constant(shaperLambda.Compile()), Expression.Constant(_contextType), Expression.Constant( - QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution)); + QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution), + Expression.Constant(_concurrencyDetectionEnabled)); } private static readonly MethodInfo _tableMethodInfo diff --git a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs index 1fa78ea0c02..9043640373b 100644 --- a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs @@ -221,10 +221,14 @@ public static int ExecuteSqlRaw( Check.NotNull(parameters, nameof(parameters)); var facadeDependencies = GetFacadeDependencies(databaseFacade); - var concurrencyDetector = facadeDependencies.ConcurrencyDetector; + var concurrencyDetector = facadeDependencies.CoreOptions.IsConcurrencyDetectionEnabled + ? facadeDependencies.ConcurrencyDetector + : null; var logger = facadeDependencies.CommandLogger; - using (concurrencyDetector.EnterCriticalSection()) + concurrencyDetector?.EnterCriticalSection(); + + try { var rawSqlCommand = facadeDependencies.RawSqlCommandBuilder .Build(sql, parameters); @@ -239,6 +243,10 @@ public static int ExecuteSqlRaw( ((IDatabaseFacadeDependenciesAccessor)databaseFacade).Context, logger)); } + finally + { + concurrencyDetector?.ExitCriticalSection(); + } } /// @@ -393,10 +401,14 @@ public static async Task ExecuteSqlRawAsync( Check.NotNull(parameters, nameof(parameters)); var facadeDependencies = GetFacadeDependencies(databaseFacade); - var concurrencyDetector = facadeDependencies.ConcurrencyDetector; + var concurrencyDetector = facadeDependencies.CoreOptions.IsConcurrencyDetectionEnabled + ? facadeDependencies.ConcurrencyDetector + : null; var logger = facadeDependencies.CommandLogger; - using (concurrencyDetector.EnterCriticalSection()) + concurrencyDetector?.EnterCriticalSection(); + + try { var rawSqlCommand = facadeDependencies.RawSqlCommandBuilder .Build(sql, parameters); @@ -413,6 +425,10 @@ public static async Task ExecuteSqlRawAsync( cancellationToken) .ConfigureAwait(false); } + finally + { + concurrencyDetector?.ExitCriticalSection(); + } } /// diff --git a/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs index 1b34616fd67..f4810a2e869 100644 --- a/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Internal/FromSqlQueryingEnumerable.cs @@ -33,6 +33,7 @@ public class FromSqlQueryingEnumerable : IEnumerable, IAsyncEnumerable, private readonly IDiagnosticsLogger _queryLogger; private readonly bool _standAloneStateManager; private readonly bool _detailedErrorsEnabled; + private readonly bool _concurrencyDetectionEnabled; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -47,7 +48,8 @@ public FromSqlQueryingEnumerable( [NotNull] Func shaper, [NotNull] Type contextType, bool standAloneStateManager, - bool detailedErrorsEnabled) + bool detailedErrorsEnabled, + bool concurrencyDetectionEnabled) { _relationalQueryContext = relationalQueryContext; _relationalCommandCache = relationalCommandCache; @@ -57,6 +59,7 @@ public FromSqlQueryingEnumerable( _queryLogger = relationalQueryContext.QueryLogger; _standAloneStateManager = standAloneStateManager; _detailedErrorsEnabled = detailedErrorsEnabled; + _concurrencyDetectionEnabled = concurrencyDetectionEnabled; } /// @@ -155,6 +158,7 @@ private sealed class Enumerator : IEnumerator private readonly IDiagnosticsLogger _queryLogger; private readonly bool _standAloneStateManager; private readonly bool _detailedErrorsEnabled; + private readonly IConcurrencyDetector? _concurrencyDetector; private RelationalDataReader? _dataReader; private int[]? _indexMap; @@ -170,6 +174,10 @@ public Enumerator(FromSqlQueryingEnumerable queryingEnumerable) _standAloneStateManager = queryingEnumerable._standAloneStateManager; _detailedErrorsEnabled = queryingEnumerable._detailedErrorsEnabled; Current = default!; + + _concurrencyDetector = queryingEnumerable._concurrencyDetectionEnabled + ? _relationalQueryContext.ConcurrencyDetector + : null; } public T Current { get; private set; } @@ -181,7 +189,9 @@ public bool MoveNext() { try { - using (_relationalQueryContext.ConcurrencyDetector.EnterCriticalSection()) + _concurrencyDetector?.EnterCriticalSection(); + + try { if (_dataReader == null) { @@ -197,6 +207,10 @@ public bool MoveNext() return hasNext; } + finally + { + _concurrencyDetector?.ExitCriticalSection(); + } } catch (Exception exception) { @@ -248,6 +262,7 @@ private sealed class AsyncEnumerator : IAsyncEnumerator private readonly IDiagnosticsLogger _queryLogger; private readonly bool _standAloneStateManager; private readonly bool _detailedErrorsEnabled; + private readonly IConcurrencyDetector? _concurrencyDetector; private RelationalDataReader? _dataReader; private int[]? _indexMap; @@ -263,6 +278,10 @@ public AsyncEnumerator(FromSqlQueryingEnumerable queryingEnumerable) _standAloneStateManager = queryingEnumerable._standAloneStateManager; _detailedErrorsEnabled = queryingEnumerable._detailedErrorsEnabled; Current = default!; + + _concurrencyDetector = queryingEnumerable._concurrencyDetectionEnabled + ? _relationalQueryContext.ConcurrencyDetector + : null; } public T Current { get; private set; } @@ -271,12 +290,15 @@ public async ValueTask MoveNextAsync() { try { - using (_relationalQueryContext.ConcurrencyDetector.EnterCriticalSection()) + _concurrencyDetector?.EnterCriticalSection(); + + try { if (_dataReader == null) { await _relationalQueryContext.ExecutionStrategyFactory.Create() - .ExecuteAsync(true, InitializeReaderAsync, null, _relationalQueryContext.CancellationToken).ConfigureAwait(false); + .ExecuteAsync(true, InitializeReaderAsync, null, _relationalQueryContext.CancellationToken) + .ConfigureAwait(false); } var hasNext = await _dataReader!.ReadAsync(_relationalQueryContext.CancellationToken).ConfigureAwait(false); @@ -287,6 +309,10 @@ await _relationalQueryContext.ExecutionStrategyFactory.Create() return hasNext; } + finally + { + _concurrencyDetector?.ExitCriticalSection(); + } } catch (Exception exception) { diff --git a/src/EFCore.Relational/Query/Internal/SingleQueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/SingleQueryingEnumerable.cs index ac913e11614..f292593e818 100644 --- a/src/EFCore.Relational/Query/Internal/SingleQueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Internal/SingleQueryingEnumerable.cs @@ -31,6 +31,7 @@ public class SingleQueryingEnumerable : IEnumerable, IAsyncEnumerable, private readonly IDiagnosticsLogger _queryLogger; private readonly bool _standAloneStateManager; private readonly bool _detailedErrorsEnabled; + private readonly bool _concurrencyDetectionEnabled; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -44,7 +45,8 @@ public SingleQueryingEnumerable( [NotNull] Func shaper, [NotNull] Type contextType, bool standAloneStateManager, - bool detailedErrorsEnabled) + bool detailedErrorsEnabled, + bool concurrencyDetectionEnabled) { _relationalQueryContext = relationalQueryContext; _relationalCommandCache = relationalCommandCache; @@ -53,6 +55,7 @@ public SingleQueryingEnumerable( _queryLogger = relationalQueryContext.QueryLogger; _standAloneStateManager = standAloneStateManager; _detailedErrorsEnabled = detailedErrorsEnabled; + _concurrencyDetectionEnabled = concurrencyDetectionEnabled; } /// @@ -123,6 +126,7 @@ private sealed class Enumerator : IEnumerator private readonly IDiagnosticsLogger _queryLogger; private readonly bool _standAloneStateManager; private readonly bool _detailedErrorsEnabled; + private readonly IConcurrencyDetector? _concurrencyDetector; private RelationalDataReader? _dataReader; private SingleQueryResultCoordinator? _resultCoordinator; @@ -137,6 +141,10 @@ public Enumerator(SingleQueryingEnumerable queryingEnumerable) _standAloneStateManager = queryingEnumerable._standAloneStateManager; _detailedErrorsEnabled = queryingEnumerable._detailedErrorsEnabled; Current = default!; + + _concurrencyDetector = queryingEnumerable._concurrencyDetectionEnabled + ? _relationalQueryContext.ConcurrencyDetector + : null; } public T Current { get; private set; } @@ -148,7 +156,9 @@ public bool MoveNext() { try { - using (_relationalQueryContext.ConcurrencyDetector.EnterCriticalSection()) + _concurrencyDetector?.EnterCriticalSection(); + + try { if (_dataReader == null) { @@ -191,6 +201,10 @@ public bool MoveNext() return hasNext; } + finally + { + _concurrencyDetector?.ExitCriticalSection(); + } } catch (Exception exception) { @@ -241,6 +255,7 @@ private sealed class AsyncEnumerator : IAsyncEnumerator private readonly IDiagnosticsLogger _queryLogger; private readonly bool _standAloneStateManager; private readonly bool _detailedErrorsEnabled; + private readonly IConcurrencyDetector? _concurrencyDetector; private RelationalDataReader? _dataReader; private SingleQueryResultCoordinator? _resultCoordinator; @@ -255,6 +270,10 @@ public AsyncEnumerator(SingleQueryingEnumerable queryingEnumerable) _standAloneStateManager = queryingEnumerable._standAloneStateManager; _detailedErrorsEnabled = queryingEnumerable._detailedErrorsEnabled; Current = default!; + + _concurrencyDetector = queryingEnumerable._concurrencyDetectionEnabled + ? _relationalQueryContext.ConcurrencyDetector + : null; } public T Current { get; private set; } @@ -263,7 +282,9 @@ public async ValueTask MoveNextAsync() { try { - using (_relationalQueryContext.ConcurrencyDetector.EnterCriticalSection()) + _concurrencyDetector?.EnterCriticalSection(); + + try { if (_dataReader == null) { @@ -308,6 +329,10 @@ await _relationalQueryContext.ExecutionStrategyFactory.Create() return hasNext; } + finally + { + _concurrencyDetector?.ExitCriticalSection(); + } } catch (Exception exception) { diff --git a/src/EFCore.Relational/Query/Internal/SplitQueryingEnumerable.cs b/src/EFCore.Relational/Query/Internal/SplitQueryingEnumerable.cs index c6fb4c2a2e7..906ed8e3c6a 100644 --- a/src/EFCore.Relational/Query/Internal/SplitQueryingEnumerable.cs +++ b/src/EFCore.Relational/Query/Internal/SplitQueryingEnumerable.cs @@ -33,6 +33,7 @@ public class SplitQueryingEnumerable : IEnumerable, IAsyncEnumerable, I private readonly IDiagnosticsLogger _queryLogger; private readonly bool _standAloneStateManager; private readonly bool _detailedErrorsEnabled; + private readonly bool _concurrencyDetectionEnabled; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -48,7 +49,8 @@ public SplitQueryingEnumerable( [NotNull] Func relatedDataLoadersAsync, [NotNull] Type contextType, bool standAloneStateManager, - bool detailedErrorsEnabled) + bool detailedErrorsEnabled, + bool concurrencyDetectionEnabled) { _relationalQueryContext = relationalQueryContext; _relationalCommandCache = relationalCommandCache; @@ -59,6 +61,7 @@ public SplitQueryingEnumerable( _queryLogger = relationalQueryContext.QueryLogger; _standAloneStateManager = standAloneStateManager; _detailedErrorsEnabled = detailedErrorsEnabled; + _concurrencyDetectionEnabled = concurrencyDetectionEnabled; } /// @@ -131,6 +134,7 @@ private sealed class Enumerator : IEnumerator private readonly IDiagnosticsLogger _queryLogger; private readonly bool _standAloneStateManager; private readonly bool _detailedErrorsEnabled; + private readonly IConcurrencyDetector? _concurrencyDetector; private RelationalDataReader? _dataReader; private SplitQueryResultCoordinator? _resultCoordinator; @@ -147,6 +151,10 @@ public Enumerator(SplitQueryingEnumerable queryingEnumerable) _standAloneStateManager = queryingEnumerable._standAloneStateManager; _detailedErrorsEnabled = queryingEnumerable._detailedErrorsEnabled; Current = default!; + + _concurrencyDetector = queryingEnumerable._concurrencyDetectionEnabled + ? _relationalQueryContext.ConcurrencyDetector + : null; } public T Current { get; private set; } @@ -158,7 +166,9 @@ public bool MoveNext() { try { - using (_relationalQueryContext.ConcurrencyDetector.EnterCriticalSection()) + _concurrencyDetector?.EnterCriticalSection(); + + try { if (_dataReader == null) { @@ -185,6 +195,10 @@ public bool MoveNext() return hasNext; } + finally + { + _concurrencyDetector?.ExitCriticalSection(); + } } catch (Exception exception) { @@ -248,6 +262,7 @@ private sealed class AsyncEnumerator : IAsyncEnumerator private readonly IDiagnosticsLogger _queryLogger; private readonly bool _standAloneStateManager; private readonly bool _detailedErrorEnabled; + private readonly IConcurrencyDetector? _concurrencyDetector; private RelationalDataReader? _dataReader; private SplitQueryResultCoordinator? _resultCoordinator; @@ -264,6 +279,10 @@ public AsyncEnumerator(SplitQueryingEnumerable queryingEnumerable) _standAloneStateManager = queryingEnumerable._standAloneStateManager; _detailedErrorEnabled = queryingEnumerable._detailedErrorsEnabled; Current = default!; + + _concurrencyDetector = queryingEnumerable._concurrencyDetectionEnabled + ? _relationalQueryContext.ConcurrencyDetector + : null; } public T Current { get; private set; } @@ -272,7 +291,9 @@ public async ValueTask MoveNextAsync() { try { - using (_relationalQueryContext.ConcurrencyDetector.EnterCriticalSection()) + _concurrencyDetector?.EnterCriticalSection(); + + try { if (_dataReader == null) { @@ -305,6 +326,10 @@ await _relatedDataLoaders(_relationalQueryContext, _executionStrategy!, _resultC return hasNext; } + finally + { + _concurrencyDetector?.ExitCriticalSection(); + } } catch (Exception exception) { diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs index 264d5522a11..2f9281c091a 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.cs @@ -22,6 +22,7 @@ public partial class RelationalShapedQueryCompilingExpressionVisitor : ShapedQue { private readonly Type _contextType; private readonly ISet _tags; + private readonly bool _concurrencyDetectionEnabled; private readonly bool _detailedErrorsEnabled; private readonly bool _useRelationalNulls; @@ -43,7 +44,8 @@ public RelationalShapedQueryCompilingExpressionVisitor( _contextType = queryCompilationContext.ContextType; _tags = queryCompilationContext.Tags; - _detailedErrorsEnabled = relationalDependencies.CoreSingletonOptions.AreDetailedErrorsEnabled; + _concurrencyDetectionEnabled = dependencies.CoreSingletonOptions.IsConcurrencyDetectionEnabled; + _detailedErrorsEnabled = dependencies.CoreSingletonOptions.AreDetailedErrorsEnabled; _useRelationalNulls = RelationalOptionsExtension.Extract(queryCompilationContext.ContextOptions).UseRelationalNulls; } @@ -79,7 +81,8 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery Expression.Constant(_contextType), Expression.Constant( QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution), - Expression.Constant(_detailedErrorsEnabled)); + Expression.Constant(_detailedErrorsEnabled), + Expression.Constant(_concurrencyDetectionEnabled)); } if (splitQuery) @@ -102,7 +105,8 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery Expression.Constant(_contextType), Expression.Constant( QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution), - Expression.Constant(_detailedErrorsEnabled)); + Expression.Constant(_detailedErrorsEnabled), + Expression.Constant(_concurrencyDetectionEnabled)); } return Expression.New( @@ -113,7 +117,8 @@ protected override Expression VisitShapedQuery(ShapedQueryExpression shapedQuery Expression.Constant(_contextType), Expression.Constant( QueryCompilationContext.QueryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution), - Expression.Constant(_detailedErrorsEnabled)); + Expression.Constant(_detailedErrorsEnabled), + Expression.Constant(_concurrencyDetectionEnabled)); } } } diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitorDependencies.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitorDependencies.cs index 4b36639edbf..695e6e93ec3 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitorDependencies.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitorDependencies.cs @@ -61,8 +61,7 @@ public RelationalShapedQueryCompilingExpressionVisitorDependencies( [NotNull] IQuerySqlGeneratorFactory querySqlGeneratorFactory, [NotNull] ISqlExpressionFactory sqlExpressionFactory, [NotNull] IParameterNameGeneratorFactory parameterNameGeneratorFactory, - [NotNull] IRelationalParameterBasedSqlProcessorFactory relationalParameterBasedSqlProcessorFactory, - [NotNull] ICoreSingletonOptions coreSingletonOptions) + [NotNull] IRelationalParameterBasedSqlProcessorFactory relationalParameterBasedSqlProcessorFactory) { Check.NotNull(querySqlGeneratorFactory, nameof(querySqlGeneratorFactory)); Check.NotNull(sqlExpressionFactory, nameof(sqlExpressionFactory)); @@ -75,7 +74,6 @@ public RelationalShapedQueryCompilingExpressionVisitorDependencies( ParameterNameGeneratorFactory = parameterNameGeneratorFactory; #pragma warning restore CS0618 // Type or member is obsolete RelationalParameterBasedSqlProcessorFactory = relationalParameterBasedSqlProcessorFactory; - CoreSingletonOptions = coreSingletonOptions; } /// @@ -99,10 +97,5 @@ public RelationalShapedQueryCompilingExpressionVisitorDependencies( /// The SQL processor based on parameter values. /// public IRelationalParameterBasedSqlProcessorFactory RelationalParameterBasedSqlProcessorFactory { get; [param: NotNull] init; } - - /// - /// Core singleton options. - /// - public ICoreSingletonOptions CoreSingletonOptions { get; [param: NotNull] init; } } } diff --git a/src/EFCore.Relational/Storage/Internal/RelationalDatabaseFacadeDependencies.cs b/src/EFCore.Relational/Storage/Internal/RelationalDatabaseFacadeDependencies.cs index ef86f1159ac..578aa18ce37 100644 --- a/src/EFCore.Relational/Storage/Internal/RelationalDatabaseFacadeDependencies.cs +++ b/src/EFCore.Relational/Storage/Internal/RelationalDatabaseFacadeDependencies.cs @@ -32,7 +32,8 @@ public RelationalDatabaseFacadeDependencies( [NotNull] IDiagnosticsLogger commandLogger, [NotNull] IConcurrencyDetector concurrencyDetector, [NotNull] IRelationalConnection relationalConnection, - [NotNull] IRawSqlCommandBuilder rawSqlCommandBuilder) + [NotNull] IRawSqlCommandBuilder rawSqlCommandBuilder, + [NotNull] ICoreSingletonOptions coreOptions) { TransactionManager = transactionManager; DatabaseCreator = databaseCreator; @@ -42,6 +43,7 @@ public RelationalDatabaseFacadeDependencies( ConcurrencyDetector = concurrencyDetector; RelationalConnection = relationalConnection; RawSqlCommandBuilder = rawSqlCommandBuilder; + CoreOptions = coreOptions; } /// @@ -107,5 +109,13 @@ public RelationalDatabaseFacadeDependencies( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual IRawSqlCommandBuilder RawSqlCommandBuilder { get; [param: NotNull] init; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ICoreSingletonOptions CoreOptions { get; [param: NotNull] init; } } } diff --git a/src/EFCore/ChangeTracking/Internal/StateManager.cs b/src/EFCore/ChangeTracking/Internal/StateManager.cs index faa38822d68..1be28bb76d2 100644 --- a/src/EFCore/ChangeTracking/Internal/StateManager.cs +++ b/src/EFCore/ChangeTracking/Internal/StateManager.cs @@ -70,7 +70,9 @@ public StateManager([NotNull] StateManagerDependencies dependencies) ValueGenerationManager = dependencies.ValueGenerationManager; _model = dependencies.Model; _database = dependencies.Database; - _concurrencyDetector = dependencies.ConcurrencyDetector; + _concurrencyDetector = dependencies.CoreSingletonOptions.IsConcurrencyDetectionEnabled + ? dependencies.ConcurrencyDetector + : null; Context = dependencies.CurrentContext.Context; EntityFinderFactory = new EntityFinderFactory( dependencies.EntityFinderSource, this, dependencies.SetSource, dependencies.CurrentContext.Context); @@ -1085,12 +1087,18 @@ private static bool KeyValuesEqual(IProperty property, object value, object curr /// protected virtual int SaveChanges([NotNull] IList entriesToSave) { - using (_concurrencyDetector.EnterCriticalSection()) + _concurrencyDetector?.EnterCriticalSection(); + + try { EntityFrameworkEventSource.Log.SavingChanges(); return _database.SaveChanges(entriesToSave); } + finally + { + _concurrencyDetector?.ExitCriticalSection(); + } } /// @@ -1103,13 +1111,19 @@ protected virtual async Task SaveChangesAsync( [NotNull] IList entriesToSave, CancellationToken cancellationToken = default) { - using (_concurrencyDetector.EnterCriticalSection()) + _concurrencyDetector?.EnterCriticalSection(); + + try { EntityFrameworkEventSource.Log.SavingChanges(); return await _database.SaveChangesAsync(entriesToSave, cancellationToken) .ConfigureAwait(false); } + finally + { + _concurrencyDetector?.ExitCriticalSection(); + } } /// diff --git a/src/EFCore/ChangeTracking/Internal/StateManagerDependencies.cs b/src/EFCore/ChangeTracking/Internal/StateManagerDependencies.cs index 038f584e267..7d494138e8b 100644 --- a/src/EFCore/ChangeTracking/Internal/StateManagerDependencies.cs +++ b/src/EFCore/ChangeTracking/Internal/StateManagerDependencies.cs @@ -76,6 +76,7 @@ public StateManagerDependencies( [NotNull] IDbSetSource setSource, [NotNull] IEntityMaterializerSource entityMaterializerSource, [NotNull] IExecutionStrategyFactory executionStrategyFactory, + [NotNull] ICoreSingletonOptions coreSingletonOptions, [NotNull] ILoggingOptions loggingOptions, [NotNull] IDiagnosticsLogger updateLogger, [NotNull] IDiagnosticsLogger changeTrackingLogger) @@ -92,6 +93,7 @@ public StateManagerDependencies( SetSource = setSource; EntityMaterializerSource = entityMaterializerSource; ExecutionStrategyFactory = executionStrategyFactory; + CoreSingletonOptions = coreSingletonOptions; LoggingOptions = loggingOptions; UpdateLogger = updateLogger; ChangeTrackingLogger = changeTrackingLogger; @@ -195,6 +197,14 @@ public StateManagerDependencies( /// public IExecutionStrategyFactory ExecutionStrategyFactory { get; [param: NotNull] init; } + /// + /// 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 ICoreSingletonOptions CoreSingletonOptions { get; [param: NotNull] init; } + /// /// 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/DbContextOptionsBuilder.cs b/src/EFCore/DbContextOptionsBuilder.cs index fb18095fbee..b21c875186b 100644 --- a/src/EFCore/DbContextOptionsBuilder.cs +++ b/src/EFCore/DbContextOptionsBuilder.cs @@ -336,6 +336,28 @@ public virtual DbContextOptionsBuilder LogTo( private DbContextOptionsBuilder LogTo([NotNull] IDbContextLogger logger) => WithOption(e => e.WithDbContextLogger(logger)); + /// + /// + /// Disables concurrency detection, which detects many cases of erroneous concurrent usage of a + /// instance and causes an informative exception to be thrown. This provides a minor performance improvement, but if a + /// instance is used concurrently, the behavior will be undefined and the program may fail in + /// unpredictable ways. + /// + /// + /// Only disable concurrency detection after confirming that the performance gains are considerable, and the application has + /// been thoroughly tested against concurrency bugs. + /// + /// + /// Note that if the application is setting the internal service provider through a call to + /// , then this option must configured the same way + /// for all uses of that service provider. Consider instead not calling + /// so that EF will manage the service providers and can create new instances as required. + /// + /// + /// The same builder instance so that multiple calls can be chained. + public virtual DbContextOptionsBuilder DisableConcurrencyDetection(bool concurrencyDetectionDisabled = true) + => WithOption(e => e.WithConcurrencyDetectionEnabled(!concurrencyDetectionDisabled)); + /// /// /// Enables detailed errors when handling of data value exceptions that occur during processing of store query results. Such errors diff --git a/src/EFCore/DbContextOptionsBuilder`.cs b/src/EFCore/DbContextOptionsBuilder`.cs index 11d82d37ebe..8207f6d32d1 100644 --- a/src/EFCore/DbContextOptionsBuilder`.cs +++ b/src/EFCore/DbContextOptionsBuilder`.cs @@ -224,6 +224,28 @@ public DbContextOptionsBuilder([NotNull] DbContextOptions options) [NotNull] Action logger) => (DbContextOptionsBuilder)base.LogTo(filter, logger); + /// + /// + /// Disables concurrency detection, which detects many cases of erroneous concurrent usage of a + /// instance and causes an informative exception to be thrown. This provides a minor performance improvement, but if a + /// instance is used concurrently, the behavior will be undefined and the program may fail in + /// unpredictable ways. + /// + /// + /// Only disable concurrency detection after confirming that the performance gains are considerable, and the application has + /// been thoroughly tested against concurrency bugs. + /// + /// + /// Note that if the application is setting the internal service provider through a call to + /// , then this option must configured the same way + /// for all uses of that service provider. Consider instead not calling + /// so that EF will manage the service providers and can create new instances as required. + /// + /// + /// The same builder instance so that multiple calls can be chained. + public new virtual DbContextOptionsBuilder DisableConcurrencyDetection(bool concurrencyDetectionDisabled = true) + => (DbContextOptionsBuilder)base.DisableConcurrencyDetection(concurrencyDetectionDisabled); + /// /// /// Enables detailed errors when handling data value exceptions that occur during processing of store query results. Such errors diff --git a/src/EFCore/Infrastructure/CoreOptionsExtension.cs b/src/EFCore/Infrastructure/CoreOptionsExtension.cs index 562d1bbb19e..b81d77c476b 100644 --- a/src/EFCore/Infrastructure/CoreOptionsExtension.cs +++ b/src/EFCore/Infrastructure/CoreOptionsExtension.cs @@ -38,6 +38,7 @@ public class CoreOptionsExtension : IDbContextOptionsExtension private IMemoryCache? _memoryCache; private bool _sensitiveDataLoggingEnabled; private bool _detailedErrorsEnabled; + private bool _concurrencyDetectionEnabled = true; private QueryTrackingBehavior _queryTrackingBehavior = QueryTrackingBehavior.TrackAll; private IDictionary<(Type, Type?), Type>? _replacedServices; private int? _maxPoolSize; @@ -73,6 +74,7 @@ protected CoreOptionsExtension([NotNull] CoreOptionsExtension copyFrom) _memoryCache = copyFrom.MemoryCache; _sensitiveDataLoggingEnabled = copyFrom.IsSensitiveDataLoggingEnabled; _detailedErrorsEnabled = copyFrom.DetailedErrorsEnabled; + _concurrencyDetectionEnabled = copyFrom.ConcurrencyDetectionEnabled; _warningsConfiguration = copyFrom.WarningsConfiguration; _queryTrackingBehavior = copyFrom.QueryTrackingBehavior; _maxPoolSize = copyFrom.MaxPoolSize; @@ -218,6 +220,21 @@ public virtual CoreOptionsExtension WithDetailedErrorsEnabled(bool detailedError return clone; } + /// + /// Creates a new instance with all options the same as for this instance, but with the given option changed. + /// It is unusual to call this method directly. Instead use . + /// + /// The option to change. + /// A new instance with the option changed. + public virtual CoreOptionsExtension WithConcurrencyDetectionEnabled(bool concurrencyDetectionEnabled) + { + var clone = Clone(); + + clone._concurrencyDetectionEnabled = concurrencyDetectionEnabled; + + return clone; + } + /// /// Creates a new instance with all options the same as for this instance, but with the given option changed. /// It is unusual to call this method directly. Instead use . @@ -334,6 +351,12 @@ public virtual bool IsSensitiveDataLoggingEnabled public virtual bool DetailedErrorsEnabled => _detailedErrorsEnabled; + /// + /// The option set from the method. + /// + public virtual bool ConcurrencyDetectionEnabled + => _concurrencyDetectionEnabled; + /// /// The option set from the method. /// @@ -504,6 +527,11 @@ public override string LogFragment builder.Append("DetailedErrorsEnabled "); } + if (!Extension._concurrencyDetectionEnabled) + { + builder.Append("ConcurrencyDetectionDisabled "); + } + if (Extension._maxPoolSize != null) { builder.Append("MaxPoolSize=").Append(Extension._maxPoolSize).Append(' '); @@ -526,6 +554,8 @@ public override void PopulateDebugInfo(IDictionary debugInfo) Extension._sensitiveDataLoggingEnabled.GetHashCode().ToString(CultureInfo.InvariantCulture); debugInfo["Core:" + nameof(DbContextOptionsBuilder.EnableDetailedErrors)] = Extension._detailedErrorsEnabled.GetHashCode().ToString(CultureInfo.InvariantCulture); + debugInfo["Core:" + nameof(DbContextOptionsBuilder.DisableConcurrencyDetection)] = + (!Extension._concurrencyDetectionEnabled).GetHashCode().ToString(CultureInfo.InvariantCulture); debugInfo["Core:" + nameof(DbContextOptionsBuilder.ConfigureWarnings)] = Extension._warningsConfiguration.GetServiceProviderHashCode().ToString(CultureInfo.InvariantCulture); @@ -552,6 +582,7 @@ public override long GetServiceProviderHashCode() var hashCode = Extension.GetMemoryCache()?.GetHashCode() ?? 0L; hashCode = (hashCode * 3) ^ Extension._sensitiveDataLoggingEnabled.GetHashCode(); hashCode = (hashCode * 3) ^ Extension._detailedErrorsEnabled.GetHashCode(); + hashCode = (hashCode * 3) ^ Extension._concurrencyDetectionEnabled.GetHashCode(); hashCode = (hashCode * 1073742113) ^ Extension._warningsConfiguration.GetServiceProviderHashCode(); if (Extension._replacedServices != null) diff --git a/src/EFCore/Infrastructure/ICoreSingletonOptions.cs b/src/EFCore/Infrastructure/ICoreSingletonOptions.cs index a918875f3ee..b2dd6a20efa 100644 --- a/src/EFCore/Infrastructure/ICoreSingletonOptions.cs +++ b/src/EFCore/Infrastructure/ICoreSingletonOptions.cs @@ -24,5 +24,10 @@ public interface ICoreSingletonOptions : ISingletonOptions /// Reflects the option set by . /// bool AreDetailedErrorsEnabled { get; } + + /// + /// Reflects the option set by . + /// + bool IsConcurrencyDetectionEnabled { get; } } } diff --git a/src/EFCore/Infrastructure/Internal/CoreSingletonOptions.cs b/src/EFCore/Infrastructure/Internal/CoreSingletonOptions.cs index fe39f88a94d..a1526eaf53a 100644 --- a/src/EFCore/Infrastructure/Internal/CoreSingletonOptions.cs +++ b/src/EFCore/Infrastructure/Internal/CoreSingletonOptions.cs @@ -36,6 +36,7 @@ public virtual void Initialize(IDbContextOptions options) var coreOptions = options.FindExtension() ?? new CoreOptionsExtension(); AreDetailedErrorsEnabled = coreOptions.DetailedErrorsEnabled; + IsConcurrencyDetectionEnabled = coreOptions.ConcurrencyDetectionEnabled; } /// @@ -57,6 +58,16 @@ public virtual void Validate(IDbContextOptions options) nameof(DbContextOptionsBuilder.EnableDetailedErrors), nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); } + + if (IsConcurrencyDetectionEnabled != coreOptions.ConcurrencyDetectionEnabled) + { + Check.DebugAssert(coreOptions.InternalServiceProvider != null, "InternalServiceProvider is null"); + + throw new InvalidOperationException( + CoreStrings.SingletonOptionChanged( + nameof(DbContextOptionsBuilder.DisableConcurrencyDetection), + nameof(DbContextOptionsBuilder.UseInternalServiceProvider))); + } } /// @@ -66,5 +77,13 @@ public virtual void Validate(IDbContextOptions options) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual bool AreDetailedErrorsEnabled { get; private set; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual bool IsConcurrencyDetectionEnabled { get; private set; } } } diff --git a/src/EFCore/Query/ShapedQueryCompilingExpressionVisitorDependencies.cs b/src/EFCore/Query/ShapedQueryCompilingExpressionVisitorDependencies.cs index 1a862445d79..a55d2f8f907 100644 --- a/src/EFCore/Query/ShapedQueryCompilingExpressionVisitorDependencies.cs +++ b/src/EFCore/Query/ShapedQueryCompilingExpressionVisitorDependencies.cs @@ -60,15 +60,18 @@ public sealed record ShapedQueryCompilingExpressionVisitorDependencies public ShapedQueryCompilingExpressionVisitorDependencies( [NotNull] IEntityMaterializerSource entityMaterializerSource, [NotNull] ITypeMappingSource typeMappingSource, - [NotNull] IMemoryCache memoryCache) + [NotNull] IMemoryCache memoryCache, + [NotNull] ICoreSingletonOptions coreSingletonOptions) { Check.NotNull(entityMaterializerSource, nameof(entityMaterializerSource)); Check.NotNull(typeMappingSource, nameof(typeMappingSource)); Check.NotNull(memoryCache, nameof(memoryCache)); + Check.NotNull(coreSingletonOptions, nameof(coreSingletonOptions)); EntityMaterializerSource = entityMaterializerSource; TypeMappingSource = typeMappingSource; MemoryCache = memoryCache; + CoreSingletonOptions = coreSingletonOptions; } /// @@ -85,5 +88,10 @@ public ShapedQueryCompilingExpressionVisitorDependencies( /// The memory cache. /// public IMemoryCache MemoryCache { get; [param: NotNull] init; } + + /// + /// Core singleton options. + /// + public ICoreSingletonOptions CoreSingletonOptions { get; [param: NotNull] init; } } } diff --git a/src/EFCore/Storage/IDatabaseFacadeDependencies.cs b/src/EFCore/Storage/IDatabaseFacadeDependencies.cs index c5b2fb0e476..e466ad6d792 100644 --- a/src/EFCore/Storage/IDatabaseFacadeDependencies.cs +++ b/src/EFCore/Storage/IDatabaseFacadeDependencies.cs @@ -56,5 +56,10 @@ public interface IDatabaseFacadeDependencies /// The concurrency detector. /// IConcurrencyDetector ConcurrencyDetector { get; } + + /// + /// The core options. + /// + public ICoreSingletonOptions CoreOptions { get; } } } diff --git a/src/EFCore/Storage/Internal/DatabaseFacadeDependencies.cs b/src/EFCore/Storage/Internal/DatabaseFacadeDependencies.cs index d0f1f8f0103..ec06fdf1921 100644 --- a/src/EFCore/Storage/Internal/DatabaseFacadeDependencies.cs +++ b/src/EFCore/Storage/Internal/DatabaseFacadeDependencies.cs @@ -40,7 +40,8 @@ public DatabaseFacadeDependencies( [NotNull] IExecutionStrategyFactory executionStrategyFactory, [NotNull] IEnumerable databaseProviders, [NotNull] IDiagnosticsLogger commandLogger, - [NotNull] IConcurrencyDetector concurrencyDetector) + [NotNull] IConcurrencyDetector concurrencyDetector, + [NotNull] ICoreSingletonOptions coreOptions) { TransactionManager = transactionManager; DatabaseCreator = databaseCreator; @@ -48,6 +49,7 @@ public DatabaseFacadeDependencies( DatabaseProviders = databaseProviders; CommandLogger = commandLogger; ConcurrencyDetector = concurrencyDetector; + CoreOptions = coreOptions; } /// @@ -97,5 +99,13 @@ public DatabaseFacadeDependencies( /// doing so can result in application failures when updating to a new Entity Framework Core release. /// public virtual IConcurrencyDetector ConcurrencyDetector { get; [param: NotNull] init; } + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual ICoreSingletonOptions CoreOptions { get; [param: NotNull] init; } } } diff --git a/test/EFCore.Cosmos.FunctionalTests/ConcurrencyDetectorCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/ConcurrencyDetectorCosmosTest.cs deleted file mode 100644 index ca061a94738..00000000000 --- a/test/EFCore.Cosmos.FunctionalTests/ConcurrencyDetectorCosmosTest.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.TestUtilities; -using Xunit; - -namespace Microsoft.EntityFrameworkCore.Cosmos -{ - public class ConcurrencyDetectorCosmosTest : ConcurrencyDetectorTestBase> - { - public ConcurrencyDetectorCosmosTest(NorthwindQueryCosmosFixture fixture) - : base(fixture) - { - } - - [ConditionalFact(Skip = "Issue #17246")] - public override Task Any_logs_concurrent_access_nonasync() - { - return base.Any_logs_concurrent_access_nonasync(); - } - - [ConditionalFact(Skip = "Issue #17246")] - public override Task Any_logs_concurrent_access_async() - { - return base.Any_logs_concurrent_access_async(); - } - } -} diff --git a/test/EFCore.Cosmos.FunctionalTests/ConcurrencyDetectorDisabledCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/ConcurrencyDetectorDisabledCosmosTest.cs new file mode 100644 index 00000000000..1f5b9abbef1 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/ConcurrencyDetectorDisabledCosmosTest.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Cosmos +{ + public class ConcurrencyDetectorDisabledCosmosTest : ConcurrencyDetectorDisabledTestBase< + ConcurrencyDetectorDisabledCosmosTest.ConcurrencyDetectorCosmosFixture> + { + public ConcurrencyDetectorDisabledCosmosTest(ConcurrencyDetectorCosmosFixture fixture) + : base(fixture) + { + } + + [ConditionalTheory(Skip = "Issue #17246")] + public override Task Any(bool async) + => base.Any(async); + + public class ConcurrencyDetectorCosmosFixture : ConcurrencyDetectorFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => CosmosTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => builder.DisableConcurrencyDetection(); + } + } +} diff --git a/test/EFCore.Cosmos.FunctionalTests/ConcurrencyDetectorEnabledCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/ConcurrencyDetectorEnabledCosmosTest.cs new file mode 100644 index 00000000000..636a3a7ba10 --- /dev/null +++ b/test/EFCore.Cosmos.FunctionalTests/ConcurrencyDetectorEnabledCosmosTest.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Cosmos +{ + public class ConcurrencyDetectorEnabledCosmosTest : ConcurrencyDetectorEnabledTestBase< + ConcurrencyDetectorEnabledCosmosTest.ConcurrencyDetectorCosmosFixture> + { + public ConcurrencyDetectorEnabledCosmosTest(ConcurrencyDetectorCosmosFixture fixture) + : base(fixture) + { + } + + [ConditionalTheory(Skip = "Issue #17246")] + public override Task Any(bool async) + => base.Any(async); + + public class ConcurrencyDetectorCosmosFixture : ConcurrencyDetectorFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => CosmosTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + } + } +} diff --git a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs index 7b35b31d08e..234d7b704a3 100644 --- a/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs +++ b/test/EFCore.Cosmos.FunctionalTests/TestUtilities/CosmosTestStore.cs @@ -235,6 +235,11 @@ public override async Task DisposeAsync() if (_initialized && _dataFilePath == null) { + if (Shared) + { + GetTestStoreIndex(ServiceProvider).RemoveShared(GetType().Name + Name); + } + await _storeContext.Database.EnsureDeletedAsync(); } diff --git a/test/EFCore.InMemory.FunctionalTests/ConcurrencyDetectorDisabledInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/ConcurrencyDetectorDisabledInMemoryTest.cs new file mode 100644 index 00000000000..6cb77d257d7 --- /dev/null +++ b/test/EFCore.InMemory.FunctionalTests/ConcurrencyDetectorDisabledInMemoryTest.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public class ConcurrencyDetectorDisabledInMemoryTest : ConcurrencyDetectorDisabledTestBase< + ConcurrencyDetectorDisabledInMemoryTest.ConcurrencyDetectorInMemoryFixture> + { + public ConcurrencyDetectorDisabledInMemoryTest(ConcurrencyDetectorInMemoryFixture fixture) + : base(fixture) + { + } + + public class ConcurrencyDetectorInMemoryFixture : ConcurrencyDetectorFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => InMemoryTestStoreFactory.Instance; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => builder.DisableConcurrencyDetection(); + } + } +} diff --git a/test/EFCore.InMemory.FunctionalTests/ConcurrencyDetectorEnabledInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/ConcurrencyDetectorEnabledInMemoryTest.cs new file mode 100644 index 00000000000..14147bdf937 --- /dev/null +++ b/test/EFCore.InMemory.FunctionalTests/ConcurrencyDetectorEnabledInMemoryTest.cs @@ -0,0 +1,23 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public class ConcurrencyDetectorEnabledInMemoryTest : ConcurrencyDetectorEnabledTestBase< + ConcurrencyDetectorEnabledInMemoryTest.ConcurrencyDetectorInMemoryFixture> + { + public ConcurrencyDetectorEnabledInMemoryTest(ConcurrencyDetectorInMemoryFixture fixture) + : base(fixture) + { + } + + public class ConcurrencyDetectorInMemoryFixture : ConcurrencyDetectorFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => InMemoryTestStoreFactory.Instance; + } + } +} diff --git a/test/EFCore.InMemory.FunctionalTests/ConcurrencyDetectorInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/ConcurrencyDetectorInMemoryTest.cs deleted file mode 100644 index f7d00c7236d..00000000000 --- a/test/EFCore.InMemory.FunctionalTests/ConcurrencyDetectorInMemoryTest.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.TestUtilities; - -namespace Microsoft.EntityFrameworkCore -{ - public class ConcurrencyDetectorInMemoryTest : ConcurrencyDetectorTestBase> - { - public ConcurrencyDetectorInMemoryTest(NorthwindQueryInMemoryFixture fixture) - : base(fixture) - { - } - } -} diff --git a/test/EFCore.Relational.Specification.Tests/ConcurrencyDetectorDisabledRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/ConcurrencyDetectorDisabledRelationalTestBase.cs new file mode 100644 index 00000000000..309bbde6f9e --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/ConcurrencyDetectorDisabledRelationalTestBase.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +// ReSharper disable InconsistentNaming +namespace Microsoft.EntityFrameworkCore +{ + public abstract class ConcurrencyDetectorDisabledRelationalTestBase : ConcurrencyDetectorDisabledTestBase + where TFixture : ConcurrencyDetectorTestBase.ConcurrencyDetectorFixtureBase, new() + { + protected ConcurrencyDetectorDisabledRelationalTestBase(TFixture fixture) + : base(fixture) + { + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task FromSql(bool async) + => ConcurrencyDetectorTest(async c => async + ? await c.Products.FromSqlRaw("select * from products").ToListAsync() + : c.Products.FromSqlRaw("select * from products").ToList()); + } +} diff --git a/test/EFCore.Relational.Specification.Tests/ConcurrencyDetectorEnabledRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/ConcurrencyDetectorEnabledRelationalTestBase.cs new file mode 100644 index 00000000000..ac542130c78 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/ConcurrencyDetectorEnabledRelationalTestBase.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +// ReSharper disable InconsistentNaming +namespace Microsoft.EntityFrameworkCore +{ + public abstract class ConcurrencyDetectorEnabledRelationalTestBase : ConcurrencyDetectorEnabledTestBase + where TFixture : ConcurrencyDetectorTestBase.ConcurrencyDetectorFixtureBase, new() + { + protected ConcurrencyDetectorEnabledRelationalTestBase(TFixture fixture) + : base(fixture) + { + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task FromSql(bool async) + => ConcurrencyDetectorTest(async c => async + ? await c.Products.FromSqlRaw("select * from products").ToListAsync() + : c.Products.FromSqlRaw("select * from products").ToList()); + } +} diff --git a/test/EFCore.Relational.Specification.Tests/ConcurrencyDetectorRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/ConcurrencyDetectorRelationalTestBase.cs deleted file mode 100644 index dbf381b1e2b..00000000000 --- a/test/EFCore.Relational.Specification.Tests/ConcurrencyDetectorRelationalTestBase.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Linq; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.TestUtilities; -using Xunit; - -// ReSharper disable InconsistentNaming -namespace Microsoft.EntityFrameworkCore -{ - public abstract class ConcurrencyDetectorRelationalTestBase : ConcurrencyDetectorTestBase - where TFixture : NorthwindQueryRelationalFixture, new() - { - protected ConcurrencyDetectorRelationalTestBase(TFixture fixture) - : base(fixture) - { - } - - [ConditionalFact] - public virtual Task FromSql_logs_concurrent_access_nonasync() - { - return ConcurrencyDetectorTest( - c => - { - // ReSharper disable once UnusedVariable - var result = c.Products.FromSqlRaw("select * from products").ToList(); - return Task.FromResult(false); - }); - } - - [ConditionalFact] - public virtual Task FromSql_logs_concurrent_access_async() - { - return ConcurrencyDetectorTest(c => c.Products.FromSqlRaw("select * from products").ToListAsync()); - } - } -} diff --git a/test/EFCore.Specification.Tests/ConcurrencyDetectorDisabledTestBase.cs b/test/EFCore.Specification.Tests/ConcurrencyDetectorDisabledTestBase.cs new file mode 100644 index 00000000000..79b4a0536af --- /dev/null +++ b/test/EFCore.Specification.Tests/ConcurrencyDetectorDisabledTestBase.cs @@ -0,0 +1,54 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Xunit; + +// ReSharper disable UnusedVariable +// ReSharper disable InconsistentNaming +namespace Microsoft.EntityFrameworkCore +{ + public abstract class ConcurrencyDetectorDisabledTestBase : ConcurrencyDetectorTestBase + where TFixture : ConcurrencyDetectorTestBase.ConcurrencyDetectorFixtureBase, new() + { + protected ConcurrencyDetectorDisabledTestBase(TFixture fixture) + : base(fixture) + { + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SaveChanges(bool async) + { + await ConcurrencyDetectorTest( + async c => + { + c.Products.Add(new Product { Id = 3, Name = "Unicorn Horseshoe Protection Pack" }); + return async ? await c.SaveChangesAsync() : c.SaveChanges(); + }); + + using var ctx = CreateContext(); + var newProduct = await ctx.Products.FindAsync(3); + Assert.NotNull(newProduct); + ctx.Products.Remove(newProduct); + await ctx.SaveChangesAsync(); + } + + protected override async Task ConcurrencyDetectorTest(Func> test) + { + using var context = CreateContext(); + + var concurrencyDetector = context.GetService(); + IDisposable disposer = null; + + await Task.Run(() => disposer = concurrencyDetector.EnterCriticalSection()); + + using (disposer) + { + await test(context); + } + } + } +} diff --git a/test/EFCore.Specification.Tests/ConcurrencyDetectorEnabledTestBase.cs b/test/EFCore.Specification.Tests/ConcurrencyDetectorEnabledTestBase.cs new file mode 100644 index 00000000000..3b0b79a7a44 --- /dev/null +++ b/test/EFCore.Specification.Tests/ConcurrencyDetectorEnabledTestBase.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Xunit; + +// ReSharper disable UnusedVariable +// ReSharper disable InconsistentNaming +namespace Microsoft.EntityFrameworkCore +{ + public abstract class ConcurrencyDetectorEnabledTestBase : ConcurrencyDetectorTestBase + where TFixture : ConcurrencyDetectorTestBase.ConcurrencyDetectorFixtureBase, new() + { + protected ConcurrencyDetectorEnabledTestBase(TFixture fixture) + : base(fixture) + { + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task SaveChanges(bool async) + { + await ConcurrencyDetectorTest( + async c => + { + c.Products.Add(new Product { Id = 2, Name = "Unicorn Replacement Horn Pack" }); + return async ? await c.SaveChangesAsync() : c.SaveChanges(); + }); + + using var ctx = CreateContext(); + var newProduct = await ctx.Products.SingleOrDefaultAsync(p => p.Id == 2); + Assert.Null(newProduct); + } + + protected override async Task ConcurrencyDetectorTest(Func> test) + { + using var context = CreateContext(); + + var concurrencyDetector = context.GetService(); + IDisposable disposer = null; + + await Task.Run(() => disposer = concurrencyDetector.EnterCriticalSection()); + + using (disposer) + { + Exception ex = await Assert.ThrowsAsync(() => test(context)); + + Assert.Equal(CoreStrings.ConcurrentMethodInvocation, ex.Message); + } + } + } +} diff --git a/test/EFCore.Specification.Tests/ConcurrencyDetectorTestBase.cs b/test/EFCore.Specification.Tests/ConcurrencyDetectorTestBase.cs index 7526d439560..543224e34d7 100644 --- a/test/EFCore.Specification.Tests/ConcurrencyDetectorTestBase.cs +++ b/test/EFCore.Specification.Tests/ConcurrencyDetectorTestBase.cs @@ -2,183 +2,112 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.Diagnostics; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.TestModels.Northwind; -using Microsoft.EntityFrameworkCore.TestUtilities; using Xunit; +#pragma warning disable CA1720 +#pragma warning disable CA1716 + // ReSharper disable UnusedVariable // ReSharper disable InconsistentNaming +// ReSharper disable MethodHasAsyncOverload namespace Microsoft.EntityFrameworkCore { public abstract class ConcurrencyDetectorTestBase : IClassFixture - where TFixture : NorthwindQueryFixtureBase, new() + where TFixture : ConcurrencyDetectorTestBase.ConcurrencyDetectorFixtureBase, new() { protected ConcurrencyDetectorTestBase(TFixture fixture) => Fixture = fixture; protected TFixture Fixture { get; } - [ConditionalFact] - public virtual Task SaveChanges_logs_concurrent_access_nonasync() - { - return ConcurrencyDetectorTest( - c => - { - c.SaveChanges(); - return Task.FromResult(false); - }); - } - - [ConditionalFact] - public virtual Task SaveChanges_logs_concurrent_access_async() - { - return ConcurrencyDetectorTest(c => c.SaveChangesAsync()); - } - - [ConditionalFact] - public virtual Task Find_logs_concurrent_access_nonasync() - { - return ConcurrencyDetectorTest( - c => - { - c.Products.Find(1); - return Task.FromResult(false); - }); - } - - [ConditionalFact] - public virtual Task Find_logs_concurrent_access_async() - { - return ConcurrencyDetectorTest(c => c.Products.FindAsync(1).AsTask()); - } - - [ConditionalFact] - public virtual Task Count_logs_concurrent_access_nonasync() - { - return ConcurrencyDetectorTest( - c => - { - var result = c.Products.Count(); - return Task.FromResult(false); - }); - } - - [ConditionalFact] - public virtual Task Count_logs_concurrent_access_async() - { - return ConcurrencyDetectorTest(c => c.Products.CountAsync()); - } - - [ConditionalFact] - public virtual Task First_logs_concurrent_access_nonasync() - { - return ConcurrencyDetectorTest( - c => - { - var result = c.Products.First(); - return Task.FromResult(false); - }); - } - - [ConditionalFact] - public virtual Task First_logs_concurrent_access_async() - { - return ConcurrencyDetectorTest(c => c.Products.FirstAsync()); - } - - [ConditionalFact] - public virtual Task Last_logs_concurrent_access_nonasync() - { - return ConcurrencyDetectorTest( - c => - { - var result = c.Products.OrderBy(p => p.ProductID).Last(); - return Task.FromResult(false); - }); - } - - [ConditionalFact] - public virtual Task Last_logs_concurrent_access_async() - { - return ConcurrencyDetectorTest(c => c.Products.OrderBy(p => p.ProductID).LastAsync()); - } - - [ConditionalFact] - public virtual Task Single_logs_concurrent_access_nonasync() - { - return ConcurrencyDetectorTest( - c => - { - var result = c.Products.Single(p => p.ProductID == 1); - return Task.FromResult(false); - }); - } - - [ConditionalFact] - public virtual Task Single_logs_concurrent_access_async() - { - return ConcurrencyDetectorTest(c => c.Products.SingleAsync(p => p.ProductID == 1)); - } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Find(bool async) + => ConcurrencyDetectorTest(async c => async ? await c.Products.FindAsync(1) : c.Products.Find(1)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Count(bool async) + => ConcurrencyDetectorTest(async c => async ? await c.Products.CountAsync() : c.Products.Count()); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task First(bool async) + => ConcurrencyDetectorTest( + async c => async + ? await c.Products.OrderBy(p => p.Id).FirstAsync() + : c.Products.OrderBy(p => p.Id).First()); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Last(bool async) + => ConcurrencyDetectorTest( + async c => async + ? await c.Products.OrderBy(p => p.Id).LastAsync() + : c.Products.OrderBy(p => p.Id).Last()); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Single(bool async) + => ConcurrencyDetectorTest( + async c => async + ? await c.Products.SingleAsync(p => p.Id == 1) + : c.Products.Single(p => p.Id == 1)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Any(bool async) + => ConcurrencyDetectorTest( + async c => async + ? await c.Products.AnyAsync(p => p.Id < 10) + : c.Products.Any(p => p.Id < 10)); + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task ToList(bool async) + => ConcurrencyDetectorTest(async c => async ? await c.Products.ToListAsync() : c.Products.ToList()); + + protected abstract Task ConcurrencyDetectorTest(Func> test); + + protected ConcurrencyDetectorDbContext CreateContext() + => Fixture.CreateContext(); - [ConditionalFact] - public virtual Task Any_logs_concurrent_access_nonasync() + public class ConcurrencyDetectorDbContext : DbContext { - return ConcurrencyDetectorTest( - c => - { - var result = c.Products.Any(p => p.ProductID < 10); - return Task.FromResult(false); - }); - } + public ConcurrencyDetectorDbContext(DbContextOptions options) + : base(options) + { + } - [ConditionalFact] - public virtual Task Any_logs_concurrent_access_async() - { - return ConcurrencyDetectorTest(c => c.Products.AnyAsync(p => p.ProductID < 10)); - } + public DbSet Products { get; set; } - [ConditionalFact] - public virtual Task ToList_logs_concurrent_access_nonasync() - { - return ConcurrencyDetectorTest( - c => - { - var result = c.Products.ToList(); - return Task.FromResult(false); - }); + public static void Seed(ConcurrencyDetectorDbContext context) + { + context.Products.Add(new() { Id = 1, Name = "Unicorn Party Pack"}); + context.SaveChanges(); + } } - [ConditionalFact] - public virtual Task ToList_logs_concurrent_access_async() + public class Product { - return ConcurrencyDetectorTest(c => c.Products.ToListAsync()); + public int Id { get; set; } + public string Name { get; set; } } - protected virtual async Task ConcurrencyDetectorTest(Func test) + public abstract class ConcurrencyDetectorFixtureBase : SharedStoreFixtureBase { - using var context = CreateContext(); - context.Products.Add( - new Product { ProductID = 10001 }); - - var concurrencyDetector = context.GetService(); - IDisposable disposer = null; - - Task.Run(() => disposer = concurrencyDetector.EnterCriticalSection()).Wait(); + protected override string StoreName => "ConcurrencyDetector"; - using (disposer) - { - Exception ex = await Assert.ThrowsAsync(() => test(context)); + protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context) + => modelBuilder.Entity().Property(p => p.Id).ValueGeneratedNever(); - Assert.Equal(CoreStrings.ConcurrentMethodInvocation, ex.Message); - } + protected override void Seed(ConcurrencyDetectorDbContext context) + => ConcurrencyDetectorDbContext.Seed(context); } - protected NorthwindContext CreateContext() - => Fixture.CreateContext(); + public static IEnumerable IsAsyncData = new[] { new object[] { false }, new object[] { true } }; } } diff --git a/test/EFCore.Specification.Tests/TestUtilities/TestStoreIndex.cs b/test/EFCore.Specification.Tests/TestUtilities/TestStoreIndex.cs index f6c9200ff62..2d734171f5b 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/TestStoreIndex.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/TestStoreIndex.cs @@ -38,6 +38,9 @@ public virtual void CreateShared(string name, Action initializeDatabase) } } + public virtual void RemoveShared(string name) + => _createdDatabases.Remove(name); + public virtual void CreateNonShared(string name, Action initializeDatabase) { var creationLock = _creationLocks.GetOrAdd(name, new object()); diff --git a/test/EFCore.SqlServer.FunctionalTests/ConcurrencyDetectorDisabledSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ConcurrencyDetectorDisabledSqlServerTest.cs new file mode 100644 index 00000000000..cc06abd3060 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/ConcurrencyDetectorDisabledSqlServerTest.cs @@ -0,0 +1,41 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.TestModels.Northwind; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public class ConcurrencyDetectorDisabledSqlServerTest : ConcurrencyDetectorDisabledRelationalTestBase< + ConcurrencyDetectorDisabledSqlServerTest.ConcurrencyDetectorSqlServerFixture> + { + public ConcurrencyDetectorDisabledSqlServerTest(ConcurrencyDetectorSqlServerFixture fixture) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + } + + protected override async Task ConcurrencyDetectorTest(Func> test) + { + await base.ConcurrencyDetectorTest(test); + + Assert.NotEmpty(Fixture.TestSqlLoggerFactory.SqlStatements); + } + + public class ConcurrencyDetectorSqlServerFixture : ConcurrencyDetectorFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => builder.DisableConcurrencyDetection(); + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/ConcurrencyDetectorEnabledSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ConcurrencyDetectorEnabledSqlServerTest.cs new file mode 100644 index 00000000000..088a58991bd --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/ConcurrencyDetectorEnabledSqlServerTest.cs @@ -0,0 +1,36 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public class ConcurrencyDetectorEnabledSqlServerTest : ConcurrencyDetectorEnabledRelationalTestBase< + ConcurrencyDetectorEnabledSqlServerTest.ConcurrencyDetectorSqlServerFixture> + { + public ConcurrencyDetectorEnabledSqlServerTest(ConcurrencyDetectorSqlServerFixture fixture) + : base(fixture) + { + Fixture.TestSqlLoggerFactory.Clear(); + } + + protected override async Task ConcurrencyDetectorTest(Func> test) + { + await base.ConcurrencyDetectorTest(test); + + Assert.Empty(Fixture.TestSqlLoggerFactory.SqlStatements); + } + + public class ConcurrencyDetectorSqlServerFixture : ConcurrencyDetectorFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => SqlServerTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/ConcurrencyDetectorSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ConcurrencyDetectorSqlServerTest.cs deleted file mode 100644 index be9e761e64b..00000000000 --- a/test/EFCore.SqlServer.FunctionalTests/ConcurrencyDetectorSqlServerTest.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.TestModels.Northwind; -using Microsoft.EntityFrameworkCore.TestUtilities; -using Xunit; - -namespace Microsoft.EntityFrameworkCore -{ - public class ConcurrencyDetectorSqlServerTest : ConcurrencyDetectorRelationalTestBase< - NorthwindQuerySqlServerFixture> - { - public ConcurrencyDetectorSqlServerTest(NorthwindQuerySqlServerFixture fixture) - : base(fixture) - { - } - - protected override async Task ConcurrencyDetectorTest(Func test) - { - await base.ConcurrencyDetectorTest(test); - - Assert.Empty(Fixture.TestSqlLoggerFactory.SqlStatements); - } - } -} diff --git a/test/EFCore.Sqlite.FunctionalTests/ConcurrencyDetectorDisabledSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/ConcurrencyDetectorDisabledSqliteTest.cs new file mode 100644 index 00000000000..9324a469c77 --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/ConcurrencyDetectorDisabledSqliteTest.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public class ConcurrencyDetectorDisabledSqliteTest : ConcurrencyDetectorDisabledRelationalTestBase< + ConcurrencyDetectorDisabledSqliteTest.ConcurrencyDetectorSqlServerFixture> + { + public ConcurrencyDetectorDisabledSqliteTest(ConcurrencyDetectorSqlServerFixture fixture) + : base(fixture) + { + } + + public class ConcurrencyDetectorSqlServerFixture : ConcurrencyDetectorFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => SqliteTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + + public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder) + => builder.DisableConcurrencyDetection(); + } + } +} diff --git a/test/EFCore.Sqlite.FunctionalTests/ConcurrencyDetectorEnabledSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/ConcurrencyDetectorEnabledSqliteTest.cs new file mode 100644 index 00000000000..137a349e288 --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/ConcurrencyDetectorEnabledSqliteTest.cs @@ -0,0 +1,26 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.EntityFrameworkCore.TestUtilities; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public class ConcurrencyDetectorEnabledSqliteTest : ConcurrencyDetectorEnabledRelationalTestBase< + ConcurrencyDetectorEnabledSqliteTest.ConcurrencyDetectorSqlServerFixture> + { + public ConcurrencyDetectorEnabledSqliteTest(ConcurrencyDetectorSqlServerFixture fixture) + : base(fixture) + { + } + + public class ConcurrencyDetectorSqlServerFixture : ConcurrencyDetectorFixtureBase + { + protected override ITestStoreFactory TestStoreFactory + => SqliteTestStoreFactory.Instance; + + public TestSqlLoggerFactory TestSqlLoggerFactory + => (TestSqlLoggerFactory)ListLoggerFactory; + } + } +} diff --git a/test/EFCore.Sqlite.FunctionalTests/ConcurrencyDetectorSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/ConcurrencyDetectorSqliteTest.cs deleted file mode 100644 index c974edbb597..00000000000 --- a/test/EFCore.Sqlite.FunctionalTests/ConcurrencyDetectorSqliteTest.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.EntityFrameworkCore.Query; -using Microsoft.EntityFrameworkCore.TestUtilities; - -namespace Microsoft.EntityFrameworkCore -{ - public class ConcurrencyDetectorSqliteTest : ConcurrencyDetectorRelationalTestBase> - { - public ConcurrencyDetectorSqliteTest(NorthwindQuerySqliteFixture fixture) - : base(fixture) - { - } - } -}