diff --git a/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs b/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs index 78267278c5f..f08ce470b5d 100644 --- a/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs +++ b/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs @@ -6,6 +6,7 @@ using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Design.Internal; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; @@ -56,7 +57,6 @@ public static IServiceCollection AddEntityFrameworkDesignTimeServices( .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/src/EFCore.InMemory/Storage/Internal/InMemoryTransaction.cs b/src/EFCore.InMemory/Storage/Internal/InMemoryTransaction.cs index eaee70beac8..93d1bb71790 100644 --- a/src/EFCore.InMemory/Storage/Internal/InMemoryTransaction.cs +++ b/src/EFCore.InMemory/Storage/Internal/InMemoryTransaction.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Storage; namespace Microsoft.EntityFrameworkCore.InMemory.Storage.Internal @@ -42,6 +44,24 @@ public virtual void Rollback() { } + /// + /// 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 Task CommitAsync(CancellationToken cancellationToken = default) + => Task.CompletedTask; + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// 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 Task RollbackAsync(CancellationToken cancellationToken = default) + => Task.CompletedTask; + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/src/EFCore.Relational/Diagnostics/CompositeDbCommandInterceptor.cs b/src/EFCore.Relational/Diagnostics/CompositeDbCommandInterceptor.cs deleted file mode 100644 index ff95a8890bc..00000000000 --- a/src/EFCore.Relational/Diagnostics/CompositeDbCommandInterceptor.cs +++ /dev/null @@ -1,350 +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.Collections.Generic; -using System.Data.Common; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Utilities; - -namespace Microsoft.EntityFrameworkCore.Diagnostics -{ - /// - /// An that chains multiple - /// together, calling one after the other in a chain. - /// - public class CompositeDbCommandInterceptor : IDbCommandInterceptor - { - private readonly IDbCommandInterceptor[] _interceptors; - - /// - /// - /// Initializes a new instance of the class, - /// creating a new composed from other - /// instances. - /// - /// - /// The result from each interceptor method is passed as the 'result' parameter to the same method - /// on the next interceptor in the chain. - /// - /// - /// The interceptors from which to create composite chain. - public CompositeDbCommandInterceptor([NotNull] params IDbCommandInterceptor[] interceptors) - { - Check.NotNull(interceptors, nameof(interceptors)); - - _interceptors = interceptors; - } - - /// - /// - /// Initializes a new instance of the class, - /// creating a new composed from other - /// instances. - /// - /// - /// The result from each interceptor method is passed as the 'result' parameter to the same method - /// on the next interceptor in the chain. - /// - /// - /// The interceptors from which to create composite chain. - public CompositeDbCommandInterceptor([NotNull] IEnumerable interceptors) - : this(Check.NotNull(interceptors, nameof(interceptors)).ToArray()) - { - } - - /// - /// Calls for all interceptors in the chain, passing - /// the result from one as the parameter for the next. - /// - /// The command. - /// Contextual information about the command and execution. - /// The current result, or null if no result yet exists. - /// The result returned from the last interceptor in the chain. - public virtual InterceptionResult? ReaderExecuting( - DbCommand command, - CommandEventData eventData, - InterceptionResult? result) - { - for (var i = 0; i < _interceptors.Length; i++) - { - result = _interceptors[i].ReaderExecuting(command, eventData, result); - } - - return result; - } - - /// - /// Calls for all interceptors in the chain, passing - /// the result from one as the parameter for the next. - /// - /// The command. - /// Contextual information about the command and execution. - /// The current result, or null if no result yet exists. - /// The result returned from the last interceptor in the chain. - public virtual InterceptionResult? ScalarExecuting( - DbCommand command, - CommandEventData eventData, - InterceptionResult? result) - { - for (var i = 0; i < _interceptors.Length; i++) - { - result = _interceptors[i].ScalarExecuting(command, eventData, result); - } - - return result; - } - - /// - /// Calls for all interceptors in the chain, passing - /// the result from one as the parameter for the next. - /// - /// The command. - /// Contextual information about the command and execution. - /// The current result, or null if no result yet exists. - /// The result returned from the last interceptor in the chain. - public virtual InterceptionResult? NonQueryExecuting( - DbCommand command, - CommandEventData eventData, - InterceptionResult? result) - { - for (var i = 0; i < _interceptors.Length; i++) - { - result = _interceptors[i].NonQueryExecuting(command, eventData, result); - } - - return result; - } - - /// - /// Calls for all interceptors in the chain, passing - /// the result from one as the parameter for the next. - /// - /// The command. - /// Contextual information about the command and execution. - /// The current result, or null if no result yet exists. - /// The cancellation token. - /// The result returned from the last interceptor in the chain. - public virtual async Task?> ReaderExecutingAsync( - DbCommand command, - CommandEventData eventData, - InterceptionResult? result, - CancellationToken cancellationToken = default) - { - for (var i = 0; i < _interceptors.Length; i++) - { - result = await _interceptors[i].ReaderExecutingAsync(command, eventData, result, cancellationToken); - } - - return result; - } - - /// - /// Calls for all interceptors in the chain, passing - /// the result from one as the parameter for the next. - /// - /// The command. - /// Contextual information about the command and execution. - /// The current result, or null if no result yet exists. - /// The cancellation token. - /// The result returned from the last interceptor in the chain. - public virtual async Task?> ScalarExecutingAsync( - DbCommand command, - CommandEventData eventData, - InterceptionResult? result, - CancellationToken cancellationToken = default) - { - for (var i = 0; i < _interceptors.Length; i++) - { - result = await _interceptors[i].ScalarExecutingAsync(command, eventData, result, cancellationToken); - } - - return result; - } - - /// - /// Calls for all interceptors in the chain, passing - /// the result from one as the parameter for the next. - /// - /// The command. - /// Contextual information about the command and execution. - /// The current result, or null if no result yet exists. - /// The cancellation token. - /// The result returned from the last interceptor in the chain. - public virtual async Task?> NonQueryExecutingAsync( - DbCommand command, - CommandEventData eventData, - InterceptionResult? result, - CancellationToken cancellationToken = default) - { - for (var i = 0; i < _interceptors.Length; i++) - { - result = await _interceptors[i].NonQueryExecutingAsync(command, eventData, result, cancellationToken); - } - - return result; - } - - /// - /// Calls for all interceptors in the chain, passing - /// the result from one as the parameter for the next. - /// - /// The command. - /// Contextual information about the command and execution. - /// The current result, or null if no result yet exists. - /// The result returned from the last interceptor in the chain. - public virtual DbDataReader ReaderExecuted( - DbCommand command, - CommandExecutedEventData eventData, - DbDataReader result) - { - for (var i = 0; i < _interceptors.Length; i++) - { - result = _interceptors[i].ReaderExecuted(command, eventData, result); - } - - return result; - } - - /// - /// Calls for all interceptors in the chain, passing - /// the result from one as the parameter for the next. - /// - /// The command. - /// Contextual information about the command and execution. - /// The current result, or null if no result yet exists. - /// The result returned from the last interceptor in the chain. - public virtual object ScalarExecuted( - DbCommand command, - CommandExecutedEventData eventData, - object result) - { - for (var i = 0; i < _interceptors.Length; i++) - { - result = _interceptors[i].ScalarExecuted(command, eventData, result); - } - - return result; - } - - /// - /// Calls for all interceptors in the chain, passing - /// the result from one as the parameter for the next. - /// - /// The command. - /// Contextual information about the command and execution. - /// The current result, or null if no result yet exists. - /// The result returned from the last interceptor in the chain. - public virtual int NonQueryExecuted( - DbCommand command, - CommandExecutedEventData eventData, - int result) - { - for (var i = 0; i < _interceptors.Length; i++) - { - result = _interceptors[i].NonQueryExecuted(command, eventData, result); - } - - return result; - } - - /// - /// Calls for all interceptors in the chain, passing - /// the result from one as the parameter for the next. - /// - /// The command. - /// Contextual information about the command and execution. - /// The current result, or null if no result yet exists. - /// The cancellation token. - /// The result returned from the last interceptor in the chain. - public virtual async Task ReaderExecutedAsync( - DbCommand command, - CommandExecutedEventData eventData, - DbDataReader result, - CancellationToken cancellationToken = default) - { - for (var i = 0; i < _interceptors.Length; i++) - { - result = await _interceptors[i].ReaderExecutedAsync(command, eventData, result, cancellationToken); - } - - return result; - } - - /// - /// Calls for all interceptors in the chain, passing - /// the result from one as the parameter for the next. - /// - /// The command. - /// Contextual information about the command and execution. - /// The current result, or null if no result yet exists. - /// The cancellation token. - /// The result returned from the last interceptor in the chain. - public virtual async Task ScalarExecutedAsync( - DbCommand command, - CommandExecutedEventData eventData, - object result, - CancellationToken cancellationToken = default) - { - for (var i = 0; i < _interceptors.Length; i++) - { - result = await _interceptors[i].ScalarExecutedAsync(command, eventData, result, cancellationToken); - } - - return result; - } - - /// - /// Calls for all interceptors in the chain, passing - /// the result from one as the parameter for the next. - /// - /// The command. - /// Contextual information about the command and execution. - /// The current result, or null if no result yet exists. - /// The cancellation token. - /// The result returned from the last interceptor in the chain. - public virtual async Task NonQueryExecutedAsync( - DbCommand command, - CommandExecutedEventData eventData, - int result, - CancellationToken cancellationToken = default) - { - for (var i = 0; i < _interceptors.Length; i++) - { - result = await _interceptors[i].NonQueryExecutedAsync(command, eventData, result, cancellationToken); - } - - return result; - } - - /// - /// Called when execution of a command has failed with an exception. />. - /// - /// The command. - /// Contextual information about the command and execution. - public virtual void CommandFailed(DbCommand command, CommandErrorEventData eventData) - { - for (var i = 0; i < _interceptors.Length; i++) - { - _interceptors[i].CommandFailed(command, eventData); - } - } - - /// - /// Called when execution of a command has failed with an exception. />. - /// - /// The command. - /// Contextual information about the command and execution. - /// The cancellation token. - /// A representing the asynchronous operation. - public virtual async Task CommandFailedAsync(DbCommand command, CommandErrorEventData eventData, CancellationToken cancellationToken = default) - { - for (var i = 0; i < _interceptors.Length; i++) - { - await _interceptors[i].CommandFailedAsync(command, eventData, cancellationToken); - } - } - } -} diff --git a/src/EFCore.Relational/Diagnostics/ConnectionEndEventData.cs b/src/EFCore.Relational/Diagnostics/ConnectionEndEventData.cs index 954865b6135..e43f3d28584 100644 --- a/src/EFCore.Relational/Diagnostics/ConnectionEndEventData.cs +++ b/src/EFCore.Relational/Diagnostics/ConnectionEndEventData.cs @@ -19,30 +19,22 @@ public class ConnectionEndEventData : ConnectionEventData /// /// The event definition. /// A delegate that generates a log message for this event. - /// - /// The . - /// - /// - /// A correlation ID that identifies the instance being used. - /// - /// - /// Indicates whether or not the operation is happening asynchronously. - /// - /// - /// The start time of this event. - /// - /// - /// The duration this event. - /// + /// The . + /// The currently being used, to null if not known. + /// A correlation ID that identifies the instance being used. + /// Indicates whether or not the operation is happening asynchronously. + /// The start time of this event. + /// The duration this event. public ConnectionEndEventData( [NotNull] EventDefinitionBase eventDefinition, [NotNull] Func messageGenerator, [NotNull] DbConnection connection, + [CanBeNull] DbContext context, Guid connectionId, bool async, DateTimeOffset startTime, TimeSpan duration) - : base(eventDefinition, messageGenerator, connection, connectionId, async, startTime) + : base(eventDefinition, messageGenerator, connection, context, connectionId, async, startTime) => Duration = duration; /// diff --git a/src/EFCore.Relational/Diagnostics/ConnectionErrorEventData.cs b/src/EFCore.Relational/Diagnostics/ConnectionErrorEventData.cs index 99aeec7e877..c19fd034454 100644 --- a/src/EFCore.Relational/Diagnostics/ConnectionErrorEventData.cs +++ b/src/EFCore.Relational/Diagnostics/ConnectionErrorEventData.cs @@ -18,34 +18,24 @@ public class ConnectionErrorEventData : ConnectionEndEventData, IErrorEventData /// /// The event definition. /// A delegate that generates a log message for this event. - /// - /// The . - /// - /// - /// A correlation ID that identifies the instance being used. - /// - /// - /// The exception that was thrown when the connection failed. - /// - /// - /// Indicates whether or not the operation is happening asynchronously. - /// - /// - /// The start time of this event. - /// - /// - /// The duration this event. - /// + /// The . + /// The currently being used, to null if not known. + /// A correlation ID that identifies the instance being used. + /// The exception that was thrown when the connection failed. + /// Indicates whether or not the operation is happening asynchronously. + /// The start time of this event. + /// The duration this event. public ConnectionErrorEventData( [NotNull] EventDefinitionBase eventDefinition, [NotNull] Func messageGenerator, [NotNull] DbConnection connection, + [CanBeNull] DbContext context, Guid connectionId, [NotNull] Exception exception, bool async, DateTimeOffset startTime, TimeSpan duration) - : base(eventDefinition, messageGenerator, connection, connectionId, async, startTime, duration) + : base(eventDefinition, messageGenerator, connection, context, connectionId, async, startTime, duration) => Exception = exception; /// diff --git a/src/EFCore.Relational/Diagnostics/ConnectionEventData.cs b/src/EFCore.Relational/Diagnostics/ConnectionEventData.cs index 207041764d2..465383c6b6e 100644 --- a/src/EFCore.Relational/Diagnostics/ConnectionEventData.cs +++ b/src/EFCore.Relational/Diagnostics/ConnectionEventData.cs @@ -12,33 +12,27 @@ namespace Microsoft.EntityFrameworkCore.Diagnostics /// The event payload base class for /// connection events. /// - public class ConnectionEventData : EventData + public class ConnectionEventData : DbContextEventData { /// /// Constructs the event payload. /// /// The event definition. /// A delegate that generates a log message for this event. - /// - /// The . - /// - /// - /// A correlation ID that identifies the instance being used. - /// - /// - /// Indicates whether or not the operation is happening asynchronously. - /// - /// - /// The start time of this event. - /// + /// The . + /// The currently being used, to null if not known. + /// A correlation ID that identifies the instance being used. + /// Indicates whether or not the operation is happening asynchronously. + /// The start time of this event. public ConnectionEventData( [NotNull] EventDefinitionBase eventDefinition, [NotNull] Func messageGenerator, [NotNull] DbConnection connection, + [CanBeNull] DbContext context, Guid connectionId, bool async, DateTimeOffset startTime) - : base(eventDefinition, messageGenerator) + : base(eventDefinition, messageGenerator, context) { Connection = connection; ConnectionId = connectionId; diff --git a/src/EFCore.Relational/Diagnostics/DataReaderDisposingEventData.cs b/src/EFCore.Relational/Diagnostics/DataReaderDisposingEventData.cs index ef38aa1cf49..2306650503d 100644 --- a/src/EFCore.Relational/Diagnostics/DataReaderDisposingEventData.cs +++ b/src/EFCore.Relational/Diagnostics/DataReaderDisposingEventData.cs @@ -11,49 +11,35 @@ namespace Microsoft.EntityFrameworkCore.Diagnostics /// /// event payload for . /// - public class DataReaderDisposingEventData : EventData + public class DataReaderDisposingEventData : DbContextEventData { /// /// Constructs a event payload for . /// /// The event definition. /// A delegate that generates a log message for this event. - /// - /// The that created the reader. - /// - /// - /// The that is being disposed. - /// - /// - /// A correlation ID that identifies the instance being used. - /// - /// - /// A correlation ID that identifies the instance being used. - /// - /// - /// Gets the number of rows changed, inserted, or deleted by execution of the SQL statement. - /// - /// - /// Gets the number of read operations performed by this reader. - /// - /// - /// The start time of this event. - /// - /// - /// The duration this event. - /// + /// The that created the reader. + /// The that is being disposed. + /// The currently being used, to null if not known. + /// A correlation ID that identifies the instance being used. + /// A correlation ID that identifies the instance being used. + /// Gets the number of rows changed, inserted, or deleted by execution of the SQL statement. + /// Gets the number of read operations performed by this reader. + /// The start time of this event. + /// The duration this event. public DataReaderDisposingEventData( [NotNull] EventDefinitionBase eventDefinition, [NotNull] Func messageGenerator, [NotNull] DbCommand command, [NotNull] DbDataReader dataReader, + [CanBeNull] DbContext context, Guid commandId, Guid connectionId, int recordsAffected, int readCount, DateTimeOffset startTime, TimeSpan duration) - : base(eventDefinition, messageGenerator) + : base(eventDefinition, messageGenerator, context) { Command = command; DataReader = dataReader; diff --git a/src/EFCore.Relational/Diagnostics/DbCommandInterceptor.cs b/src/EFCore.Relational/Diagnostics/DbCommandInterceptor.cs index 71b079fc25f..890500fef42 100644 --- a/src/EFCore.Relational/Diagnostics/DbCommandInterceptor.cs +++ b/src/EFCore.Relational/Diagnostics/DbCommandInterceptor.cs @@ -1,13 +1,9 @@ // 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.Collections.Generic; using System.Data.Common; -using System.Linq; using System.Threading; using System.Threading.Tasks; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Diagnostics { @@ -19,40 +15,6 @@ namespace Microsoft.EntityFrameworkCore.Diagnostics /// public abstract class DbCommandInterceptor : IDbCommandInterceptor { - /// - /// - /// Chains the given instances into a chain that will - /// call each in order. - /// - /// - /// If only a single interceptor is supplied, then it is simply returned. - /// - /// - /// The interceptors to chain. - /// An interceptor that calls each of the given interceptors in order. - public static IDbCommandInterceptor CreateChain([NotNull] IEnumerable interceptors) - => CreateChain(Check.NotNull(interceptors, nameof(interceptors)).ToArray()); - - /// - /// - /// Chains the given instances into a chain that will - /// call each in order. - /// - /// - /// If only a single interceptor is supplied, then it is simply returned. - /// - /// - /// The interceptors to chain. - /// An interceptor that calls each of the given interceptors in order. - public static IDbCommandInterceptor CreateChain([NotNull] params IDbCommandInterceptor[] interceptors) - { - Check.NotNull(interceptors, nameof(interceptors)); - - return interceptors.Length == 1 - ? interceptors[0] - : new CompositeDbCommandInterceptor(interceptors); - } - /// /// Called just before EF intends to call . /// @@ -389,5 +351,28 @@ public virtual Task CommandFailedAsync( CommandErrorEventData eventData, CancellationToken cancellationToken = default) => Task.CompletedTask; + + /// + /// Called when execution of a is about to be disposed. />. + /// + /// The command. + /// Contextual information about the command and reader. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed execution by returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// + /// If null, then EF will dispose the reader as normal. + /// If non-null, then disposing the reader is suppressed. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in. + /// + public virtual InterceptionResult? DataReaderDisposing( + DbCommand command, + DataReaderDisposingEventData eventData, + InterceptionResult? result) + => result; } } diff --git a/src/EFCore.Relational/Diagnostics/DbConnectionInterceptor.cs b/src/EFCore.Relational/Diagnostics/DbConnectionInterceptor.cs new file mode 100644 index 00000000000..0862c3422ec --- /dev/null +++ b/src/EFCore.Relational/Diagnostics/DbConnectionInterceptor.cs @@ -0,0 +1,184 @@ +// 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.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.EntityFrameworkCore.Diagnostics +{ + /// + /// + /// Abstract base class for for use when implementing a subset + /// of the interface methods. + /// + /// + public abstract class DbConnectionInterceptor : IDbConnectionInterceptor + { + /// + /// Called just before EF intends to call . + /// + /// The connection. + /// Contextual information about the connection. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed execution by returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// + /// If null, then EF will open the connection as normal. + /// If non-null, then connection opening is suppressed. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in. + /// + public virtual InterceptionResult? ConnectionOpening( + DbConnection connection, + ConnectionEventData eventData, + InterceptionResult? result) + => result; + + /// + /// Called just before EF intends to call . + /// + /// The connection. + /// Contextual information about the connection. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed execution by returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// The cancellation token. + /// + /// If the result is null, then EF will open the connection as normal. + /// If the result is non-null value, then connection opening is suppressed. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in, often using + /// + public virtual Task ConnectionOpeningAsync( + DbConnection connection, + ConnectionEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + => Task.FromResult(result); + + /// + /// Called just after EF has called . + /// + /// The connection. + /// Contextual information about the connection. + public virtual void ConnectionOpened(DbConnection connection, ConnectionEndEventData eventData) + { + } + + /// + /// Called just after EF has called . + /// + /// The connection. + /// Contextual information about the connection. + /// The cancellation token. + /// A representing the asynchronous operation. + public virtual Task ConnectionOpenedAsync( + DbConnection connection, + ConnectionEndEventData eventData, + CancellationToken cancellationToken = default) + => Task.CompletedTask; + + /// + /// Called just before EF intends to call . + /// + /// The connection. + /// Contextual information about the connection. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed execution by returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// + /// If null, then EF will close the connection as normal. + /// If non-null, then connection closing is suppressed. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in. + /// + public virtual InterceptionResult? ConnectionClosing( + DbConnection connection, + ConnectionEventData eventData, + InterceptionResult? result) + => result; + + /// + /// Called just before EF intends to call in an async context. + /// + /// The connection. + /// Contextual information about the connection. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed execution by returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// The cancellation token. + /// + /// If the result is null, then EF will close the connection as normal. + /// If the result is non-null value, then connection closing is suppressed. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in, often using + /// + public virtual Task ConnectionClosingAsync( + DbConnection connection, + ConnectionEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + => Task.FromResult(result); + + /// + /// Called just after EF has called in an async context. + /// + /// The connection. + /// Contextual information about the connection. + public virtual void ConnectionClosed( + DbConnection connection, + ConnectionEndEventData eventData) + { + } + + /// + /// Called just after EF has called . + /// + /// The connection. + /// Contextual information about the connection. + /// The cancellation token. + /// A representing the asynchronous operation. + public virtual Task ConnectionClosedAsync( + DbConnection connection, + ConnectionEndEventData eventData, + CancellationToken cancellationToken = default) + => Task.CompletedTask; + + /// + /// Called when opening of a connection has failed with an exception. />. + /// + /// The connection. + /// Contextual information about the connection. + public virtual void ConnectionFailed( + DbConnection connection, + ConnectionErrorEventData eventData) + { + } + + /// + /// Called when opening of a connection has failed with an exception. />. + /// + /// The connection. + /// Contextual information about the connection. + /// The cancellation token. + /// A representing the asynchronous operation. + public virtual Task ConnectionFailedAsync( + DbConnection connection, + ConnectionErrorEventData eventData, + CancellationToken cancellationToken = default) + => Task.CompletedTask; + } +} diff --git a/src/EFCore.Relational/Diagnostics/DbTransactionInterceptor.cs b/src/EFCore.Relational/Diagnostics/DbTransactionInterceptor.cs new file mode 100644 index 00000000000..65997df9f36 --- /dev/null +++ b/src/EFCore.Relational/Diagnostics/DbTransactionInterceptor.cs @@ -0,0 +1,333 @@ +// 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.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.EntityFrameworkCore.Diagnostics +{ + /// + /// + /// Abstract base class for for use when implementing a subset + /// of the interface methods. + /// + /// + public abstract class DbTransactionInterceptor : IDbTransactionInterceptor + { + /// + /// Called just before EF intends to call . + /// + /// The connection. + /// Contextual information about connection and transaction. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed creation by returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// + /// If null, then EF will start the transaction as normal. + /// If non-null, then transaction creation is suppressed and the value contained in + /// the we be used by EF instead. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in. + /// + public virtual InterceptionResult? TransactionStarting( + DbConnection connection, + TransactionStartingEventData eventData, + InterceptionResult? result) + => result; + + /// + /// + /// Called immediately after EF calls . + /// + /// + /// This method is still called if an interceptor suppressed creation in . + /// In this case, is the result returned by . + /// + /// + /// The connection. + /// Contextual information about connection and transaction. + /// + /// The result of the call to . + /// This value is typically used as the return value for the implementation of this method. + /// + /// + /// The result that EF will use. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in. + /// + public virtual DbTransaction TransactionStarted( + DbConnection connection, + TransactionEndEventData eventData, + DbTransaction result) + => result; + + /// + /// Called just before EF intends to call . + /// + /// The connection. + /// Contextual information about connection and transaction. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed creation by returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// The cancellation token. + /// + /// If the result is null, then EF will start the transaction as normal. + /// If the result is non-null value, then transaction creation is suppressed and the value contained in + /// the we be used by EF instead. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in, often using + /// + public virtual Task?> TransactionStartingAsync( + DbConnection connection, TransactionStartingEventData eventData, InterceptionResult? result, + CancellationToken cancellationToken = default) + => Task.FromResult(result); + + /// + /// + /// Called immediately after EF calls . + /// + /// + /// This method is still called if an interceptor suppressed creation in . + /// In this case, is the result returned by . + /// + /// + /// The connection. + /// Contextual information about connection and transaction. + /// + /// The result of the call to . + /// This value is typically used as the return value for the implementation of this method. + /// + /// The cancellation token. + /// + /// A providing the result that EF will use. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in, often using + /// + public virtual Task TransactionStartedAsync( + DbConnection connection, TransactionEndEventData eventData, DbTransaction result, CancellationToken cancellationToken = default) + => Task.FromResult(result); + + /// + /// + /// Called immediately after is called. + /// + /// + /// The connection. + /// Contextual information about connection and transaction. + /// + /// The that was passed to . + /// This value is typically used as the return value for the implementation of this method. + /// + /// + /// The value that will be used as the effective value passed to + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in. + /// + public virtual DbTransaction TransactionUsed( + DbConnection connection, + TransactionEventData eventData, + DbTransaction result) + => result; + + /// + /// + /// Called immediately after is called. + /// + /// + /// The connection. + /// Contextual information about connection and transaction. + /// + /// The that was passed to . + /// This value is typically used as the return value for the implementation of this method. + /// + /// The cancellation token. + /// + /// A containing the value that will be used as the effective value passed + /// to + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in, often using + /// + public virtual Task TransactionUsedAsync( + DbConnection connection, + TransactionEventData eventData, + DbTransaction result, + CancellationToken cancellationToken = default) + => Task.FromResult(result); + + /// + /// Called just before EF intends to call . + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed committing by returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// + /// If null, then EF will committing the transaction as normal. + /// If non-null, then committing the transaction is suppressed. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in. + /// + public virtual InterceptionResult? TransactionCommitting( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult? result) + => result; + + /// + /// Called immediately after EF calls . + /// + /// The transaction. + /// Contextual information about connection and transaction. + public virtual void TransactionCommitted( + DbTransaction transaction, + TransactionEndEventData eventData) + { + } + + /// + /// Called just before EF intends to call . + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed committing returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// The cancellation token. + /// + /// If the result is null, then EF will commit the transaction as normal. + /// If the result is non-null value, committing the transaction is suppressed. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in, often using + /// + public virtual Task TransactionCommittingAsync( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + => Task.FromResult(result); + + /// + /// Called immediately after EF calls . + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// The cancellation token. + /// A representing the asynchronous operation. + public virtual Task TransactionCommittedAsync( + DbTransaction transaction, + TransactionEndEventData eventData, + CancellationToken cancellationToken = default) + => Task.CompletedTask; + + /// + /// Called just before EF intends to call . + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed rolling back by returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// + /// If null, then EF will roll back the transaction as normal. + /// If non-null, then rolling back the transaction is suppressed. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in. + /// + public virtual InterceptionResult? TransactionRollingBack( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult? result) + => result; + + /// + /// Called immediately after EF calls . + /// + /// The transaction. + /// Contextual information about connection and transaction. + public virtual void TransactionRolledBack( + DbTransaction transaction, + TransactionEndEventData eventData) + { + } + + /// + /// Called just before EF intends to call . + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed rolling back returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// The cancellation token. + /// + /// If the result is null, then EF will roll back the transaction as normal. + /// If the result is non-null value, rolling back the transaction is suppressed. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in, often using + /// + public virtual Task TransactionRollingBackAsync( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + => Task.FromResult(result); + + /// + /// Called immediately after EF calls . + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// The cancellation token. + /// A representing the asynchronous operation. + public virtual Task TransactionRolledBackAsync( + DbTransaction transaction, + TransactionEndEventData eventData, + CancellationToken cancellationToken = default) + => Task.CompletedTask; + + /// + /// Called when use of a has failed with an exception. />. + /// + /// The transaction. + /// Contextual information about connection and transaction. + public virtual void TransactionFailed( + DbTransaction transaction, + TransactionErrorEventData eventData) + { + } + + /// + /// Called when use of a has failed with an exception. />. + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// The cancellation token. + /// A representing the asynchronous operation. + public virtual Task TransactionFailedAsync( + DbTransaction transaction, + TransactionErrorEventData eventData, + CancellationToken cancellationToken = default) + => Task.CompletedTask; + } +} diff --git a/src/EFCore.Relational/Diagnostics/IDbCommandInterceptor.cs b/src/EFCore.Relational/Diagnostics/IDbCommandInterceptor.cs index 099b7cf753a..df8ac2a9e62 100644 --- a/src/EFCore.Relational/Diagnostics/IDbCommandInterceptor.cs +++ b/src/EFCore.Relational/Diagnostics/IDbCommandInterceptor.cs @@ -5,7 +5,6 @@ using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Infrastructure; namespace Microsoft.EntityFrameworkCore.Diagnostics { @@ -21,19 +20,16 @@ namespace Microsoft.EntityFrameworkCore.Diagnostics /// Consider inheriting from if not implementing all methods. /// /// - /// Use to register - /// an application interceptor. - /// - /// - /// Multiple interceptors can be composed using the . + /// Use + /// to register application interceptors. /// /// /// Extensions can also register interceptors in the internal service provider. /// If both injected and application interceptors are found, then the injected interceptors are run in the - /// order that they are resolved from the service provider, and then the application interceptor is run last. + /// order that they are resolved from the service provider, and then the application interceptors are run last. /// /// - public interface IDbCommandInterceptor + public interface IDbCommandInterceptor : IInterceptor { /// /// Called just before EF intends to call . @@ -355,5 +351,27 @@ Task CommandFailedAsync( [NotNull] DbCommand command, [NotNull] CommandErrorEventData eventData, CancellationToken cancellationToken = default); + + /// + /// Called when execution of a is about to be disposed. />. + /// + /// The command. + /// Contextual information about the command and reader. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed execution by returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// + /// If null, then EF will dispose the reader as normal. + /// If non-null, then disposing the reader is suppressed. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in. + /// + InterceptionResult? DataReaderDisposing( + [NotNull] DbCommand command, + [NotNull] DataReaderDisposingEventData eventData, + InterceptionResult? result); } } diff --git a/src/EFCore.Relational/Diagnostics/IDbConnectionInterceptor.cs b/src/EFCore.Relational/Diagnostics/IDbConnectionInterceptor.cs new file mode 100644 index 00000000000..0a8106fd93e --- /dev/null +++ b/src/EFCore.Relational/Diagnostics/IDbConnectionInterceptor.cs @@ -0,0 +1,189 @@ +// 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.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Diagnostics +{ + /// + /// + /// Allows interception of operations on . + /// + /// + /// Connection interceptors can be used to view, change, or suppress the operation on , and + /// to modify the result before it is returned to EF. + /// + /// + /// Consider inheriting from if not implementing all methods. + /// + /// + /// Use + /// to register application interceptors. + /// + /// + /// Extensions can also register interceptors in the internal service provider. + /// If both injected and application interceptors are found, then the injected interceptors are run in the + /// order that they are resolved from the service provider, and then the application interceptors are run last. + /// + /// + public interface IDbConnectionInterceptor : IInterceptor + { + /// + /// Called just before EF intends to call . + /// + /// The connection. + /// Contextual information about the connection. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed execution by returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// + /// If null, then EF will open the connection as normal. + /// If non-null, then connection opening is suppressed. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in. + /// + InterceptionResult? ConnectionOpening( + [NotNull] DbConnection connection, + [NotNull] ConnectionEventData eventData, + InterceptionResult? result); + + /// + /// Called just before EF intends to call . + /// + /// The connection. + /// Contextual information about the connection. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed execution by returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// The cancellation token. + /// + /// If the result is null, then EF will open the connection as normal. + /// If the result is non-null value, then connection opening is suppressed. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in, often using + /// + Task ConnectionOpeningAsync( + [NotNull] DbConnection connection, + [NotNull] ConnectionEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default); + + /// + /// Called just after EF has called . + /// + /// The connection. + /// Contextual information about the connection. + void ConnectionOpened( + [NotNull] DbConnection connection, + [NotNull] ConnectionEndEventData eventData); + + /// + /// Called just after EF has called . + /// + /// The connection. + /// Contextual information about the connection. + /// The cancellation token. + /// A representing the asynchronous operation. + Task ConnectionOpenedAsync( + [NotNull] DbConnection connection, + [NotNull] ConnectionEndEventData eventData, + CancellationToken cancellationToken = default); + + /// + /// Called just before EF intends to call . + /// + /// The connection. + /// Contextual information about the connection. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed execution by returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// + /// If null, then EF will close the connection as normal. + /// If non-null, then connection closing is suppressed. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in. + /// + InterceptionResult? ConnectionClosing( + [NotNull] DbConnection connection, + [NotNull] ConnectionEventData eventData, + InterceptionResult? result); + + /// + /// Called just before EF intends to call in an async context. + /// + /// The connection. + /// Contextual information about the connection. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed execution by returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// The cancellation token. + /// + /// If the result is null, then EF will close the connection as normal. + /// If the result is non-null value, then connection closing is suppressed. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in, often using + /// + Task ConnectionClosingAsync( + [NotNull] DbConnection connection, + [NotNull] ConnectionEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default); + + /// + /// Called just after EF has called in an async context. + /// + /// The connection. + /// Contextual information about the connection. + void ConnectionClosed( + [NotNull] DbConnection connection, + [NotNull] ConnectionEndEventData eventData); + + /// + /// Called just after EF has called . + /// + /// The connection. + /// Contextual information about the connection. + /// The cancellation token. + /// A representing the asynchronous operation. + Task ConnectionClosedAsync( + [NotNull] DbConnection connection, + [NotNull] ConnectionEndEventData eventData, + CancellationToken cancellationToken = default); + + /// + /// Called when closing of a connection has failed with an exception. />. + /// + /// The connection. + /// Contextual information about the connection. + void ConnectionFailed( + [NotNull] DbConnection connection, + [NotNull] ConnectionErrorEventData eventData); + + /// + /// Called when closing of a connection has failed with an exception. />. + /// + /// The connection. + /// Contextual information about the connection. + /// The cancellation token. + /// A representing the asynchronous operation. + Task ConnectionFailedAsync( + [NotNull] DbConnection connection, + [NotNull] ConnectionErrorEventData eventData, + CancellationToken cancellationToken = default); + } +} diff --git a/src/EFCore.Relational/Diagnostics/IDbTransactionInterceptor.cs b/src/EFCore.Relational/Diagnostics/IDbTransactionInterceptor.cs new file mode 100644 index 00000000000..2842dd4f481 --- /dev/null +++ b/src/EFCore.Relational/Diagnostics/IDbTransactionInterceptor.cs @@ -0,0 +1,336 @@ +// 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.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Diagnostics +{ + /// + /// + /// Allows interception of operations related to a . + /// + /// + /// Transaction interceptors can be used to view, change, or suppress operations on , and + /// to modify the result before it is returned to EF. + /// + /// + /// Consider inheriting from if not implementing all methods. + /// + /// + /// Use + /// to register application interceptors. + /// + /// + /// Extensions can also register interceptors in the internal service provider. + /// If both injected and application interceptors are found, then the injected interceptors are run in the + /// order that they are resolved from the service provider, and then the application interceptors are run last. + /// + /// + public interface IDbTransactionInterceptor : IInterceptor + { + /// + /// Called just before EF intends to call . + /// + /// The connection. + /// Contextual information about connection and transaction. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed creation by returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// + /// If null, then EF will start the transaction as normal. + /// If non-null, then transaction creation is suppressed and the value contained in + /// the we be used by EF instead. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in. + /// + InterceptionResult? TransactionStarting( + [NotNull] DbConnection connection, + [NotNull] TransactionStartingEventData eventData, + InterceptionResult? result); + + /// + /// + /// Called immediately after EF calls . + /// + /// + /// This method is still called if an interceptor suppressed creation in . + /// In this case, is the result returned by . + /// + /// + /// The connection. + /// Contextual information about connection and transaction. + /// + /// The result of the call to . + /// This value is typically used as the return value for the implementation of this method. + /// + /// + /// The result that EF will use. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in. + /// + DbTransaction TransactionStarted( + [NotNull] DbConnection connection, + [NotNull] TransactionEndEventData eventData, + [CanBeNull] DbTransaction result); + + /// + /// Called just before EF intends to call . + /// + /// The connection. + /// Contextual information about connection and transaction. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed creation by returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// The cancellation token. + /// + /// If the result is null, then EF will start the transaction as normal. + /// If the result is non-null value, then transaction creation is suppressed and the value contained in + /// the we be used by EF instead. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in, often using + /// + Task?> TransactionStartingAsync( + [NotNull] DbConnection connection, + [NotNull] TransactionStartingEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default); + + /// + /// + /// Called immediately after EF calls . + /// + /// + /// This method is still called if an interceptor suppressed creation in . + /// In this case, is the result returned by . + /// + /// + /// The connection. + /// Contextual information about connection and transaction. + /// + /// The result of the call to . + /// This value is typically used as the return value for the implementation of this method. + /// + /// The cancellation token. + /// + /// A providing the result that EF will use. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in, often using + /// + Task TransactionStartedAsync( + [NotNull] DbConnection connection, + [NotNull] TransactionEndEventData eventData, + [CanBeNull] DbTransaction result, + CancellationToken cancellationToken = default); + + /// + /// + /// Called immediately after is called. + /// + /// + /// The connection. + /// Contextual information about connection and transaction. + /// + /// The that was passed to . + /// This value is typically used as the return value for the implementation of this method. + /// + /// + /// The value that will be used as the effective value passed to + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in. + /// + DbTransaction TransactionUsed( + [NotNull] DbConnection connection, + [NotNull] TransactionEventData eventData, + [CanBeNull] DbTransaction result); + + /// + /// + /// Called immediately after is called. + /// + /// + /// The connection. + /// Contextual information about connection and transaction. + /// + /// The that was passed to . + /// This value is typically used as the return value for the implementation of this method. + /// + /// The cancellation token. + /// + /// A containing the value that will be used as the effective value passed + /// to + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in, often using + /// + Task TransactionUsedAsync( + [NotNull] DbConnection connection, + [NotNull] TransactionEventData eventData, + [CanBeNull] DbTransaction result, + CancellationToken cancellationToken = default); + + /// + /// Called just before EF intends to call . + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed committing by returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// + /// If null, then EF will committing the transaction as normal. + /// If non-null, then committing the transaction is suppressed. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in. + /// + InterceptionResult? TransactionCommitting( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEventData eventData, + InterceptionResult? result); + + /// + /// Called immediately after EF calls . + /// + /// The transaction. + /// Contextual information about connection and transaction. + void TransactionCommitted( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEndEventData eventData); + + /// + /// Called just before EF intends to call . + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed committing returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// The cancellation token. + /// + /// If the result is null, then EF will commit the transaction as normal. + /// If the result is non-null value, committing the transaction is suppressed. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in, often using + /// + Task TransactionCommittingAsync( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default); + + /// + /// Called immediately after EF calls . + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// The cancellation token. + /// A representing the asynchronous operation. + Task TransactionCommittedAsync( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEndEventData eventData, + CancellationToken cancellationToken = default); + + /// + /// Called just before EF intends to call . + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed rolling back by returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// + /// If null, then EF will roll back the transaction as normal. + /// If non-null, then rolling back the transaction is suppressed. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in. + /// + InterceptionResult? TransactionRollingBack( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEventData eventData, + InterceptionResult? result); + + /// + /// Called immediately after EF calls . + /// + /// The transaction. + /// Contextual information about connection and transaction. + void TransactionRolledBack( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEndEventData eventData); + + /// + /// Called just before EF intends to call . + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// + /// The current result, or null if no result yet exists. + /// This value will be non-null if some previous interceptor suppressed rolling back returning a result from + /// its implementation of this method. + /// This value is typically used as the return value for the implementation of this method. + /// + /// The cancellation token. + /// + /// If the result is null, then EF will roll back the transaction as normal. + /// If the result is non-null value, rolling back the transaction is suppressed. + /// A normal implementation of this method for any interceptor that is not attempting to change the result + /// is to return the value passed in, often using + /// + Task TransactionRollingBackAsync( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default); + + /// + /// Called immediately after EF calls . + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// The cancellation token. + /// A representing the asynchronous operation. + Task TransactionRolledBackAsync( + [NotNull] DbTransaction transaction, + [NotNull] TransactionEndEventData eventData, + CancellationToken cancellationToken = default); + + /// + /// Called when use of a has failed with an exception. />. + /// + /// The transaction. + /// Contextual information about connection and transaction. + void TransactionFailed( + [NotNull] DbTransaction transaction, + [NotNull] TransactionErrorEventData eventData); + + /// + /// Called when use of a has failed with an exception. />. + /// + /// The transaction. + /// Contextual information about connection and transaction. + /// The cancellation token. + /// A representing the asynchronous operation. + Task TransactionFailedAsync( + [NotNull] DbTransaction transaction, + [NotNull] TransactionErrorEventData eventData, + CancellationToken cancellationToken = default); + } +} diff --git a/src/EFCore.Relational/Diagnostics/IRelationalInterceptors.cs b/src/EFCore.Relational/Diagnostics/IRelationalInterceptors.cs deleted file mode 100644 index b7d8935173c..00000000000 --- a/src/EFCore.Relational/Diagnostics/IRelationalInterceptors.cs +++ /dev/null @@ -1,34 +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.Extensions.DependencyInjection; - -namespace Microsoft.EntityFrameworkCore.Diagnostics -{ - /// - /// - /// Base interface for all relational interceptor definitions. - /// - /// - /// Rather than implementing this interface directly, relational providers that need to add interceptors should inherit - /// from . Relational providers should inherit from . - /// - /// - /// This type is typically used by database providers (and other extensions). It is generally - /// not used in application code. - /// - /// - /// The service lifetime is . This means that each - /// instance will use its own instance of this service. - /// The implementation may depend on other services registered with any lifetime. - /// The implementation does not need to be thread-safe. - /// - /// - public interface IRelationalInterceptors : IInterceptors - { - /// - /// The registered, or null if none is registered. - /// - IDbCommandInterceptor CommandInterceptor { get; } - } -} diff --git a/src/EFCore.Relational/Diagnostics/Internal/DbCommandInterceptorResolver.cs b/src/EFCore.Relational/Diagnostics/Internal/DbCommandInterceptorResolver.cs new file mode 100644 index 00000000000..487a125ca94 --- /dev/null +++ b/src/EFCore.Relational/Diagnostics/Internal/DbCommandInterceptorResolver.cs @@ -0,0 +1,232 @@ +// 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.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Diagnostics.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class DbCommandInterceptorResolver : InterceptorResolver + { + /// + /// 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. + /// + protected override IDbCommandInterceptor CreateChain(IEnumerable interceptors) + => new CompositeDbCommandInterceptor(interceptors); + + private sealed class CompositeDbCommandInterceptor : IDbCommandInterceptor + { + private readonly IDbCommandInterceptor[] _interceptors; + + public CompositeDbCommandInterceptor([NotNull] IEnumerable interceptors) + { + _interceptors = interceptors.ToArray(); + } + + public InterceptionResult? ReaderExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = _interceptors[i].ReaderExecuting(command, eventData, result); + } + + return result; + } + + public InterceptionResult? ScalarExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = _interceptors[i].ScalarExecuting(command, eventData, result); + } + + return result; + } + + public InterceptionResult? NonQueryExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = _interceptors[i].NonQueryExecuting(command, eventData, result); + } + + return result; + } + + public async Task?> ReaderExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = await _interceptors[i].ReaderExecutingAsync(command, eventData, result, cancellationToken); + } + + return result; + } + + public async Task?> ScalarExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = await _interceptors[i].ScalarExecutingAsync(command, eventData, result, cancellationToken); + } + + return result; + } + + public async Task?> NonQueryExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = await _interceptors[i].NonQueryExecutingAsync(command, eventData, result, cancellationToken); + } + + return result; + } + + public DbDataReader ReaderExecuted( + DbCommand command, + CommandExecutedEventData eventData, + DbDataReader result) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = _interceptors[i].ReaderExecuted(command, eventData, result); + } + + return result; + } + + public object ScalarExecuted( + DbCommand command, + CommandExecutedEventData eventData, + object result) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = _interceptors[i].ScalarExecuted(command, eventData, result); + } + + return result; + } + + public int NonQueryExecuted( + DbCommand command, + CommandExecutedEventData eventData, + int result) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = _interceptors[i].NonQueryExecuted(command, eventData, result); + } + + return result; + } + + public async Task ReaderExecutedAsync( + DbCommand command, + CommandExecutedEventData eventData, + DbDataReader result, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = await _interceptors[i].ReaderExecutedAsync(command, eventData, result, cancellationToken); + } + + return result; + } + + public async Task ScalarExecutedAsync( + DbCommand command, + CommandExecutedEventData eventData, + object result, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = await _interceptors[i].ScalarExecutedAsync(command, eventData, result, cancellationToken); + } + + return result; + } + + public async Task NonQueryExecutedAsync( + DbCommand command, + CommandExecutedEventData eventData, + int result, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = await _interceptors[i].NonQueryExecutedAsync(command, eventData, result, cancellationToken); + } + + return result; + } + + public void CommandFailed(DbCommand command, CommandErrorEventData eventData) + { + for (var i = 0; i < _interceptors.Length; i++) + { + _interceptors[i].CommandFailed(command, eventData); + } + } + + public async Task CommandFailedAsync( + DbCommand command, CommandErrorEventData eventData, CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + await _interceptors[i].CommandFailedAsync(command, eventData, cancellationToken); + } + } + + public InterceptionResult? DataReaderDisposing( + DbCommand command, + DataReaderDisposingEventData eventData, + InterceptionResult? result) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = _interceptors[i].DataReaderDisposing(command, eventData, result); + } + + return result; + } + } + } +} diff --git a/src/EFCore.Relational/Diagnostics/Internal/DbConnectionInterceptorResolver.cs b/src/EFCore.Relational/Diagnostics/Internal/DbConnectionInterceptorResolver.cs new file mode 100644 index 00000000000..2b11c609fe0 --- /dev/null +++ b/src/EFCore.Relational/Diagnostics/Internal/DbConnectionInterceptorResolver.cs @@ -0,0 +1,157 @@ +// 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.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Diagnostics.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class DbConnectionInterceptorResolver : InterceptorResolver + { + /// + /// 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. + /// + protected override IDbConnectionInterceptor CreateChain(IEnumerable interceptors) + => new CompositeDbConnectionInterceptor(interceptors); + + private sealed class CompositeDbConnectionInterceptor : IDbConnectionInterceptor + { + private readonly IDbConnectionInterceptor[] _interceptors; + + public CompositeDbConnectionInterceptor([NotNull] IEnumerable interceptors) + { + _interceptors = interceptors.ToArray(); + } + + public InterceptionResult? ConnectionOpening( + DbConnection connection, + ConnectionEventData eventData, + InterceptionResult? result) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = _interceptors[i].ConnectionOpening(connection, eventData, result); + } + + return result; + } + + public async Task ConnectionOpeningAsync( + DbConnection connection, + ConnectionEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = await _interceptors[i].ConnectionOpeningAsync(connection, eventData, result, cancellationToken); + } + + return result; + } + + public void ConnectionOpened( + DbConnection connection, + ConnectionEndEventData eventData) + { + for (var i = 0; i < _interceptors.Length; i++) + { + _interceptors[i].ConnectionOpened(connection, eventData); + } + } + + public async Task ConnectionOpenedAsync( + DbConnection connection, + ConnectionEndEventData eventData, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + await _interceptors[i].ConnectionOpenedAsync(connection, eventData, cancellationToken); + } + } + + public InterceptionResult? ConnectionClosing( + DbConnection connection, + ConnectionEventData eventData, + InterceptionResult? result) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = _interceptors[i].ConnectionClosing(connection, eventData, result); + } + + return result; + } + + public async Task ConnectionClosingAsync( + DbConnection connection, + ConnectionEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = await _interceptors[i].ConnectionClosingAsync(connection, eventData, result, cancellationToken); + } + + return result; + } + + public void ConnectionClosed( + DbConnection connection, + ConnectionEndEventData eventData) + { + for (var i = 0; i < _interceptors.Length; i++) + { + _interceptors[i].ConnectionClosed(connection, eventData); + } + } + + public async Task ConnectionClosedAsync( + DbConnection connection, + ConnectionEndEventData eventData, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + await _interceptors[i].ConnectionClosedAsync(connection, eventData, cancellationToken); + } + } + + public void ConnectionFailed( + DbConnection connection, + ConnectionErrorEventData eventData) + { + for (var i = 0; i < _interceptors.Length; i++) + { + _interceptors[i].ConnectionFailed(connection, eventData); + } + } + + public async Task ConnectionFailedAsync( + DbConnection connection, + ConnectionErrorEventData eventData, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + await _interceptors[i].ConnectionFailedAsync(connection, eventData, cancellationToken); + } + } + } + } +} diff --git a/src/EFCore.Relational/Diagnostics/Internal/DbTransactionInterceptorResolver.cs b/src/EFCore.Relational/Diagnostics/Internal/DbTransactionInterceptorResolver.cs new file mode 100644 index 00000000000..15c2803b692 --- /dev/null +++ b/src/EFCore.Relational/Diagnostics/Internal/DbTransactionInterceptorResolver.cs @@ -0,0 +1,236 @@ +// 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.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Diagnostics.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class DbTransactionInterceptorResolver : InterceptorResolver + { + /// + /// 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. + /// + protected override IDbTransactionInterceptor CreateChain(IEnumerable interceptors) + => new CompositeDbTransactionInterceptor(interceptors); + + private sealed class CompositeDbTransactionInterceptor : IDbTransactionInterceptor + { + private readonly IDbTransactionInterceptor[] _interceptors; + + public CompositeDbTransactionInterceptor([NotNull] IEnumerable interceptors) + { + _interceptors = interceptors.ToArray(); + } + + public InterceptionResult? TransactionStarting( + DbConnection connection, + TransactionStartingEventData eventData, + InterceptionResult? result) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = _interceptors[i].TransactionStarting(connection, eventData, result); + } + + return result; + } + + public DbTransaction TransactionStarted( + DbConnection connection, + TransactionEndEventData eventData, + DbTransaction result) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = _interceptors[i].TransactionStarted(connection, eventData, result); + } + + return result; + } + + public async Task?> TransactionStartingAsync( + DbConnection connection, + TransactionStartingEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = await _interceptors[i].TransactionStartingAsync(connection, eventData, result, cancellationToken); + } + + return result; + } + + public async Task TransactionStartedAsync( + DbConnection connection, + TransactionEndEventData eventData, + DbTransaction result, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = await _interceptors[i].TransactionStartedAsync(connection, eventData, result, cancellationToken); + } + + return result; + } + + public DbTransaction TransactionUsed( + DbConnection connection, + TransactionEventData eventData, + DbTransaction result) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = _interceptors[i].TransactionUsed(connection, eventData, result); + } + + return result; + } + + public async Task TransactionUsedAsync( + DbConnection connection, + TransactionEventData eventData, + DbTransaction result, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = await _interceptors[i].TransactionUsedAsync(connection, eventData, result, cancellationToken); + } + + return result; + } + + public InterceptionResult? TransactionCommitting( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult? result) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = _interceptors[i].TransactionCommitting(transaction, eventData, result); + } + + return result; + } + + public void TransactionCommitted( + DbTransaction transaction, + TransactionEndEventData eventData) + { + for (var i = 0; i < _interceptors.Length; i++) + { + _interceptors[i].TransactionCommitted(transaction, eventData); + } + } + + public async Task TransactionCommittingAsync( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = await _interceptors[i].TransactionCommittingAsync(transaction, eventData, result, cancellationToken); + } + + return result; + } + + public async Task TransactionCommittedAsync( + DbTransaction transaction, + TransactionEndEventData eventData, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + await _interceptors[i].TransactionCommittedAsync(transaction, eventData, cancellationToken); + } + } + + public InterceptionResult? TransactionRollingBack( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult? result) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = _interceptors[i].TransactionRollingBack(transaction, eventData, result); + } + + return result; + } + + public void TransactionRolledBack( + DbTransaction transaction, + TransactionEndEventData eventData) + { + for (var i = 0; i < _interceptors.Length; i++) + { + _interceptors[i].TransactionRolledBack(transaction, eventData); + } + } + + public async Task TransactionRollingBackAsync( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + result = await _interceptors[i].TransactionRollingBackAsync(transaction, eventData, result, cancellationToken); + } + + return result; + } + + public async Task TransactionRolledBackAsync( + DbTransaction transaction, + TransactionEndEventData eventData, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + await _interceptors[i].TransactionRolledBackAsync(transaction, eventData, cancellationToken); + } + } + + public void TransactionFailed(DbTransaction transaction, TransactionErrorEventData eventData) + { + for (var i = 0; i < _interceptors.Length; i++) + { + _interceptors[i].TransactionFailed(transaction, eventData); + } + } + + public async Task TransactionFailedAsync( + DbTransaction transaction, + TransactionErrorEventData eventData, + CancellationToken cancellationToken = default) + { + for (var i = 0; i < _interceptors.Length; i++) + { + await _interceptors[i].TransactionFailedAsync(transaction, eventData, cancellationToken); + } + } + } + } +} diff --git a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs index 7981a6fe119..c8eaf070bfe 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalEventId.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalEventId.cs @@ -47,6 +47,9 @@ private enum Id AmbientTransactionWarning, AmbientTransactionEnlisted, ExplicitTransactionEnlisted, + TransactionStarting, + TransactionCommitting, + TransactionRollingBack, // DataReader events DataReaderDisposing = CoreEventId.RelationalBaseId + 300, @@ -197,11 +200,24 @@ private enum Id /// This event is in the category. /// /// - /// This event uses the payload when used with a . + /// This event uses the payload when used with a . /// /// public static readonly EventId TransactionStarted = MakeTransactionId(Id.TransactionStarted); + /// + /// + /// A database transaction is starting. + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId TransactionStarting = MakeTransactionId(Id.TransactionStarting); + /// /// /// Entity Framework started using an already existing database transaction. @@ -215,6 +231,19 @@ private enum Id /// public static readonly EventId TransactionUsed = MakeTransactionId(Id.TransactionUsed); + /// + /// + /// A database transaction is being committed. + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId TransactionCommitting = MakeTransactionId(Id.TransactionCommitting); + /// /// /// A database transaction has been committed. @@ -228,6 +257,19 @@ private enum Id /// public static readonly EventId TransactionCommitted = MakeTransactionId(Id.TransactionCommitted); + /// + /// + /// A database transaction is being rolled back. + /// + /// + /// This event is in the category. + /// + /// + /// This event uses the payload when used with a . + /// + /// + public static readonly EventId TransactionRollingBack = MakeTransactionId(Id.TransactionRollingBack); + /// /// /// A database transaction has been rolled back. diff --git a/src/EFCore.Relational/Diagnostics/RelationalInterceptors.cs b/src/EFCore.Relational/Diagnostics/RelationalInterceptors.cs deleted file mode 100644 index 5a63b10a279..00000000000 --- a/src/EFCore.Relational/Diagnostics/RelationalInterceptors.cs +++ /dev/null @@ -1,100 +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 JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Utilities; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.EntityFrameworkCore.Diagnostics -{ - /// - /// - /// Base implementation for all relational interceptors. - /// - /// - /// Relational providers that need to add interceptors should inherit from this class. - /// Non-Relational providers should inherit from . - /// - /// - /// This type is typically used by database providers (and other extensions). It is generally - /// not used in application code. - /// - /// - /// The service lifetime is . This means that each - /// instance will use its own instance of this service. - /// The implementation may depend on other services registered with any lifetime. - /// The implementation does not need to be thread-safe. - /// - /// - public class RelationalInterceptors : Interceptors, IRelationalInterceptors - { - private bool _initialized; - private IDbCommandInterceptor _interceptor; - - /// - /// Creates a new instance using the given dependencies. - /// - /// The dependencies for this service. - /// The relational-specific dependencies for this service. - public RelationalInterceptors( - [NotNull] InterceptorsDependencies dependencies, - [NotNull] RelationalInterceptorsDependencies relationalDependencies) - : base(dependencies) - { - Check.NotNull(relationalDependencies, nameof(relationalDependencies)); - - RelationalDependencies = relationalDependencies; - } - - /// - /// The relational-specific dependencies for this service. - /// - protected virtual RelationalInterceptorsDependencies RelationalDependencies { get; } - - /// - /// The registered, or null if none is registered. - /// - public virtual IDbCommandInterceptor CommandInterceptor - { - get - { - if (!_initialized) - { - var injectedInterceptors = RelationalDependencies.CommandInterceptors.ToList(); - - if (TryFindAppInterceptor(out var appInterceptor)) - { - injectedInterceptors.Add(appInterceptor); - } - - _interceptor = DbCommandInterceptor.CreateChain(injectedInterceptors); - - _initialized = true; - } - - return _interceptor; - } - } - - /// - /// We resolve this lazily because loggers are created very early in the initialization - /// process where is not yet available from D.I. - /// This means those loggers can't do interception, but that's okay because nothing - /// else is ready for them to do interception anyway. - /// - private bool TryFindAppInterceptor(out IDbCommandInterceptor interceptor) - { - interceptor = Dependencies - .ServiceProvider - .GetService() - .Extensions - .OfType() - .FirstOrDefault() - ?.CommandInterceptor; - - return interceptor != null; - } - } -} diff --git a/src/EFCore.Relational/Diagnostics/RelationalInterceptorsDependencies.cs b/src/EFCore.Relational/Diagnostics/RelationalInterceptorsDependencies.cs deleted file mode 100644 index c285b191dc5..00000000000 --- a/src/EFCore.Relational/Diagnostics/RelationalInterceptorsDependencies.cs +++ /dev/null @@ -1,70 +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.Collections.Generic; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Utilities; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.EntityFrameworkCore.Diagnostics -{ - /// - /// - /// Service dependencies parameter class for - /// - /// - /// This type is typically used by database providers (and other extensions). It is generally - /// not used in application code. - /// - /// - /// Do not construct instances of this class directly from either provider or application code as the - /// constructor signature may change as new dependencies are added. Instead, use this type in - /// your constructor so that an instance will be created and injected automatically by the - /// dependency injection container. To create an instance with some dependent services replaced, - /// first resolve the object from the dependency injection container, then replace selected - /// services using the 'With...' methods. Do not call the constructor at any point in this process. - /// - /// - /// The service lifetime is . This means that each - /// instance will use its own instance of this service. - /// The implementation may depend on other services registered with any lifetime. - /// The implementation does not need to be thread-safe. - /// - /// - public sealed class RelationalInterceptorsDependencies - { - /// - /// - /// Creates the service dependencies parameter object for a . - /// - /// - /// Do not call this constructor directly from either provider or application code as it may change - /// as new dependencies are added. Instead, use this type in your constructor so that an instance - /// will be created and injected automatically by the dependency injection container. To create - /// an instance with some dependent services replaced, first resolve the object from the dependency - /// injection container, then replace selected services using the 'With...' methods. Do not call - /// the constructor at any point in this process. - /// - /// - /// Command interceptors registered in D.I. - public RelationalInterceptorsDependencies([NotNull] IEnumerable commandInterceptors) - { - Check.NotNull(commandInterceptors, nameof(commandInterceptors)); - - CommandInterceptors = commandInterceptors; - } - - /// - /// Command interceptors registered in D.I. - /// - public IEnumerable CommandInterceptors { get; } - - /// - /// Clones this dependency parameter object with one service replaced. - /// - /// A replacement for the current dependency of this type. - /// A new parameter object with the given service replaced. - public RelationalInterceptorsDependencies With([NotNull] IEnumerable commandInterceptors) - => new RelationalInterceptorsDependencies(commandInterceptors); - } -} diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs index 28cf7621a69..2a5486ca1f5 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs @@ -21,6 +21,7 @@ using Microsoft.EntityFrameworkCore.Storage.Internal; using Microsoft.EntityFrameworkCore.Update; using Microsoft.Extensions.Logging; +using IsolationLevel = System.Data.IsolationLevel; namespace Microsoft.EntityFrameworkCore.Diagnostics { @@ -60,7 +61,7 @@ public static class RelationalLoggerExtensions LogCommandExecuting(diagnostics, command, definition); var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); - var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + var interceptor = diagnostics.Interceptors?.Resolve(); if (interceptor != null || diagnosticSourceEnabled) @@ -109,7 +110,7 @@ public static class RelationalLoggerExtensions LogCommandExecuting(diagnostics, command, definition); var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); - var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + var interceptor = diagnostics.Interceptors?.Resolve(); if (interceptor != null || diagnosticSourceEnabled) @@ -158,7 +159,7 @@ public static class RelationalLoggerExtensions LogCommandExecuting(diagnostics, command, definition); var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); - var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + var interceptor = diagnostics.Interceptors?.Resolve(); if (interceptor != null || diagnosticSourceEnabled) @@ -209,7 +210,7 @@ public static class RelationalLoggerExtensions LogCommandExecuting(diagnostics, command, definition); var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); - var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + var interceptor = diagnostics.Interceptors?.Resolve(); if (interceptor != null || diagnosticSourceEnabled) @@ -260,7 +261,7 @@ public static class RelationalLoggerExtensions LogCommandExecuting(diagnostics, command, definition); var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); - var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + var interceptor = diagnostics.Interceptors?.Resolve(); if (interceptor != null || diagnosticSourceEnabled) @@ -311,7 +312,7 @@ public static class RelationalLoggerExtensions LogCommandExecuting(diagnostics, command, definition); var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); - var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + var interceptor = diagnostics.Interceptors?.Resolve(); if (interceptor != null || diagnosticSourceEnabled) @@ -435,7 +436,7 @@ public static DbDataReader CommandReaderExecuted( LogCommandExecuted(diagnostics, command, duration, definition); var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); - var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + var interceptor = diagnostics.Interceptors?.Resolve(); if (interceptor != null || diagnosticSourceEnabled) @@ -490,7 +491,7 @@ public static object CommandScalarExecuted( LogCommandExecuted(diagnostics, command, duration, definition); var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); - var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + var interceptor = diagnostics.Interceptors?.Resolve(); if (interceptor != null || diagnosticSourceEnabled) @@ -545,7 +546,7 @@ public static int CommandNonQueryExecuted( LogCommandExecuted(diagnostics, command, duration, definition); var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); - var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + var interceptor = diagnostics.Interceptors?.Resolve(); if (interceptor != null || diagnosticSourceEnabled) @@ -602,7 +603,7 @@ public static Task CommandReaderExecutedAsync( LogCommandExecuted(diagnostics, command, duration, definition); var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); - var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + var interceptor = diagnostics.Interceptors?.Resolve(); if (interceptor != null || diagnosticSourceEnabled) @@ -659,7 +660,7 @@ public static Task CommandScalarExecutedAsync( LogCommandExecuted(diagnostics, command, duration, definition); var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); - var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + var interceptor = diagnostics.Interceptors?.Resolve(); if (interceptor != null || diagnosticSourceEnabled) @@ -716,7 +717,7 @@ public static Task CommandNonQueryExecutedAsync( LogCommandExecuted(diagnostics, command, duration, definition); var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); - var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + var interceptor = diagnostics.Interceptors?.Resolve(); if (interceptor != null || diagnosticSourceEnabled) @@ -841,22 +842,10 @@ public static void CommandError( { var definition = RelationalResources.LogCommandFailed(diagnostics); - var warningBehavior = definition.GetLogBehavior(diagnostics); - if (warningBehavior != WarningBehavior.Ignore) - { - definition.Log( - diagnostics, - warningBehavior, - string.Format(CultureInfo.InvariantCulture, "{0:N0}", duration.TotalMilliseconds), - command.Parameters.FormatParameters(ShouldLogParameterValues(diagnostics, command)), - command.CommandType, - command.CommandTimeout, - Environment.NewLine, - command.CommandText.TrimEnd()); - } + LogCommandError(diagnostics, command, duration, definition); var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); - var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + var interceptor = diagnostics.Interceptors?.Resolve(); if (interceptor != null || diagnosticSourceEnabled) @@ -879,6 +868,27 @@ public static void CommandError( } } + private static void LogCommandError( + IDiagnosticsLogger diagnostics, + DbCommand command, + TimeSpan duration, + EventDefinition definition) + { + var warningBehavior = definition.GetLogBehavior(diagnostics); + if (warningBehavior != WarningBehavior.Ignore) + { + definition.Log( + diagnostics, + warningBehavior, + string.Format(CultureInfo.InvariantCulture, "{0:N0}", duration.TotalMilliseconds), + command.Parameters.FormatParameters(ShouldLogParameterValues(diagnostics, command)), + command.CommandType, + command.CommandTimeout, + Environment.NewLine, + command.CommandText.TrimEnd()); + } + } + /// /// Logs for the event. /// @@ -907,22 +917,10 @@ public static Task CommandErrorAsync( { var definition = RelationalResources.LogCommandFailed(diagnostics); - var warningBehavior = definition.GetLogBehavior(diagnostics); - if (warningBehavior != WarningBehavior.Ignore) - { - definition.Log( - diagnostics, - warningBehavior, - string.Format(CultureInfo.InvariantCulture, "{0:N0}", duration.TotalMilliseconds), - command.Parameters.FormatParameters(ShouldLogParameterValues(diagnostics, command)), - command.CommandType, - command.CommandTimeout, - Environment.NewLine, - command.CommandText.TrimEnd()); - } + LogCommandError(diagnostics, command, duration, definition); var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); - var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + var interceptor = diagnostics.Interceptors?.Resolve(); if (interceptor != null || diagnosticSourceEnabled) @@ -1007,64 +1005,85 @@ private static string CommandError(EventDefinitionBase definition, EventData pay /// The diagnostics logger to use. /// The connection. /// The time that the operation was started. - /// Indicates whether or not this is an async operation. - public static void ConnectionOpening( + /// The result of execution, which may have been modified by an interceptor. + public static InterceptionResult? ConnectionOpening( [NotNull] this IDiagnosticsLogger diagnostics, [NotNull] IRelationalConnection connection, - DateTimeOffset startTime, - bool async) + DateTimeOffset startTime) { var definition = RelationalResources.LogOpeningConnection(diagnostics); - var warningBehavior = definition.GetLogBehavior(diagnostics); - if (warningBehavior != WarningBehavior.Ignore) + LogConnectionOpening(diagnostics, connection, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) { - definition.Log( + var eventData = BroadcastConnectionOpening( diagnostics, - warningBehavior, - connection.DbConnection.Database, connection.DbConnection.DataSource); - } + connection, + startTime, + definition, + false, + diagnosticSourceEnabled); - if (diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name)) - { - diagnostics.DiagnosticSource.Write( - definition.EventId.Name, - new ConnectionEventData( - definition, - ConnectionOpening, - connection.DbConnection, - connection.ConnectionId, - async, - startTime)); + if (interceptor != null) + { + return interceptor.ConnectionOpening(connection.DbConnection, eventData, null); + } } - } - private static string ConnectionOpening(EventDefinitionBase definition, EventData payload) - { - var d = (EventDefinition)definition; - var p = (ConnectionEventData)payload; - return d.GenerateMessage( - p.Connection.Database, - p.Connection.DataSource); + return null; } /// - /// Logs for the event. + /// Logs for the event. /// /// The diagnostics logger to use. /// The connection. /// The time that the operation was started. - /// The amount of time before the connection was opened. - /// Indicates whether or not this is an async operation. - public static void ConnectionOpened( + /// The cancellation token. + /// A representing the async operation. + public static Task ConnectionOpeningAsync( [NotNull] this IDiagnosticsLogger diagnostics, [NotNull] IRelationalConnection connection, DateTimeOffset startTime, - TimeSpan duration, - bool async) + CancellationToken cancellationToken) { - var definition = RelationalResources.LogOpenedConnection(diagnostics); + var definition = RelationalResources.LogOpeningConnection(diagnostics); + + LogConnectionOpening(diagnostics, connection, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastConnectionOpening( + diagnostics, + connection, + startTime, + definition, + true, + diagnosticSourceEnabled); + + if (interceptor != null) + { + return interceptor.ConnectionOpeningAsync(connection.DbConnection, eventData, null, cancellationToken); + } + } + + return Task.FromResult(null); + } + private static void LogConnectionOpening( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + EventDefinition definition) + { var warningBehavior = definition.GetLogBehavior(diagnostics); if (warningBehavior != WarningBehavior.Ignore) { @@ -1073,95 +1092,155 @@ public static void ConnectionOpened( warningBehavior, connection.DbConnection.Database, connection.DbConnection.DataSource); } + } - if (diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name)) + + private static ConnectionEventData BroadcastConnectionOpening( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + DateTimeOffset startTime, + EventDefinition definition, + bool async, + bool diagnosticSourceEnabled) + { + var eventData = new ConnectionEventData( + definition, + ConnectionOpening, + connection.DbConnection, + connection.Context, + connection.ConnectionId, + async, + startTime); + + if (diagnosticSourceEnabled) { - diagnostics.DiagnosticSource.Write( - definition.EventId.Name, - new ConnectionEndEventData( - definition, - ConnectionOpened, - connection.DbConnection, - connection.ConnectionId, - async, - startTime, - duration)); + diagnostics.DiagnosticSource.Write(definition.EventId.Name, eventData); } + + return eventData; } - private static string ConnectionOpened(EventDefinitionBase definition, EventData payload) + private static string ConnectionOpening(EventDefinitionBase definition, EventData payload) { var d = (EventDefinition)definition; - var p = (ConnectionEndEventData)payload; + var p = (ConnectionEventData)payload; return d.GenerateMessage( p.Connection.Database, p.Connection.DataSource); } /// - /// Logs for the event. + /// Logs for the event. /// /// The diagnostics logger to use. /// The connection. /// The time that the operation was started. - /// Indicates whether or not this is an async operation. - public static void ConnectionClosing( + /// The amount of time before the connection was opened. + public static void ConnectionOpened( [NotNull] this IDiagnosticsLogger diagnostics, [NotNull] IRelationalConnection connection, DateTimeOffset startTime, - bool async) + TimeSpan duration) { - var definition = RelationalResources.LogClosingConnection(diagnostics); + var definition = RelationalResources.LogOpenedConnection(diagnostics); - var warningBehavior = definition.GetLogBehavior(diagnostics); - if (warningBehavior != WarningBehavior.Ignore) + LogConnectionOpened(diagnostics, connection, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) { - definition.Log( + var eventData = BroadcastConnectionOpened( diagnostics, - warningBehavior, - connection.DbConnection.Database, connection.DbConnection.DataSource); - } + connection, + false, + startTime, + duration, + definition, + diagnosticSourceEnabled); - if (diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name)) - { - diagnostics.DiagnosticSource.Write( - definition.EventId.Name, - new ConnectionEventData( - definition, - ConnectionClosing, - connection.DbConnection, - connection.ConnectionId, - false, - startTime)); + interceptor?.ConnectionOpened(connection.DbConnection, eventData); } } - private static string ConnectionClosing(EventDefinitionBase definition, EventData payload) - { - var d = (EventDefinition)definition; - var p = (ConnectionEventData)payload; - return d.GenerateMessage( - p.Connection.Database, - p.Connection.DataSource); - } - /// - /// Logs for the event. + /// Logs for the event. /// /// The diagnostics logger to use. /// The connection. /// The time that the operation was started. - /// The amount of time before the connection was closed. - /// Indicates whether or not this is an async operation. - public static void ConnectionClosed( + /// The amount of time before the connection was opened. + /// The cancellation token. + /// A representing the async operation. + public static Task ConnectionOpenedAsync( [NotNull] this IDiagnosticsLogger diagnostics, [NotNull] IRelationalConnection connection, DateTimeOffset startTime, TimeSpan duration, - bool async) + CancellationToken cancellationToken = default) { - var definition = RelationalResources.LogClosedConnection(diagnostics); + var definition = RelationalResources.LogOpenedConnection(diagnostics); + + LogConnectionOpened(diagnostics, connection, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastConnectionOpened( + diagnostics, + connection, + true, + startTime, + duration, + definition, + diagnosticSourceEnabled); + + if (interceptor != null) + { + return interceptor.ConnectionOpenedAsync(connection.DbConnection, eventData, cancellationToken); + } + } + + return Task.CompletedTask; + } + + private static ConnectionEndEventData BroadcastConnectionOpened( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + bool async, + DateTimeOffset startTime, + TimeSpan duration, + EventDefinition definition, + bool diagnosticSourceEnabled) + { + var eventData = new ConnectionEndEventData( + definition, + ConnectionOpened, + connection.DbConnection, + connection.Context, + connection.ConnectionId, + async, + startTime, + duration); + + if (diagnosticSourceEnabled) + { + diagnostics.DiagnosticSource.Write(definition.EventId.Name, eventData); + } + + return eventData; + } + + private static void LogConnectionOpened( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + EventDefinition definition) + { var warningBehavior = definition.GetLogBehavior(diagnostics); if (warningBehavior != WarningBehavior.Ignore) { @@ -1170,23 +1249,9 @@ public static void ConnectionClosed( warningBehavior, connection.DbConnection.Database, connection.DbConnection.DataSource); } - - if (diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name)) - { - diagnostics.DiagnosticSource.Write( - definition.EventId.Name, - new ConnectionEndEventData( - definition, - ConnectionClosed, - connection.DbConnection, - connection.ConnectionId, - false, - startTime, - duration)); - } } - private static string ConnectionClosed(EventDefinitionBase definition, EventData payload) + private static string ConnectionOpened(EventDefinitionBase definition, EventData payload) { var d = (EventDefinition)definition; var p = (ConnectionEndEventData)payload; @@ -1196,28 +1261,112 @@ private static string ConnectionClosed(EventDefinitionBase definition, EventData } /// - /// Logs for the event. + /// Logs for the event. /// /// The diagnostics logger to use. /// The connection. - /// The exception representing the error. /// The time that the operation was started. - /// The elapsed time before the operation failed. - /// Indicates whether or not this is an async operation. - /// A flag indicating the exception is being handled and so it should be logged at Debug level. - public static void ConnectionError( + /// The result of execution, which may have been modified by an interceptor. + public static InterceptionResult? ConnectionClosing( [NotNull] this IDiagnosticsLogger diagnostics, [NotNull] IRelationalConnection connection, - [NotNull] Exception exception, - DateTimeOffset startTime, - TimeSpan duration, - bool async, - bool logErrorAsDebug) + DateTimeOffset startTime) { - var definition = logErrorAsDebug - ? RelationalResources.LogConnectionErrorAsDebug(diagnostics) - : RelationalResources.LogConnectionError(diagnostics); + var definition = RelationalResources.LogClosingConnection(diagnostics); + + LogConnectionClosing(diagnostics, connection, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastConnectionClosing( + diagnostics, + connection, + startTime, + false, + definition, + diagnosticSourceEnabled); + + return interceptor?.ConnectionClosing(connection.DbConnection, eventData, null); + } + + return null; + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The time that the operation was started. + /// The cancellation token. + /// A representing the async operation. + public static Task ConnectionClosingAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + DateTimeOffset startTime, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogClosingConnection(diagnostics); + + LogConnectionClosing(diagnostics, connection, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastConnectionClosing( + diagnostics, + connection, + startTime, + true, + definition, + diagnosticSourceEnabled); + + if (interceptor != null) + { + return interceptor.ConnectionClosingAsync(connection.DbConnection, eventData, null, cancellationToken); + } + } + + return Task.FromResult(null); + } + + private static ConnectionEventData BroadcastConnectionClosing( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + DateTimeOffset startTime, + bool async, + EventDefinition definition, + bool diagnosticSourceEnabled) + { + var eventData = new ConnectionEventData( + definition, + ConnectionClosing, + connection.DbConnection, + connection.Context, + connection.ConnectionId, + async, + startTime); + + if (diagnosticSourceEnabled) + { + diagnostics.DiagnosticSource.Write(definition.EventId.Name, eventData); + } + return eventData; + } + + private static void LogConnectionClosing( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + EventDefinition definition) + { var warningBehavior = definition.GetLogBehavior(diagnostics); if (warningBehavior != WarningBehavior.Ignore) { @@ -1226,186 +1375,1248 @@ public static void ConnectionError( warningBehavior, connection.DbConnection.Database, connection.DbConnection.DataSource); } + } - if (diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name)) + private static string ConnectionClosing(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (ConnectionEventData)payload; + return d.GenerateMessage( + p.Connection.Database, + p.Connection.DataSource); + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The time that the operation was started. + /// The amount of time before the connection was closed. + public static void ConnectionClosed( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + DateTimeOffset startTime, + TimeSpan duration) + { + var definition = RelationalResources.LogClosedConnection(diagnostics); + + LogConnectionClosed(diagnostics, connection, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) { - diagnostics.DiagnosticSource.Write( - definition.EventId.Name, - new ConnectionErrorEventData( - definition, - ConnectionError, - connection.DbConnection, - connection.ConnectionId, - exception, - async, - startTime, - duration)); + var eventData = BroadcastCollectionClosed( + diagnostics, + connection, + startTime, + duration, + false, + definition, + diagnosticSourceEnabled); + + if (interceptor != null) + { + interceptor.ConnectionClosed(connection.DbConnection, eventData); + } + } + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The time that the operation was started. + /// The amount of time before the connection was closed. + /// The cancellation token. + /// A representing the async operation. + public static Task ConnectionClosedAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + DateTimeOffset startTime, + TimeSpan duration, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogClosedConnection(diagnostics); + + LogConnectionClosed(diagnostics, connection, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastCollectionClosed( + diagnostics, + connection, + startTime, + duration, + true, + definition, + diagnosticSourceEnabled); + + if (interceptor != null) + { + return interceptor.ConnectionClosedAsync(connection.DbConnection, eventData, cancellationToken); + } + } + + return Task.CompletedTask; + } + + private static ConnectionEndEventData BroadcastCollectionClosed( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + DateTimeOffset startTime, + TimeSpan duration, + bool async, + EventDefinition definition, + bool diagnosticSourceEnabled) + { + var eventData = new ConnectionEndEventData( + definition, + ConnectionClosed, + connection.DbConnection, + connection.Context, + connection.ConnectionId, + async, + startTime, + duration); + + if (diagnosticSourceEnabled) + { + diagnostics.DiagnosticSource.Write(definition.EventId.Name, eventData); + } + + return eventData; + } + + private static void LogConnectionClosed( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + EventDefinition definition) + { + var warningBehavior = definition.GetLogBehavior(diagnostics); + if (warningBehavior != WarningBehavior.Ignore) + { + definition.Log( + diagnostics, + warningBehavior, + connection.DbConnection.Database, connection.DbConnection.DataSource); + } + } + + private static string ConnectionClosed(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (ConnectionEndEventData)payload; + return d.GenerateMessage( + p.Connection.Database, + p.Connection.DataSource); + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The exception representing the error. + /// The time that the operation was started. + /// The elapsed time before the operation failed. + /// A flag indicating the exception is being handled and so it should be logged at Debug level. + public static void ConnectionError( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] Exception exception, + DateTimeOffset startTime, + TimeSpan duration, + bool logErrorAsDebug) + { + var definition = logErrorAsDebug + ? RelationalResources.LogConnectionErrorAsDebug(diagnostics) + : RelationalResources.LogConnectionError(diagnostics); + + LogConnectionError(diagnostics, connection, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastConnectionError( + diagnostics, + connection, + exception, + startTime, + duration, + false, + definition, + diagnosticSourceEnabled); + + if (interceptor != null) + { + interceptor.ConnectionFailed(connection.DbConnection, eventData); + } } } - private static string ConnectionError(EventDefinitionBase definition, EventData payload) + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The exception representing the error. + /// The time that the operation was started. + /// The elapsed time before the operation failed. + /// A flag indicating the exception is being handled and so it should be logged at Debug level. + /// The cancellation token. + /// A representing the async operation. + public static Task ConnectionErrorAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] Exception exception, + DateTimeOffset startTime, + TimeSpan duration, + bool logErrorAsDebug, + CancellationToken cancellationToken = default) + { + var definition = logErrorAsDebug + ? RelationalResources.LogConnectionErrorAsDebug(diagnostics) + : RelationalResources.LogConnectionError(diagnostics); + + LogConnectionError(diagnostics, connection, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastConnectionError( + diagnostics, + connection, + exception, + startTime, + duration, + true, + definition, + diagnosticSourceEnabled); + + if (interceptor != null) + { + return interceptor.ConnectionFailedAsync(connection.DbConnection, eventData, cancellationToken); + } + } + + return Task.CompletedTask; + } + + private static ConnectionErrorEventData BroadcastConnectionError( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + Exception exception, + DateTimeOffset startTime, + TimeSpan duration, + bool async, + EventDefinition definition, + bool diagnosticSourceEnabled) + { + var eventData = new ConnectionErrorEventData( + definition, + ConnectionError, + connection.DbConnection, + connection.Context, + connection.ConnectionId, + exception, + async, + startTime, + duration); + + if (diagnosticSourceEnabled) + { + diagnostics.DiagnosticSource.Write(definition.EventId.Name, eventData); + } + + return eventData; + } + + private static void LogConnectionError( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + EventDefinition definition) + { + var warningBehavior = definition.GetLogBehavior(diagnostics); + if (warningBehavior != WarningBehavior.Ignore) + { + definition.Log( + diagnostics, + warningBehavior, + connection.DbConnection.Database, connection.DbConnection.DataSource); + } + } + + private static string ConnectionError(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (ConnectionErrorEventData)payload; + return d.GenerateMessage( + p.Connection.Database, + p.Connection.DataSource); + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction isolation level. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The result of execution, which may have been modified by an interceptor. + public static InterceptionResult? TransactionStarting( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + IsolationLevel isolationLevel, + Guid transactionId, + DateTimeOffset startTime) + { + var definition = RelationalResources.LogBeginningTransaction(diagnostics); + + LogTransactionStarting(diagnostics, isolationLevel, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastTransactionStarting( + diagnostics, + connection, + isolationLevel, + transactionId, + false, + startTime, + definition, + diagnosticSourceEnabled); + + if (interceptor != null) + { + return interceptor.TransactionStarting(connection.DbConnection, eventData, null); + } + } + + return null; + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction isolation level. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The cancellation token. + /// The result of execution, which may have been modified by an interceptor. + public static Task?> TransactionStartingAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + IsolationLevel isolationLevel, + Guid transactionId, + DateTimeOffset startTime, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogBeginningTransaction(diagnostics); + + LogTransactionStarting(diagnostics, isolationLevel, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastTransactionStarting( + diagnostics, + connection, + isolationLevel, + transactionId, + true, + startTime, + definition, + diagnosticSourceEnabled); + + return interceptor?.TransactionStartingAsync(connection.DbConnection, eventData, null, cancellationToken); + } + + return Task.FromResult?>(null); + } + + private static TransactionStartingEventData BroadcastTransactionStarting( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + IsolationLevel isolationLevel, + Guid transactionId, + bool async, + DateTimeOffset startTime, + EventDefinition definition, + bool diagnosticSourceEnabled) + { + var eventData = new TransactionStartingEventData( + definition, + TransactionStarting, + connection.Context, + isolationLevel, + transactionId, + connection.ConnectionId, + async, + startTime); + + if (diagnosticSourceEnabled) + { + diagnostics.DiagnosticSource.Write(definition.EventId.Name, eventData); + } + + return eventData; + } + + private static void LogTransactionStarting( + IDiagnosticsLogger diagnostics, + System.Data.IsolationLevel isolationLevel, + EventDefinition definition) + { + var warningBehavior = definition.GetLogBehavior(diagnostics); + if (warningBehavior != WarningBehavior.Ignore) + { + definition.Log( + diagnostics, + warningBehavior, + isolationLevel.ToString("G")); + } + } + + private static string TransactionStarting(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (TransactionStartingEventData)payload; + return d.GenerateMessage( + p.IsolationLevel.ToString("G")); + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The amount of time before the connection was opened. + /// The result of execution, which may have been modified by an interceptor. + public static DbTransaction TransactionStarted( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + TimeSpan duration) + { + var definition = RelationalResources.LogBeganTransaction(diagnostics); + + LogTransactionStarted(diagnostics, transaction, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastTransactionStarted( + diagnostics, + connection, + transaction, + transactionId, + false, + startTime, + duration, + definition, + diagnosticSourceEnabled); + + return interceptor?.TransactionStarted(connection.DbConnection, eventData, transaction); + } + + return transaction; + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The amount of time before the connection was opened. + /// The cancellation token. + /// The result of execution, which may have been modified by an interceptor. + public static Task TransactionStartedAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + TimeSpan duration, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogBeganTransaction(diagnostics); + + LogTransactionStarted(diagnostics, transaction, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastTransactionStarted( + diagnostics, + connection, + transaction, + transactionId, + true, + startTime, + duration, + definition, + diagnosticSourceEnabled); + + return interceptor?.TransactionStartedAsync(connection.DbConnection, eventData, transaction, cancellationToken); + } + + return Task.FromResult(transaction); + } + + private static TransactionEndEventData BroadcastTransactionStarted( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + DbTransaction transaction, + Guid transactionId, + bool async, + DateTimeOffset startTime, + TimeSpan duration, + EventDefinition definition, + bool diagnosticSourceEnabled) + { + var eventData = new TransactionEndEventData( + definition, + TransactionStarted, + transaction, + connection.Context, + transactionId, + connection.ConnectionId, + async, + startTime, + duration); + + if (diagnosticSourceEnabled) + { + diagnostics.DiagnosticSource.Write(definition.EventId.Name, eventData); + } + + return eventData; + } + + private static void LogTransactionStarted( + IDiagnosticsLogger diagnostics, + DbTransaction transaction, + EventDefinition definition) + { + var warningBehavior = definition.GetLogBehavior(diagnostics); + if (warningBehavior != WarningBehavior.Ignore) + { + definition.Log( + diagnostics, + warningBehavior, + transaction.IsolationLevel.ToString("G")); + } + } + + private static string TransactionStarted(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (TransactionEndEventData)payload; + return d.GenerateMessage(p.Transaction.IsolationLevel.ToString("G")); + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The result of execution, which may have been modified by an interceptor. + public static DbTransaction TransactionUsed( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime) + { + var definition = RelationalResources.LogUsingTransaction(diagnostics); + + LogTransactionUsed(diagnostics, transaction, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcasstTransactionUsed( + diagnostics, + connection, + transaction, + transactionId, + false, + startTime, + definition, + diagnosticSourceEnabled); + + return interceptor?.TransactionUsed(connection.DbConnection, eventData, transaction); + } + + return transaction; + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The cancellation token. + /// The result of execution, which may have been modified by an interceptor. + public static Task TransactionUsedAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogUsingTransaction(diagnostics); + + LogTransactionUsed(diagnostics, transaction, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcasstTransactionUsed( + diagnostics, + connection, + transaction, + transactionId, + true, + startTime, + definition, + diagnosticSourceEnabled); + + return interceptor?.TransactionUsedAsync(connection.DbConnection, eventData, transaction, cancellationToken); + } + + return Task.FromResult(transaction); + } + + private static TransactionEventData BroadcasstTransactionUsed( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + DbTransaction transaction, + Guid transactionId, + bool async, + DateTimeOffset startTime, + EventDefinition definition, + bool diagnosticSourceEnabled) + { + var eventData = new TransactionEventData( + definition, + TransactionUsed, + transaction, + connection.Context, + transactionId, + connection.ConnectionId, + async, + startTime); + + if (diagnosticSourceEnabled) + { + diagnostics.DiagnosticSource.Write(definition.EventId.Name, eventData); + } + + return eventData; + } + + private static void LogTransactionUsed( + IDiagnosticsLogger diagnostics, + DbTransaction transaction, + EventDefinition definition) + { + var warningBehavior = definition.GetLogBehavior(diagnostics); + if (warningBehavior != WarningBehavior.Ignore) + { + definition.Log( + diagnostics, + warningBehavior, + transaction.IsolationLevel.ToString("G")); + } + } + + private static string TransactionUsed(EventDefinitionBase definition, EventData payload) + { + var d = (EventDefinition)definition; + var p = (TransactionEventData)payload; + return d.GenerateMessage( + p.Transaction.IsolationLevel.ToString("G")); + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The result of execution, which may have been modified by an interceptor. + public static InterceptionResult? TransactionCommitting( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime) + { + var definition = RelationalResources.LogCommittingTransaction(diagnostics); + + LogTransactionCommitting(diagnostics, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastTransactionCommitting( + diagnostics, + connection, + transaction, + transactionId, + startTime, + definition, + false, + diagnosticSourceEnabled); + + return interceptor?.TransactionCommitting(transaction, eventData, null); + } + + return null; + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The cancellation token. + /// A representing the async operation. + public static Task TransactionCommittingAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogCommittingTransaction(diagnostics); + + LogTransactionCommitting(diagnostics, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastTransactionCommitting( + diagnostics, + connection, + transaction, + transactionId, + startTime, + definition, + true, + diagnosticSourceEnabled); + + return interceptor?.TransactionCommittingAsync(transaction, eventData, null, cancellationToken); + } + + return Task.FromResult(null); + } + + private static TransactionEventData BroadcastTransactionCommitting( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + EventDefinition definition, + bool async, + bool diagnosticSourceEnabled) + { + var eventData = new TransactionEventData( + definition, + (d, p) => ((EventDefinition)d).GenerateMessage(), + transaction, + connection.Context, + transactionId, + connection.ConnectionId, + async, + startTime); + + if (diagnosticSourceEnabled) + { + diagnostics.DiagnosticSource.Write(definition.EventId.Name, eventData); + } + + return eventData; + } + + private static void LogTransactionCommitting( + IDiagnosticsLogger diagnostics, + EventDefinition definition) + { + var warningBehavior = definition.GetLogBehavior(diagnostics); + if (warningBehavior != WarningBehavior.Ignore) + { + definition.Log(diagnostics, warningBehavior); + } + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The elapsed time from when the operation was started. + public static void TransactionCommitted( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + TimeSpan duration) + { + var definition = RelationalResources.LogCommittedTransaction(diagnostics); + + LogTransactionCommitted(diagnostics, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastTransactionCommitted( + diagnostics, + connection, + transaction, + transactionId, + startTime, + duration, + definition, + false, + diagnosticSourceEnabled); + + interceptor?.TransactionCommitted(transaction, eventData); + } + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The elapsed time from when the operation was started. + /// The cancellation token. + /// A representing the async operation. + public static Task TransactionCommittedAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + TimeSpan duration, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogCommittedTransaction(diagnostics); + + LogTransactionCommitted(diagnostics, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastTransactionCommitted( + diagnostics, + connection, + transaction, + transactionId, + startTime, + duration, + definition, + true, + diagnosticSourceEnabled); + + return interceptor?.TransactionCommittedAsync(transaction, eventData, cancellationToken); + } + + return Task.CompletedTask; + } + + private static TransactionEndEventData BroadcastTransactionCommitted( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + TimeSpan duration, + EventDefinition definition, + bool async, + bool diagnosticSourceEnabled) + { + var eventData = new TransactionEndEventData( + definition, + (d, p) => ((EventDefinition)d).GenerateMessage(), + transaction, + connection.Context, + transactionId, + connection.ConnectionId, + async, + startTime, + duration); + + if (diagnosticSourceEnabled) + { + diagnostics.DiagnosticSource.Write(definition.EventId.Name, eventData); + } + + return eventData; + } + + private static void LogTransactionCommitted( + IDiagnosticsLogger diagnostics, + EventDefinition definition) + { + var warningBehavior = definition.GetLogBehavior(diagnostics); + if (warningBehavior != WarningBehavior.Ignore) + { + definition.Log(diagnostics, warningBehavior); + } + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The elapsed time from when the operation was started. + public static void TransactionRolledBack( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + TimeSpan duration) + { + var definition = RelationalResources.LogRolledBackTransaction(diagnostics); + + LogTransactionRolledBack(diagnostics, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastTransactionRolledBack( + diagnostics, + connection, + transaction, + transactionId, + startTime, + duration, + definition, + false, + diagnosticSourceEnabled); + + interceptor?.TransactionRolledBack(transaction, eventData); + } + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The connection. + /// The transaction. + /// The correlation ID associated with the . + /// The time that the operation was started. + /// The elapsed time from when the operation was started. + /// The cancellation token. + /// A representing the async operation. + public static Task TransactionRolledBackAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] IRelationalConnection connection, + [NotNull] DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + TimeSpan duration, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogRolledBackTransaction(diagnostics); + + LogTransactionRolledBack(diagnostics, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastTransactionRolledBack( + diagnostics, + connection, + transaction, + transactionId, + startTime, + duration, + definition, + true, + diagnosticSourceEnabled); + + return interceptor?.TransactionRolledBackAsync(transaction, eventData, cancellationToken); + } + + return Task.CompletedTask; + } + + private static TransactionEndEventData BroadcastTransactionRolledBack( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + DbTransaction transaction, + Guid transactionId, + DateTimeOffset startTime, + TimeSpan duration, + EventDefinition definition, + bool async, + bool diagnosticSourceEnabled) + { + var eventData = new TransactionEndEventData( + definition, + (d, p) => ((EventDefinition)d).GenerateMessage(), + transaction, + connection.Context, + transactionId, + connection.ConnectionId, + @async, + startTime, + duration); + + if (diagnosticSourceEnabled) + { + diagnostics.DiagnosticSource.Write(definition.EventId.Name, eventData); + } + + return eventData; + } + + private static void LogTransactionRolledBack( + IDiagnosticsLogger diagnostics, + EventDefinition definition) { - var d = (EventDefinition)definition; - var p = (ConnectionErrorEventData)payload; - return d.GenerateMessage( - p.Connection.Database, - p.Connection.DataSource); + var warningBehavior = definition.GetLogBehavior(diagnostics); + if (warningBehavior != WarningBehavior.Ignore) + { + definition.Log(diagnostics, warningBehavior); + } } /// - /// Logs for the event. + /// Logs for the event. /// /// The diagnostics logger to use. /// The connection. /// The transaction. /// The correlation ID associated with the . /// The time that the operation was started. - public static void TransactionStarted( + /// The result of execution, which may have been modified by an interceptor. + public static InterceptionResult? TransactionRollingBack( [NotNull] this IDiagnosticsLogger diagnostics, [NotNull] IRelationalConnection connection, [NotNull] DbTransaction transaction, Guid transactionId, DateTimeOffset startTime) { - var definition = RelationalResources.LogBeginningTransaction(diagnostics); + var definition = RelationalResources.LogRollingBackTransaction(diagnostics); - var warningBehavior = definition.GetLogBehavior(diagnostics); - if (warningBehavior != WarningBehavior.Ignore) + LogTransactionRollingBack(diagnostics, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) { - definition.Log( + var eventData = BroadcastTransactionRollingBack( diagnostics, - warningBehavior, - transaction.IsolationLevel.ToString("G")); - } + connection, + transaction, + transactionId, + startTime, + definition, + false, + diagnosticSourceEnabled); - if (diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name)) - { - diagnostics.DiagnosticSource.Write( - definition.EventId.Name, - new TransactionEventData( - definition, - TransactionStarted, - transaction, - transactionId, - connection.ConnectionId, - startTime)); + return interceptor?.TransactionRollingBack(transaction, eventData, null); } - } - private static string TransactionStarted(EventDefinitionBase definition, EventData payload) - { - var d = (EventDefinition)definition; - var p = (TransactionEventData)payload; - return d.GenerateMessage( - p.Transaction.IsolationLevel.ToString("G")); + return null; } /// - /// Logs for the event. + /// Logs for the event. /// /// The diagnostics logger to use. /// The connection. /// The transaction. /// The correlation ID associated with the . /// The time that the operation was started. - public static void TransactionUsed( + /// The cancellation token. + /// A representing the async operation. + public static Task TransactionRollingBackAsync( [NotNull] this IDiagnosticsLogger diagnostics, [NotNull] IRelationalConnection connection, [NotNull] DbTransaction transaction, Guid transactionId, - DateTimeOffset startTime) + DateTimeOffset startTime, + CancellationToken cancellationToken = default) { - var definition = RelationalResources.LogUsingTransaction(diagnostics); + var definition = RelationalResources.LogRollingBackTransaction(diagnostics); - var warningBehavior = definition.GetLogBehavior(diagnostics); - if (warningBehavior != WarningBehavior.Ignore) + LogTransactionRollingBack(diagnostics, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) { - definition.Log( + var eventData = BroadcastTransactionRollingBack( diagnostics, - warningBehavior, - transaction.IsolationLevel.ToString("G")); - } + connection, + transaction, + transactionId, + startTime, + definition, + true, + diagnosticSourceEnabled); - if (diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name)) - { - diagnostics.DiagnosticSource.Write( - definition.EventId.Name, - new TransactionEventData( - definition, - TransactionUsed, - transaction, - transactionId, - connection.ConnectionId, - startTime)); + return interceptor?.TransactionRollingBackAsync(transaction, eventData, null, cancellationToken); } - } - private static string TransactionUsed(EventDefinitionBase definition, EventData payload) - { - var d = (EventDefinition)definition; - var p = (TransactionEventData)payload; - return d.GenerateMessage( - p.Transaction.IsolationLevel.ToString("G")); + return Task.FromResult(null); } - /// - /// Logs for the event. - /// - /// The diagnostics logger to use. - /// The connection. - /// The transaction. - /// The correlation ID associated with the . - /// The time that the operation was started. - /// The elapsed time from when the operation was started. - public static void TransactionCommitted( - [NotNull] this IDiagnosticsLogger diagnostics, - [NotNull] IRelationalConnection connection, - [NotNull] DbTransaction transaction, + private static TransactionEventData BroadcastTransactionRollingBack( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + DbTransaction transaction, Guid transactionId, DateTimeOffset startTime, - TimeSpan duration) + EventDefinition definition, + bool async, + bool diagnosticSourceEnabled) { - var definition = RelationalResources.LogCommittingTransaction(diagnostics); + var eventData = new TransactionEventData( + definition, + (d, p) => ((EventDefinition)d).GenerateMessage(), + transaction, + connection.Context, + transactionId, + connection.ConnectionId, + @async, + startTime); - var warningBehavior = definition.GetLogBehavior(diagnostics); - if (warningBehavior != WarningBehavior.Ignore) + if (diagnosticSourceEnabled) { - definition.Log(diagnostics, warningBehavior); + diagnostics.DiagnosticSource.Write(definition.EventId.Name, eventData); } - if (diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name)) + return eventData; + } + + private static void LogTransactionRollingBack( + IDiagnosticsLogger diagnostics, + EventDefinition definition) + { + var warningBehavior = definition.GetLogBehavior(diagnostics); + if (warningBehavior != WarningBehavior.Ignore) { - diagnostics.DiagnosticSource.Write( - definition.EventId.Name, - new TransactionEndEventData( - definition, - (d, p) => ((EventDefinition)d).GenerateMessage(), - transaction, - transactionId, - connection.ConnectionId, - startTime, - duration)); + definition.Log(diagnostics, warningBehavior); } } /// - /// Logs for the event. + /// Logs for the event. /// /// The diagnostics logger to use. /// The connection. /// The transaction. /// The correlation ID associated with the . /// The time that the operation was started. - /// The elapsed time from when the operation was started. - public static void TransactionRolledBack( + public static void TransactionDisposed( [NotNull] this IDiagnosticsLogger diagnostics, [NotNull] IRelationalConnection connection, [NotNull] DbTransaction transaction, Guid transactionId, - DateTimeOffset startTime, - TimeSpan duration) + DateTimeOffset startTime) { - var definition = RelationalResources.LogRollingbackTransaction(diagnostics); + var definition = RelationalResources.LogDisposingTransaction(diagnostics); var warningBehavior = definition.GetLogBehavior(diagnostics); if (warningBehavior != WarningBehavior.Ignore) @@ -1417,51 +2628,63 @@ public static void TransactionRolledBack( { diagnostics.DiagnosticSource.Write( definition.EventId.Name, - new TransactionEndEventData( + new TransactionEventData( definition, (d, p) => ((EventDefinition)d).GenerateMessage(), transaction, + connection.Context, transactionId, connection.ConnectionId, - startTime, - duration)); + false, + startTime)); } } /// - /// Logs for the event. + /// Logs for the event. /// /// The diagnostics logger to use. /// The connection. /// The transaction. /// The correlation ID associated with the . + /// The action being taken. + /// The exception that represents the error. /// The time that the operation was started. - public static void TransactionDisposed( + /// The elapsed time from when the operation was started. + public static void TransactionError( [NotNull] this IDiagnosticsLogger diagnostics, [NotNull] IRelationalConnection connection, [NotNull] DbTransaction transaction, Guid transactionId, - DateTimeOffset startTime) + [NotNull] string action, + [NotNull] Exception exception, + DateTimeOffset startTime, + TimeSpan duration) { - var definition = RelationalResources.LogDisposingTransaction(diagnostics); + var definition = RelationalResources.LogTransactionError(diagnostics); - var warningBehavior = definition.GetLogBehavior(diagnostics); - if (warningBehavior != WarningBehavior.Ignore) - { - definition.Log(diagnostics, warningBehavior); - } + LogTransactionErrror(diagnostics, exception, definition); - if (diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name)) + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) { - diagnostics.DiagnosticSource.Write( - definition.EventId.Name, - new TransactionEventData( - definition, - (d, p) => ((EventDefinition)d).GenerateMessage(), - transaction, - transactionId, - connection.ConnectionId, - startTime)); + var eventData = BroadcastTransactionError( + diagnostics, + connection, + transaction, + transactionId, + action, + exception, + startTime, + duration, + definition, + false, + diagnosticSourceEnabled); + + interceptor?.TransactionFailed(transaction, eventData); } } @@ -1476,7 +2699,9 @@ public static void TransactionDisposed( /// The exception that represents the error. /// The time that the operation was started. /// The elapsed time from when the operation was started. - public static void TransactionError( + /// The cancellation token. + /// A representing the async operation. + public static Task TransactionErrorAsync( [NotNull] this IDiagnosticsLogger diagnostics, [NotNull] IRelationalConnection connection, [NotNull] DbTransaction transaction, @@ -1484,30 +2709,81 @@ public static void TransactionError( [NotNull] string action, [NotNull] Exception exception, DateTimeOffset startTime, - TimeSpan duration) + TimeSpan duration, + CancellationToken cancellationToken = default) { var definition = RelationalResources.LogTransactionError(diagnostics); - var warningBehavior = definition.GetLogBehavior(diagnostics); - if (warningBehavior != WarningBehavior.Ignore) + LogTransactionErrror(diagnostics, exception, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) { - definition.Log(diagnostics, warningBehavior, exception); + var eventData = BroadcastTransactionError( + diagnostics, + connection, + transaction, + transactionId, + action, + exception, + startTime, + duration, + definition, + true, + diagnosticSourceEnabled); + + return interceptor?.TransactionFailedAsync(transaction, eventData, cancellationToken); } - if (diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name)) + return Task.CompletedTask; + } + + private static TransactionErrorEventData BroadcastTransactionError( + IDiagnosticsLogger diagnostics, + IRelationalConnection connection, + DbTransaction transaction, + Guid transactionId, + string action, + Exception exception, + DateTimeOffset startTime, + TimeSpan duration, + EventDefinition definition, + bool async, + bool diagnosticSourceEnabled) + { + var eventData = new TransactionErrorEventData( + definition, + (d, p) => ((EventDefinition)d).GenerateMessage(), + transaction, + connection.Context, + transactionId, + connection.ConnectionId, + async, + action, + exception, + startTime, + duration); + + if (diagnosticSourceEnabled) { - diagnostics.DiagnosticSource.Write( - definition.EventId.Name, - new TransactionErrorEventData( - definition, - (d, p) => ((EventDefinition)d).GenerateMessage(), - transaction, - connection.ConnectionId, - transactionId, - action, - exception, - startTime, - duration)); + diagnostics.DiagnosticSource.Write(definition.EventId.Name, eventData); + } + + return eventData; + } + + private static void LogTransactionErrror( + IDiagnosticsLogger diagnostics, + Exception exception, + EventDefinition definition) + { + var warningBehavior = definition.GetLogBehavior(diagnostics); + if (warningBehavior != WarningBehavior.Ignore) + { + definition.Log(diagnostics, warningBehavior, exception); } } @@ -1538,6 +2814,7 @@ public static void AmbientTransactionWarning( definition, (d, p) => ((EventDefinition)d).GenerateMessage(), connection.DbConnection, + connection.Context, connection.ConnectionId, false, startTime)); @@ -1640,7 +2917,8 @@ private static string ExplicitTransactionEnlisted(EventDefinitionBase definition /// The number of records that were read. /// The time that the operation was started. /// The elapsed time from when the operation was started. - public static void DataReaderDisposing( + /// The result of execution, which may have been modified by an interceptor. + public static InterceptionResult? DataReaderDisposing( [NotNull] this IDiagnosticsLogger diagnostics, [NotNull] IRelationalConnection connection, [NotNull] DbCommand command, @@ -1659,22 +2937,36 @@ public static void DataReaderDisposing( definition.Log(diagnostics, warningBehavior); } - if (diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name)) + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = diagnostics.Interceptors?.Resolve(); + + if (interceptor != null + || diagnosticSourceEnabled) { - diagnostics.DiagnosticSource.Write( - definition.EventId.Name, - new DataReaderDisposingEventData( - definition, - (d, p) => ((EventDefinition)d).GenerateMessage(), - command, - dataReader, - commandId, - connection.ConnectionId, - recordsAffected, - readCount, - startTime, - duration)); + var eventData = new DataReaderDisposingEventData( + definition, + (d, p) => ((EventDefinition)d).GenerateMessage(), + command, + dataReader, + connection.Context, + commandId, + connection.ConnectionId, + recordsAffected, + readCount, + startTime, + duration); + + if (diagnosticSourceEnabled) + { + diagnostics.DiagnosticSource.Write( + definition.EventId.Name, + eventData); + } + + return interceptor?.DataReaderDisposing(command, eventData, null); } + + return null; } /// diff --git a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs index ce9a4e8bcf3..699c31683f3 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggingDefinitions.cs @@ -97,6 +97,15 @@ public abstract class RelationalLoggingDefinitions : LoggingDefinitions [EntityFrameworkInternal] public EventDefinitionBase LogBeginningTransaction; + /// + /// 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] + public EventDefinitionBase LogBeganTransaction; + /// /// 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 @@ -122,7 +131,25 @@ public abstract class RelationalLoggingDefinitions : LoggingDefinitions /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - public EventDefinitionBase LogRollingbackTransaction; + public EventDefinitionBase LogRollingBackTransaction; + + /// + /// 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] + public EventDefinitionBase LogCommittedTransaction; + + /// + /// 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] + public EventDefinitionBase LogRolledBackTransaction; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to diff --git a/src/EFCore.Relational/Diagnostics/TransactionEndEventData.cs b/src/EFCore.Relational/Diagnostics/TransactionEndEventData.cs index 8de782cca87..c53bf7c4515 100644 --- a/src/EFCore.Relational/Diagnostics/TransactionEndEventData.cs +++ b/src/EFCore.Relational/Diagnostics/TransactionEndEventData.cs @@ -19,34 +19,28 @@ public class TransactionEndEventData : TransactionEventData /// /// The event definition. /// A delegate that generates a log message for this event. - /// - /// The . - /// - /// - /// A correlation ID that identifies the Entity Framework transaction being used. - /// - /// - /// A correlation ID that identifies the instance being used. - /// - /// - /// The start time of this event. - /// - /// - /// The duration this event. - /// + /// The . + /// The currently being used, or null if not known. + /// A correlation ID that identifies the Entity Framework transaction being used. + /// A correlation ID that identifies the instance being used. + /// Indicates whether or not the transaction is being used asynchronously. + /// The start time of this event. + /// The duration this event. public TransactionEndEventData( [NotNull] EventDefinitionBase eventDefinition, [NotNull] Func messageGenerator, [NotNull] DbTransaction transaction, + [CanBeNull] DbContext context, Guid transactionId, Guid connectionId, + bool async, DateTimeOffset startTime, TimeSpan duration) - : base(eventDefinition, messageGenerator, transaction, transactionId, connectionId, startTime) + : base(eventDefinition, messageGenerator, transaction, context, transactionId, connectionId, async, startTime) => Duration = duration; /// - /// The duration this event. + /// The duration of this event. /// public virtual TimeSpan Duration { get; } } diff --git a/src/EFCore.Relational/Diagnostics/TransactionErrorEventData.cs b/src/EFCore.Relational/Diagnostics/TransactionErrorEventData.cs index f0671199ac0..ed7047d2d52 100644 --- a/src/EFCore.Relational/Diagnostics/TransactionErrorEventData.cs +++ b/src/EFCore.Relational/Diagnostics/TransactionErrorEventData.cs @@ -19,38 +19,28 @@ public class TransactionErrorEventData : TransactionEndEventData, IErrorEventDat /// /// The event definition. /// A delegate that generates a log message for this event. - /// - /// The . - /// - /// - /// A correlation ID that identifies the Entity Framework transaction being used. - /// - /// - /// A correlation ID that identifies the instance being used. - /// - /// - /// One of "Commit" or "Rollback". - /// - /// - /// The exception that was thrown when the transaction failed. - /// - /// - /// The start time of this event. - /// - /// - /// The duration this event. - /// + /// The . + /// The currently being used, or null if not known. + /// A correlation ID that identifies the Entity Framework transaction being used. + /// A correlation ID that identifies the instance being used. + /// Indicates whether or not the transaction is being used asynchronously. + /// One of "Commit" or "Rollback". + /// The exception that was thrown when the transaction failed. + /// The start time of this event. + /// The duration this event. public TransactionErrorEventData( [NotNull] EventDefinitionBase eventDefinition, [NotNull] Func messageGenerator, [NotNull] DbTransaction transaction, + [CanBeNull] DbContext context, Guid transactionId, Guid connectionId, + bool async, [NotNull] string action, [NotNull] Exception exception, DateTimeOffset startTime, TimeSpan duration) - : base(eventDefinition, messageGenerator, transaction, transactionId, connectionId, startTime, duration) + : base(eventDefinition, messageGenerator, transaction, context, transactionId, connectionId, async, startTime, duration) { Action = action; Exception = exception; diff --git a/src/EFCore.Relational/Diagnostics/TransactionEventData.cs b/src/EFCore.Relational/Diagnostics/TransactionEventData.cs index 3f7f741d895..7951c7c2aac 100644 --- a/src/EFCore.Relational/Diagnostics/TransactionEventData.cs +++ b/src/EFCore.Relational/Diagnostics/TransactionEventData.cs @@ -12,42 +12,39 @@ namespace Microsoft.EntityFrameworkCore.Diagnostics /// The event payload base class for /// transaction events. /// - public class TransactionEventData : EventData + public class TransactionEventData : DbContextEventData { /// /// Constructs the event payload. /// /// The event definition. /// A delegate that generates a log message for this event. - /// - /// The . - /// - /// - /// A correlation ID that identifies the Entity Framework transaction being used. - /// - /// - /// A correlation ID that identifies the instance being used. - /// - /// - /// The start time of this event. - /// + /// The . + /// The currently in use, or null if not known. + /// A correlation ID that identifies the Entity Framework transaction being used. + /// A correlation ID that identifies the instance being used. + /// Indicates whether or not the transaction is being used asynchronously. + /// The start time of this event. public TransactionEventData( [NotNull] EventDefinitionBase eventDefinition, [NotNull] Func messageGenerator, [NotNull] DbTransaction transaction, + [CanBeNull] DbContext context, Guid transactionId, Guid connectionId, + bool async, DateTimeOffset startTime) - : base(eventDefinition, messageGenerator) + : base(eventDefinition, messageGenerator, context) { Transaction = transaction; TransactionId = transactionId; ConnectionId = connectionId; + IsAsync = async; StartTime = startTime; } /// - /// The . + /// The , or null if it has not yet been created. /// public virtual DbTransaction Transaction { get; } @@ -61,6 +58,11 @@ public TransactionEventData( /// public virtual Guid ConnectionId { get; } + /// + /// Indicates whether or not the transaction is being used asynchronously. + /// + public virtual bool IsAsync { get; } + /// /// The start time of this event. /// diff --git a/src/EFCore.Relational/Diagnostics/TransactionStartingEventData.cs b/src/EFCore.Relational/Diagnostics/TransactionStartingEventData.cs new file mode 100644 index 00000000000..56f0cb37cb0 --- /dev/null +++ b/src/EFCore.Relational/Diagnostics/TransactionStartingEventData.cs @@ -0,0 +1,72 @@ +// 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.Data; +using System.Data.Common; +using System.Diagnostics; +using JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Diagnostics +{ + /// + /// The event payload base class for + /// transaction events. + /// + public class TransactionStartingEventData : DbContextEventData + { + /// + /// Constructs the event payload. + /// + /// The event definition. + /// A delegate that generates a log message for this event. + /// The currently in use, or null if not known. + /// The transaction isolation level. + /// A correlation ID that identifies the Entity Framework transaction being used. + /// A correlation ID that identifies the instance being used. + /// Indicates whether or not the transaction is being used asynchronously. + /// The start time of this event. + public TransactionStartingEventData( + [NotNull] EventDefinitionBase eventDefinition, + [NotNull] Func messageGenerator, + [CanBeNull] DbContext context, + IsolationLevel isolationLevel, + Guid transactionId, + Guid connectionId, + bool async, + DateTimeOffset startTime) + : base(eventDefinition, messageGenerator, context) + { + IsolationLevel = isolationLevel; + TransactionId = transactionId; + ConnectionId = connectionId; + IsAsync = async; + StartTime = startTime; + } + + /// + /// The transaction isolation level. + /// + public virtual IsolationLevel IsolationLevel { get; } + + /// + /// A correlation ID that identifies the Entity Framework transaction being used. + /// + public virtual Guid TransactionId { get; } + + /// + /// A correlation ID that identifies the instance being used. + /// + public virtual Guid ConnectionId { get; } + + /// + /// Indicates whether or not the transaction is being used asynchronously. + /// + public virtual bool IsAsync { get; } + + /// + /// The start time of this event. + /// + public virtual DateTimeOffset StartTime { get; } + } +} diff --git a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs index 11b2a3bc919..2e2573b6088 100644 --- a/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs +++ b/src/EFCore.Relational/Extensions/RelationalDatabaseFacadeExtensions.cs @@ -684,9 +684,7 @@ public static void OpenConnection([NotNull] this DatabaseFacade databaseFacade) /// /// The for the context. /// A to observe while waiting for the task to complete. - /// - /// A task that represents the asynchronous operation. - /// + /// A task that represents the asynchronous operation. public static Task OpenConnectionAsync( [NotNull] this DatabaseFacade databaseFacade, CancellationToken cancellationToken = default) @@ -701,6 +699,15 @@ public static Task OpenConnectionAsync( public static void CloseConnection([NotNull] this DatabaseFacade databaseFacade) => GetFacadeDependencies(databaseFacade).RelationalConnection.Close(); + /// + /// Closes the underlying . + /// + /// The for the context. + /// A to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. + public static Task CloseConnectionAsync([NotNull] this DatabaseFacade databaseFacade, CancellationToken cancellationToken = default) + => GetFacadeDependencies(databaseFacade).RelationalConnection.CloseAsync(cancellationToken); + /// /// Starts a new transaction with a given . /// @@ -761,6 +768,28 @@ public static IDbContextTransaction UseTransaction( return relationalTransactionManager.UseTransaction(transaction); } + /// + /// Sets the to be used by database operations on the . + /// + /// The for the context. + /// The to use. + /// A token to observe while waiting for the task to complete. + /// A containing the for the given transaction. + public static Task UseTransactionAsync( + [NotNull] this DatabaseFacade databaseFacade, + [CanBeNull] DbTransaction transaction, + CancellationToken cancellationToken = default) + { + var transactionManager = GetTransactionManager(databaseFacade); + + if (!(transactionManager is IRelationalTransactionManager relationalTransactionManager)) + { + throw new InvalidOperationException(RelationalStrings.RelationalNotInUse); + } + + return relationalTransactionManager.UseTransactionAsync(transaction, cancellationToken); + } + /// /// /// Sets the timeout (in seconds) to use for commands executed with this . diff --git a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs index 0c2c1dc75bf..da4f4013d8c 100644 --- a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -69,7 +70,6 @@ public static readonly IDictionary RelationalServi { typeof(IRelationalCommandBuilderFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(IRawSqlCommandBuilder), new ServiceCharacteristics(ServiceLifetime.Singleton) }, { typeof(ICommandBatchPreparer), new ServiceCharacteristics(ServiceLifetime.Scoped) }, - { typeof(IRelationalInterceptors), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IModificationCommandBatchFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IMigrationsModelDiffer), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IMigrationsSqlGenerator), new ServiceCharacteristics(ServiceLifetime.Scoped) }, @@ -81,7 +81,8 @@ public static readonly IDictionary RelationalServi { typeof(IRelationalDatabaseCreator), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IHistoryRepository), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(INamedConnectionStringResolver), new ServiceCharacteristics(ServiceLifetime.Scoped) }, - { typeof(IDbCommandInterceptor), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) }, + { typeof(IInterceptorResolver), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) }, + { typeof(IInterceptor), new ServiceCharacteristics(ServiceLifetime.Scoped, multipleRegistrations: true) }, { typeof(IRelationalTypeMappingSourcePlugin), new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) }, // New Query Pipeline @@ -157,8 +158,9 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd(p => p.GetService()); TryAdd(); - TryAdd(); - TryAdd(p => p.GetService()); + TryAdd(); + TryAdd(); + TryAdd(); // New Query pipeline TryAdd(); @@ -181,7 +183,6 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() .AddDependencySingleton() .AddDependencySingleton() .AddDependencySingleton() - .AddDependencyScoped() .AddDependencyScoped() .AddDependencyScoped() .AddDependencyScoped() diff --git a/src/EFCore.Relational/Infrastructure/RelationalDbContextOptionsBuilder.cs b/src/EFCore.Relational/Infrastructure/RelationalDbContextOptionsBuilder.cs index 330fe290725..0ae25f53545 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalDbContextOptionsBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalDbContextOptionsBuilder.cs @@ -108,20 +108,6 @@ public virtual TBuilder ExecutionStrategy( => WithOption( e => (TExtension)e.WithExecutionStrategyFactory(Check.NotNull(getExecutionStrategy, nameof(getExecutionStrategy)))); - /// - /// - /// Configures the context to use the given . - /// - /// - /// Note that only a single can be registered. - /// Use to combine multiple interceptors into one. - /// - /// - /// The interceptor to use. - public virtual TBuilder CommandInterceptor( - [NotNull] IDbCommandInterceptor interceptor) - => WithOption(e => (TExtension)e.WithCommandInterceptor(Check.NotNull(interceptor, nameof(interceptor)))); - /// /// Sets an option by cloning the extension used to store the settings. This ensures the builder /// does not modify options that are already in use elsewhere. diff --git a/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs b/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs index 05e79b0fb95..f0ddc14b698 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs @@ -38,7 +38,6 @@ public abstract class RelationalOptionsExtension : IDbContextOptionsExtension private string _migrationsHistoryTableName; private string _migrationsHistoryTableSchema; private Func _executionStrategyFactory; - private IDbCommandInterceptor _commandInterceptor; /// /// Creates a new set of options with everything set to default values. @@ -65,7 +64,6 @@ protected RelationalOptionsExtension([NotNull] RelationalOptionsExtension copyFr _migrationsHistoryTableName = copyFrom._migrationsHistoryTableName; _migrationsHistoryTableSchema = copyFrom._migrationsHistoryTableSchema; _executionStrategyFactory = copyFrom._executionStrategyFactory; - _commandInterceptor = copyFrom._commandInterceptor; } /// @@ -309,27 +307,6 @@ public virtual RelationalOptionsExtension WithExecutionStrategyFactory( return clone; } - /// - /// The registered , if any. - /// - public virtual IDbCommandInterceptor CommandInterceptor => _commandInterceptor; - - /// - /// 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 RelationalOptionsExtension WithCommandInterceptor( - [CanBeNull] IDbCommandInterceptor commandInterceptor) - { - var clone = Clone(); - - clone._commandInterceptor = commandInterceptor; - - return clone; - } - /// /// Finds an existing registered on the given options /// or throws if none has been registered. This is typically used to find some relational diff --git a/src/EFCore.Relational/Migrations/Internal/MigrationCommandExecutor.cs b/src/EFCore.Relational/Migrations/Internal/MigrationCommandExecutor.cs index f6b4579c4e0..da2c2b7d303 100644 --- a/src/EFCore.Relational/Migrations/Internal/MigrationCommandExecutor.cs +++ b/src/EFCore.Relational/Migrations/Internal/MigrationCommandExecutor.cs @@ -134,7 +134,7 @@ public virtual async Task ExecuteNonQueryAsync( } finally { - connection.Close(); + await connection.CloseAsync(cancellationToken); } } } diff --git a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs index 602bcae5745..c9d58ecf7cd 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs +++ b/src/EFCore.Relational/Properties/RelationalStrings.Designer.cs @@ -1,4 +1,4 @@ -// +// using System; using System.Reflection; @@ -772,6 +772,30 @@ public static EventDefinition LogBeginningTransaction([NotNull] IDiagnos { definition = LazyInitializer.EnsureInitialized( ref ((RelationalLoggingDefinitions)logger.Definitions).LogBeginningTransaction, + () => new EventDefinition( + logger.Options, + RelationalEventId.TransactionStarting, + LogLevel.Debug, + "RelationalEventId.TransactionStarting", + level => LoggerMessage.Define( + level, + RelationalEventId.TransactionStarting, + _resourceManager.GetString("LogBeginningTransaction")))); + } + + return (EventDefinition)definition; + } + + /// + /// Began transaction with isolation level '{isolationLevel}'. + /// + public static EventDefinition LogBeganTransaction([NotNull] IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogBeganTransaction; + if (definition == null) + { + definition = LazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogBeganTransaction, () => new EventDefinition( logger.Options, RelationalEventId.TransactionStarted, @@ -780,7 +804,7 @@ public static EventDefinition LogBeginningTransaction([NotNull] IDiagnos level => LoggerMessage.Define( level, RelationalEventId.TransactionStarted, - _resourceManager.GetString("LogBeginningTransaction")))); + _resourceManager.GetString("LogBeganTransaction")))); } return (EventDefinition)definition; @@ -820,6 +844,30 @@ public static EventDefinition LogCommittingTransaction([NotNull] IDiagnosticsLog { definition = LazyInitializer.EnsureInitialized( ref ((RelationalLoggingDefinitions)logger.Definitions).LogCommittingTransaction, + () => new EventDefinition( + logger.Options, + RelationalEventId.TransactionCommitting, + LogLevel.Debug, + "RelationalEventId.TransactionCommitting", + level => LoggerMessage.Define( + level, + RelationalEventId.TransactionCommitting, + _resourceManager.GetString("LogCommittingTransaction")))); + } + + return (EventDefinition)definition; + } + + /// + /// Committed transaction. + /// + public static EventDefinition LogCommittedTransaction([NotNull] IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogCommittedTransaction; + if (definition == null) + { + definition = LazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogCommittedTransaction, () => new EventDefinition( logger.Options, RelationalEventId.TransactionCommitted, @@ -828,7 +876,7 @@ public static EventDefinition LogCommittingTransaction([NotNull] IDiagnosticsLog level => LoggerMessage.Define( level, RelationalEventId.TransactionCommitted, - _resourceManager.GetString("LogCommittingTransaction")))); + _resourceManager.GetString("LogCommittedTransaction")))); } return (EventDefinition)definition; @@ -837,13 +885,37 @@ public static EventDefinition LogCommittingTransaction([NotNull] IDiagnosticsLog /// /// Rolling back transaction. /// - public static EventDefinition LogRollingbackTransaction([NotNull] IDiagnosticsLogger logger) + public static EventDefinition LogRollingBackTransaction([NotNull] IDiagnosticsLogger logger) + { + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogRollingBackTransaction; + if (definition == null) + { + definition = LazyInitializer.EnsureInitialized( + ref ((RelationalLoggingDefinitions)logger.Definitions).LogRollingBackTransaction, + () => new EventDefinition( + logger.Options, + RelationalEventId.TransactionRollingBack, + LogLevel.Debug, + "RelationalEventId.TransactionRollingBack", + level => LoggerMessage.Define( + level, + RelationalEventId.TransactionRollingBack, + _resourceManager.GetString("LogRollingBackTransaction")))); + } + + return (EventDefinition)definition; + } + + /// + /// Rolled back transaction. + /// + public static EventDefinition LogRolledBackTransaction([NotNull] IDiagnosticsLogger logger) { - var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogRollingbackTransaction; + var definition = ((RelationalLoggingDefinitions)logger.Definitions).LogRolledBackTransaction; if (definition == null) { definition = LazyInitializer.EnsureInitialized( - ref ((RelationalLoggingDefinitions)logger.Definitions).LogRollingbackTransaction, + ref ((RelationalLoggingDefinitions)logger.Definitions).LogRolledBackTransaction, () => new EventDefinition( logger.Options, RelationalEventId.TransactionRolledBack, @@ -852,7 +924,7 @@ public static EventDefinition LogRollingbackTransaction([NotNull] IDiagnosticsLo level => LoggerMessage.Define( level, RelationalEventId.TransactionRolledBack, - _resourceManager.GetString("LogRollingbackTransaction")))); + _resourceManager.GetString("LogRolledBackTransaction")))); } return (EventDefinition)definition; diff --git a/src/EFCore.Relational/Properties/RelationalStrings.resx b/src/EFCore.Relational/Properties/RelationalStrings.resx index 81c96afd259..9d409350f3e 100644 --- a/src/EFCore.Relational/Properties/RelationalStrings.resx +++ b/src/EFCore.Relational/Properties/RelationalStrings.resx @@ -1,17 +1,17 @@  - @@ -177,20 +177,32 @@ An error occurred using the connection to database '{database}' on server '{server}'. Error RelationalEventId.ConnectionError string string + + Began transaction with isolation level '{isolationLevel}'. + Debug RelationalEventId.TransactionStarted string + Beginning transaction with isolation level '{isolationLevel}'. - Debug RelationalEventId.TransactionStarted string + Debug RelationalEventId.TransactionStarting string Using an existing transaction with isolation level '{isolationLevel}'. Debug RelationalEventId.TransactionUsed string + Committing transaction. + Debug RelationalEventId.TransactionCommitting + + Committing transaction. Debug RelationalEventId.TransactionCommitted - + Rolling back transaction. + Debug RelationalEventId.TransactionRollingBack + + + Rolled back transaction. Debug RelationalEventId.TransactionRolledBack diff --git a/src/EFCore.Relational/Storage/IRelationalConnection.cs b/src/EFCore.Relational/Storage/IRelationalConnection.cs index a10bccb5b61..9fd673e34d5 100644 --- a/src/EFCore.Relational/Storage/IRelationalConnection.cs +++ b/src/EFCore.Relational/Storage/IRelationalConnection.cs @@ -36,6 +36,11 @@ public interface IRelationalConnection : IRelationalTransactionManager, IDisposa /// DbConnection DbConnection { get; } + /// + /// The currently in use, or null if not known. + /// + DbContext Context { get; } + /// /// Gets the connection identifier. /// @@ -72,6 +77,18 @@ public interface IRelationalConnection : IRelationalTransactionManager, IDisposa /// True if the underlying connection was actually closed; false otherwise. bool Close(); + /// + /// Closes the connection to the database. + /// + /// + /// A to observe while waiting for the task to complete. + /// + /// + /// A task that represents the asynchronous operation, with a value of true if the connection + /// was actually closed. + /// + Task CloseAsync(CancellationToken cancellationToken = default); + /// /// Gets a value indicating whether the multiple active result sets feature is enabled. /// diff --git a/src/EFCore.Relational/Storage/IRelationalTransactionFactory.cs b/src/EFCore.Relational/Storage/IRelationalTransactionFactory.cs index de246501883..859b29658ab 100644 --- a/src/EFCore.Relational/Storage/IRelationalTransactionFactory.cs +++ b/src/EFCore.Relational/Storage/IRelationalTransactionFactory.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Data.Common; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -28,6 +29,7 @@ public interface IRelationalTransactionFactory /// /// The connection to the database. /// The underlying . + /// The unique correlation ID for this transaction. /// The logger to write to. /// /// A value indicating whether the transaction is owned by this class (i.e. if it can be disposed when this class is disposed). @@ -36,6 +38,7 @@ public interface IRelationalTransactionFactory RelationalTransaction Create( [NotNull] IRelationalConnection connection, [NotNull] DbTransaction transaction, + Guid transactionId, [NotNull] IDiagnosticsLogger logger, bool transactionOwned); } diff --git a/src/EFCore.Relational/Storage/IRelationalTransactionManager.cs b/src/EFCore.Relational/Storage/IRelationalTransactionManager.cs index 812bf4d7797..15e2bb19ea1 100644 --- a/src/EFCore.Relational/Storage/IRelationalTransactionManager.cs +++ b/src/EFCore.Relational/Storage/IRelationalTransactionManager.cs @@ -39,18 +39,24 @@ public interface IRelationalTransactionManager : IDbContextTransactionManager /// /// The isolation level to use for the transaction. /// A to observe while waiting for the task to complete. - /// - /// A task that represents the asynchronous operation. The task result contains the newly created transaction. - /// + /// A task that represents the asynchronous operation. The task result contains the newly created transaction. Task BeginTransactionAsync(IsolationLevel isolationLevel, CancellationToken cancellationToken = default); /// /// Specifies an existing to be used for database operations. /// /// The transaction to be used. - /// - /// An instance of that wraps the provided transaction. - /// + /// An instance of that wraps the provided transaction. IDbContextTransaction UseTransaction([CanBeNull] DbTransaction transaction); + + /// + /// Specifies an existing to be used for database operations. + /// + /// The transaction to be used. + /// A to observe while waiting for the task to complete. + /// An instance of that wraps the provided transaction. + Task UseTransactionAsync([CanBeNull] + DbTransaction transaction, + CancellationToken cancellationToken = default); } } diff --git a/src/EFCore.Relational/Storage/RelationalConnection.cs b/src/EFCore.Relational/Storage/RelationalConnection.cs index bbf54afdb49..0d955f00c89 100644 --- a/src/EFCore.Relational/Storage/RelationalConnection.cs +++ b/src/EFCore.Relational/Storage/RelationalConnection.cs @@ -49,6 +49,8 @@ protected RelationalConnection([NotNull] RelationalConnectionDependencies depend { Check.NotNull(dependencies, nameof(dependencies)); + Context = dependencies.CurrentContext.Context; + Dependencies = dependencies; var relationalOptions = RelationalOptionsExtension.Extract(dependencies.ContextOptions); @@ -81,6 +83,11 @@ protected RelationalConnection([NotNull] RelationalConnectionDependencies depend /// public virtual Guid ConnectionId { get; } = Guid.NewGuid(); + /// + /// The currently in use, or null if not known. + /// + public virtual DbContext Context { get; } + /// /// Parameter object containing service dependencies. /// @@ -199,7 +206,6 @@ public virtual async Task BeginTransactionAsync(Cancellat /// /// The isolation level to use for the transaction. /// The newly created transaction. - [NotNull] // ReSharper disable once RedundantNameQualifier public virtual IDbContextTransaction BeginTransaction(System.Data.IsolationLevel isolationLevel) { @@ -207,7 +213,28 @@ public virtual IDbContextTransaction BeginTransaction(System.Data.IsolationLevel EnsureNoTransactions(); - return BeginTransactionWithNoPreconditions(isolationLevel); + var transactionId = Guid.NewGuid(); + var startTime = DateTimeOffset.UtcNow; + var stopwatch = Stopwatch.StartNew(); + + var interceptionResult = Dependencies.TransactionLogger.TransactionStarting( + this, + isolationLevel, + transactionId, + startTime); + + var dbTransaction = interceptionResult.HasValue + ? interceptionResult.Value.Result + : DbConnection.BeginTransaction(isolationLevel); + + dbTransaction = Dependencies.TransactionLogger.TransactionStarted( + this, + dbTransaction, + transactionId, + startTime, + stopwatch.Elapsed); + + return CreateRelationalTransaction(dbTransaction, transactionId, true); } /// @@ -218,7 +245,6 @@ public virtual IDbContextTransaction BeginTransaction(System.Data.IsolationLevel /// /// A task that represents the asynchronous operation. The task result contains the newly created transaction. /// - [NotNull] public virtual async Task BeginTransactionAsync( // ReSharper disable once RedundantNameQualifier System.Data.IsolationLevel isolationLevel, @@ -228,7 +254,30 @@ public virtual async Task BeginTransactionAsync( EnsureNoTransactions(); - return BeginTransactionWithNoPreconditions(isolationLevel); + var transactionId = Guid.NewGuid(); + var startTime = DateTimeOffset.UtcNow; + var stopwatch = Stopwatch.StartNew(); + + var interceptionResult = await Dependencies.TransactionLogger.TransactionStartingAsync( + this, + isolationLevel, + transactionId, + startTime, + cancellationToken); + + var dbTransaction = interceptionResult.HasValue + ? interceptionResult.Value.Result + : DbConnection.BeginTransaction(isolationLevel); // Use BeginTransactionAsync when available + + dbTransaction = await Dependencies.TransactionLogger.TransactionStartedAsync( + this, + dbTransaction, + transactionId, + startTime, + stopwatch.Elapsed, + cancellationToken); + + return CreateRelationalTransaction(dbTransaction, transactionId, true); } private void EnsureNoTransactions() @@ -249,23 +298,36 @@ private void EnsureNoTransactions() } } - // ReSharper disable once RedundantNameQualifier - private IDbContextTransaction BeginTransactionWithNoPreconditions(System.Data.IsolationLevel isolationLevel) - { - var dbTransaction = DbConnection.BeginTransaction(isolationLevel); - - CurrentTransaction + private IDbContextTransaction CreateRelationalTransaction(DbTransaction transaction, Guid transactionId, bool transactionOwned) + => CurrentTransaction = Dependencies.RelationalTransactionFactory.Create( this, - dbTransaction, + transaction, + transactionId, Dependencies.TransactionLogger, - transactionOwned: true); + transactionOwned: transactionOwned); - Dependencies.TransactionLogger.TransactionStarted( - this, - dbTransaction, - CurrentTransaction.TransactionId, - DateTimeOffset.UtcNow); + /// + /// Specifies an existing to be used for database operations. + /// + /// The transaction to be used. + public virtual IDbContextTransaction UseTransaction(DbTransaction transaction) + { + if (ShouldUseTransaction(transaction)) + { + Open(); + + var transactionId = Guid.NewGuid(); + + transaction = Dependencies.TransactionLogger.TransactionUsed( + this, + // ReSharper disable once AssignNullToNotNullAttribute + transaction, + transactionId, + DateTimeOffset.UtcNow); + + CurrentTransaction = CreateRelationalTransaction(transaction, transactionId, transactionOwned: false); + } return CurrentTransaction; } @@ -274,37 +336,49 @@ private IDbContextTransaction BeginTransactionWithNoPreconditions(System.Data.Is /// Specifies an existing to be used for database operations. /// /// The transaction to be used. - public virtual IDbContextTransaction UseTransaction(DbTransaction transaction) + /// A to observe while waiting for the task to complete. + /// An instance of that wraps the provided transaction. + public virtual async Task UseTransactionAsync( + DbTransaction transaction, + CancellationToken cancellationToken = default) { - if (transaction == null) - { - if (CurrentTransaction != null) - { - CurrentTransaction = null; - } - } - else + if (ShouldUseTransaction(transaction)) { - EnsureNoTransactions(); + await OpenAsync(cancellationToken); - Open(); + var transactionId = Guid.NewGuid(); - CurrentTransaction = Dependencies.RelationalTransactionFactory.Create( + transaction = await Dependencies.TransactionLogger.TransactionUsedAsync( this, + // ReSharper disable once AssignNullToNotNullAttribute transaction, - Dependencies.TransactionLogger, - transactionOwned: false); + transactionId, + DateTimeOffset.UtcNow, + cancellationToken); - Dependencies.TransactionLogger.TransactionUsed( - this, - transaction, - CurrentTransaction.TransactionId, - DateTimeOffset.UtcNow); + CurrentTransaction = CreateRelationalTransaction(transaction, transactionId, transactionOwned: false); } return CurrentTransaction; } + private bool ShouldUseTransaction(DbTransaction transaction) + { + if (transaction == null) + { + if (CurrentTransaction != null) + { + CurrentTransaction = null; + } + + return false; + } + + EnsureNoTransactions(); + + return true; + } + /// /// Commits all changes made to the database in the current transaction. /// @@ -416,30 +490,20 @@ private void OpenDbConnection(bool errorsExpected) var startTime = DateTimeOffset.UtcNow; var stopwatch = Stopwatch.StartNew(); - Dependencies.ConnectionLogger.ConnectionOpening( - this, - startTime, - async: false); + var interceptionResult = Dependencies.ConnectionLogger.ConnectionOpening(this, startTime); try { - DbConnection.Open(); + if (interceptionResult == null) + { + DbConnection.Open(); + } - Dependencies.ConnectionLogger.ConnectionOpened( - this, - startTime, - stopwatch.Elapsed, - async: false); + Dependencies.ConnectionLogger.ConnectionOpened(this, startTime, stopwatch.Elapsed); } catch (Exception e) { - Dependencies.ConnectionLogger.ConnectionError( - this, - e, - startTime, - stopwatch.Elapsed, - async: false, - logErrorAsDebug: errorsExpected); + Dependencies.ConnectionLogger.ConnectionError(this, e, startTime, stopwatch.Elapsed, errorsExpected); throw; } @@ -455,30 +519,27 @@ private async Task OpenDbConnectionAsync(bool errorsExpected, CancellationToken var startTime = DateTimeOffset.UtcNow; var stopwatch = Stopwatch.StartNew(); - Dependencies.ConnectionLogger.ConnectionOpening( - this, - startTime, - async: true); + var interceptionResult + = await Dependencies.ConnectionLogger.ConnectionOpeningAsync(this, startTime, cancellationToken); try { - await DbConnection.OpenAsync(cancellationToken); + if (interceptionResult == null) + { + await DbConnection.OpenAsync(cancellationToken); + } - Dependencies.ConnectionLogger.ConnectionOpened( - this, - startTime, - stopwatch.Elapsed, - async: true); + await Dependencies.ConnectionLogger.ConnectionOpenedAsync(this, startTime, stopwatch.Elapsed, cancellationToken); } catch (Exception e) { - Dependencies.ConnectionLogger.ConnectionError( + await Dependencies.ConnectionLogger.ConnectionErrorAsync( this, e, startTime, stopwatch.Elapsed, - async: true, - logErrorAsDebug: errorsExpected); + errorsExpected, + cancellationToken); throw; } @@ -548,37 +609,90 @@ public virtual bool Close() { var wasClosed = false; - if ((_openedCount == 0 - || _openedCount > 0 - && --_openedCount == 0) - && _openedInternally) + if (ShouldClose()) { - ClearTransactions(clearAmbient: false); + if (DbConnection.State != ConnectionState.Closed) + { + var startTime = DateTimeOffset.UtcNow; + var stopwatch = Stopwatch.StartNew(); + + var interceptionResult = Dependencies.ConnectionLogger.ConnectionClosing(this, startTime); + + try + { + if (interceptionResult == null) + { + DbConnection.Close(); + } + + wasClosed = true; + + Dependencies.ConnectionLogger.ConnectionClosed(this, startTime, stopwatch.Elapsed); + } + catch (Exception e) + { + Dependencies.ConnectionLogger.ConnectionError(this, e, startTime, stopwatch.Elapsed, false); + + throw; + } + } + + _openedInternally = false; + } + return wasClosed; + } + + /// + /// Closes the connection to the database. + /// + /// + /// A to observe while waiting for the task to complete. + /// + /// + /// A task that represents the asynchronous operation, with a value of true if the connection + /// was actually closed. + /// + public virtual async Task CloseAsync(CancellationToken cancellationToken = default) + { + var wasClosed = false; + + if (ShouldClose()) + { if (DbConnection.State != ConnectionState.Closed) { var startTime = DateTimeOffset.UtcNow; var stopwatch = Stopwatch.StartNew(); - Dependencies.ConnectionLogger.ConnectionClosing(this, startTime, async: false); + var interceptionResult = await Dependencies.ConnectionLogger.ConnectionClosingAsync( + this, + startTime, + cancellationToken); try { - DbConnection.Close(); + if (interceptionResult == null) + { + DbConnection.Close(); // Since no CloseAsync yet + } wasClosed = true; - Dependencies.ConnectionLogger.ConnectionClosed(this, startTime, stopwatch.Elapsed, async: false); + await Dependencies.ConnectionLogger.ConnectionClosedAsync( + this, + startTime, + stopwatch.Elapsed, + cancellationToken); } catch (Exception e) { - Dependencies.ConnectionLogger.ConnectionError( + await Dependencies.ConnectionLogger.ConnectionErrorAsync( this, e, startTime, stopwatch.Elapsed, - async: false, - logErrorAsDebug: false); + false, + cancellationToken); throw; } @@ -590,6 +704,21 @@ public virtual bool Close() return wasClosed; } + private bool ShouldClose() + { + if ((_openedCount == 0 + || _openedCount > 0 + && --_openedCount == 0) + && _openedInternally) + { + ClearTransactions(clearAmbient: false); + + return true; + } + + return false; + } + /// /// Gets a value indicating whether the multiple active result sets feature is enabled. /// diff --git a/src/EFCore.Relational/Storage/RelationalConnectionDependencies.cs b/src/EFCore.Relational/Storage/RelationalConnectionDependencies.cs index 39f5c110888..26224d84207 100644 --- a/src/EFCore.Relational/Storage/RelationalConnectionDependencies.cs +++ b/src/EFCore.Relational/Storage/RelationalConnectionDependencies.cs @@ -53,24 +53,28 @@ public sealed class RelationalConnectionDependencies /// The logger to which connection messages will be written. /// A service for resolving a connection string from a name. /// A service for creating instances. + /// Contains the instance currently in use. public RelationalConnectionDependencies( [NotNull] IDbContextOptions contextOptions, [NotNull] IDiagnosticsLogger transactionLogger, [NotNull] IDiagnosticsLogger connectionLogger, [NotNull] INamedConnectionStringResolver connectionStringResolver, - [NotNull] IRelationalTransactionFactory relationalTransactionFactory) + [NotNull] IRelationalTransactionFactory relationalTransactionFactory, + [NotNull] ICurrentDbContext currentContext) { Check.NotNull(contextOptions, nameof(contextOptions)); Check.NotNull(transactionLogger, nameof(transactionLogger)); Check.NotNull(connectionLogger, nameof(connectionLogger)); Check.NotNull(connectionStringResolver, nameof(connectionStringResolver)); Check.NotNull(relationalTransactionFactory, nameof(relationalTransactionFactory)); + Check.NotNull(currentContext, nameof(currentContext)); ContextOptions = contextOptions; TransactionLogger = transactionLogger; ConnectionLogger = connectionLogger; ConnectionStringResolver = connectionStringResolver; RelationalTransactionFactory = relationalTransactionFactory; + CurrentContext = currentContext; } /// @@ -98,54 +102,93 @@ public RelationalConnectionDependencies( /// public IRelationalTransactionFactory RelationalTransactionFactory { get; } + /// + /// Contains the instance currently in use. + /// + public ICurrentDbContext CurrentContext { get; } + /// /// Clones this dependency parameter object with one service replaced. /// - /// - /// A replacement for the current dependency of this type. - /// + /// A replacement for the current dependency of this type. /// A new parameter object with the given service replaced. public RelationalConnectionDependencies With([NotNull] IDbContextOptions contextOptions) - => new RelationalConnectionDependencies(contextOptions, TransactionLogger, ConnectionLogger, ConnectionStringResolver, RelationalTransactionFactory); + => new RelationalConnectionDependencies( + contextOptions, + TransactionLogger, + ConnectionLogger, + ConnectionStringResolver, + RelationalTransactionFactory, + CurrentContext); /// /// Clones this dependency parameter object with one service replaced. /// - /// - /// A replacement for the current dependency of this type. - /// + /// A replacement for the current dependency of this type. /// A new parameter object with the given service replaced. public RelationalConnectionDependencies With([NotNull] IDiagnosticsLogger connectionLogger) - => new RelationalConnectionDependencies(ContextOptions, TransactionLogger, connectionLogger, ConnectionStringResolver, RelationalTransactionFactory); + => new RelationalConnectionDependencies( + ContextOptions, + TransactionLogger, + connectionLogger, + ConnectionStringResolver, + RelationalTransactionFactory, + CurrentContext); /// /// Clones this dependency parameter object with one service replaced. /// - /// - /// A replacement for the current dependency of this type. - /// + /// A replacement for the current dependency of this type. /// A new parameter object with the given service replaced. public RelationalConnectionDependencies With([NotNull] IDiagnosticsLogger transactionLogger) - => new RelationalConnectionDependencies(ContextOptions, transactionLogger, ConnectionLogger, ConnectionStringResolver, RelationalTransactionFactory); + => new RelationalConnectionDependencies( + ContextOptions, + transactionLogger, + ConnectionLogger, + ConnectionStringResolver, + RelationalTransactionFactory, + CurrentContext); /// /// Clones this dependency parameter object with one service replaced. /// - /// - /// A replacement for the current dependency of this type. - /// + /// A replacement for the current dependency of this type. /// A new parameter object with the given service replaced. public RelationalConnectionDependencies With([NotNull] INamedConnectionStringResolver connectionStringResolver) - => new RelationalConnectionDependencies(ContextOptions, TransactionLogger, ConnectionLogger, connectionStringResolver, RelationalTransactionFactory); + => new RelationalConnectionDependencies( + ContextOptions, + TransactionLogger, + ConnectionLogger, + connectionStringResolver, + RelationalTransactionFactory, + CurrentContext); /// /// Clones this dependency parameter object with one service replaced. /// - /// - /// A replacement for the current dependency of this type. - /// + /// A replacement for the current dependency of this type. /// A new parameter object with the given service replaced. public RelationalConnectionDependencies With([NotNull] IRelationalTransactionFactory relationalTransactionFactory) - => new RelationalConnectionDependencies(ContextOptions, TransactionLogger, ConnectionLogger, ConnectionStringResolver, relationalTransactionFactory); + => new RelationalConnectionDependencies( + ContextOptions, + TransactionLogger, + ConnectionLogger, + ConnectionStringResolver, + relationalTransactionFactory, + CurrentContext); + + /// + /// Clones this dependency parameter object with one service replaced. + /// + /// A replacement for the current dependency of this type. + /// A new parameter object with the given service replaced. + public RelationalConnectionDependencies With([NotNull] ICurrentDbContext currentContext) + => new RelationalConnectionDependencies( + ContextOptions, + TransactionLogger, + ConnectionLogger, + ConnectionStringResolver, + RelationalTransactionFactory, + currentContext); } } diff --git a/src/EFCore.Relational/Storage/RelationalDataReader.cs b/src/EFCore.Relational/Storage/RelationalDataReader.cs index 1f591cb7730..91134cb13c7 100644 --- a/src/EFCore.Relational/Storage/RelationalDataReader.cs +++ b/src/EFCore.Relational/Storage/RelationalDataReader.cs @@ -102,11 +102,12 @@ public virtual void Dispose() { if (!_disposed) { + InterceptionResult? interceptionResult = null; try { _reader.Close(); // can throw - _logger?.DataReaderDisposing( + interceptionResult = _logger?.DataReaderDisposing( _connection, _command, _reader, @@ -120,10 +121,13 @@ public virtual void Dispose() { _disposed = true; - _reader.Dispose(); - _command.Parameters.Clear(); - _command.Dispose(); - _connection.Close(); + if (interceptionResult == null) + { + _reader.Dispose(); + _command.Parameters.Clear(); + _command.Dispose(); + _connection.Close(); + } } } } diff --git a/src/EFCore.Relational/Storage/RelationalTransaction.cs b/src/EFCore.Relational/Storage/RelationalTransaction.cs index 0de39d76cc2..8621e8a8fdd 100644 --- a/src/EFCore.Relational/Storage/RelationalTransaction.cs +++ b/src/EFCore.Relational/Storage/RelationalTransaction.cs @@ -4,6 +4,8 @@ using System; using System.Data.Common; using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -33,6 +35,7 @@ public class RelationalTransaction : IDbContextTransaction, IInfrastructure /// The connection to the database. /// The underlying . + /// The correlation ID for the transaction. /// The logger to write to. /// /// A value indicating whether the transaction is owned by this class (i.e. if it can be disposed when this class is disposed). @@ -40,6 +43,7 @@ public class RelationalTransaction : IDbContextTransaction, IInfrastructure logger, bool transactionOwned) { @@ -53,6 +57,7 @@ public RelationalTransaction( } Connection = connection; + TransactionId = transactionId; _dbTransaction = transaction; Logger = logger; @@ -73,7 +78,7 @@ public RelationalTransaction( /// A correlation ID that allows this transaction to be identified and /// correlated across multiple database calls. /// - public virtual Guid TransactionId { get; } = Guid.NewGuid(); + public virtual Guid TransactionId { get; } /// /// Commits all changes made to the database in the current transaction. @@ -85,7 +90,16 @@ public virtual void Commit() try { - _dbTransaction.Commit(); + var interceptionResult = Logger.TransactionCommitting( + Connection, + _dbTransaction, + TransactionId, + startTime); + + if (interceptionResult == null) + { + _dbTransaction.Commit(); + } Logger.TransactionCommitted( Connection, @@ -104,6 +118,7 @@ public virtual void Commit() e, startTime, stopwatch.Elapsed); + throw; } @@ -120,7 +135,16 @@ public virtual void Rollback() try { - _dbTransaction.Rollback(); + var interceptionResult = Logger.TransactionRollingBack( + Connection, + _dbTransaction, + TransactionId, + startTime); + + if (interceptionResult == null) + { + _dbTransaction.Rollback(); + } Logger.TransactionRolledBack( Connection, @@ -139,6 +163,107 @@ public virtual void Rollback() e, startTime, stopwatch.Elapsed); + + throw; + } + + ClearTransaction(); + } + + /// + /// Commits all changes made to the database in the current transaction asynchronously. + /// + /// The cancellation token. + /// A representing the asynchronous operation. + public virtual async Task CommitAsync(CancellationToken cancellationToken = default) + { + var startTime = DateTimeOffset.UtcNow; + var stopwatch = Stopwatch.StartNew(); + + try + { + var interceptionResult = await Logger.TransactionCommittingAsync( + Connection, + _dbTransaction, + TransactionId, + startTime, + cancellationToken); + + if (interceptionResult == null) + { + _dbTransaction.Commit(); + } + + await Logger.TransactionCommittedAsync( + Connection, + _dbTransaction, + TransactionId, + startTime, + stopwatch.Elapsed, + cancellationToken); + } + catch (Exception e) + { + await Logger.TransactionErrorAsync( + Connection, + _dbTransaction, + TransactionId, + "Commit", + e, + startTime, + stopwatch.Elapsed, + cancellationToken); + + throw; + } + + ClearTransaction(); + } + + /// + /// Discards all changes made to the database in the current transaction asynchronously. + /// + /// The cancellation token. + /// A representing the asynchronous operation. + public virtual async Task RollbackAsync(CancellationToken cancellationToken = default) + { + var startTime = DateTimeOffset.UtcNow; + var stopwatch = Stopwatch.StartNew(); + + try + { + var interceptionResult = await Logger.TransactionRollingBackAsync( + Connection, + _dbTransaction, + TransactionId, + startTime, + cancellationToken); + + if (interceptionResult == null) + { + _dbTransaction.Rollback(); // Use RollbackAsync when available + } + + await Logger.TransactionRolledBackAsync( + Connection, + _dbTransaction, + TransactionId, + startTime, + stopwatch.Elapsed, + cancellationToken); + } + catch (Exception e) + { + await Logger.TransactionErrorAsync( + Connection, + _dbTransaction, + TransactionId, + "Rollback", + e, + startTime, + stopwatch.Elapsed, + cancellationToken); + throw; } diff --git a/src/EFCore.Relational/Storage/RelationalTransactionFactory.cs b/src/EFCore.Relational/Storage/RelationalTransactionFactory.cs index bb5de6ad9a1..a8e51dee0c5 100644 --- a/src/EFCore.Relational/Storage/RelationalTransactionFactory.cs +++ b/src/EFCore.Relational/Storage/RelationalTransactionFactory.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Data.Common; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics; @@ -45,6 +46,7 @@ public RelationalTransactionFactory([NotNull] RelationalTransactionFactoryDepend /// /// The connection to the database. /// The underlying . + /// The unique correlation ID for this transaction. /// The logger to write to. /// /// A value indicating whether the transaction is owned by this class (i.e. if it can be disposed when this class is disposed). @@ -53,8 +55,9 @@ public RelationalTransactionFactory([NotNull] RelationalTransactionFactoryDepend public virtual RelationalTransaction Create( IRelationalConnection connection, DbTransaction transaction, + Guid transactionId, IDiagnosticsLogger logger, bool transactionOwned) - => new RelationalTransaction(connection, transaction, logger, transactionOwned); + => new RelationalTransaction(connection, transaction, transactionId, logger, transactionOwned); } } diff --git a/src/EFCore.Relational/Update/Internal/BatchExecutor.cs b/src/EFCore.Relational/Update/Internal/BatchExecutor.cs index fd6b8712a94..5413a9f0359 100644 --- a/src/EFCore.Relational/Update/Internal/BatchExecutor.cs +++ b/src/EFCore.Relational/Update/Internal/BatchExecutor.cs @@ -165,7 +165,7 @@ private async Task ExecuteAsync( } else { - connection.Close(); + await connection.CloseAsync(cancellationToken); } } diff --git a/src/EFCore.SqlServer/Storage/Internal/SqlServerDatabaseCreator.cs b/src/EFCore.SqlServer/Storage/Internal/SqlServerDatabaseCreator.cs index 41ecf151122..f6e76c1839d 100644 --- a/src/EFCore.SqlServer/Storage/Internal/SqlServerDatabaseCreator.cs +++ b/src/EFCore.SqlServer/Storage/Internal/SqlServerDatabaseCreator.cs @@ -239,7 +239,7 @@ private Task ExistsAsync(bool retryOnNotExists, CancellationToken cancella { await _connection.OpenAsync(ct, errorsExpected: true); - _connection.Close(); + await _connection.CloseAsync(); } return true; diff --git a/src/EFCore/DbContextOptionsBuilder.cs b/src/EFCore/DbContextOptionsBuilder.cs index f2819d1766b..26642a4a789 100644 --- a/src/EFCore/DbContextOptionsBuilder.cs +++ b/src/EFCore/DbContextOptionsBuilder.cs @@ -2,6 +2,7 @@ // 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.ComponentModel; using System.Linq; using JetBrains.Annotations; @@ -76,7 +77,7 @@ public DbContextOptionsBuilder([NotNull] DbContextOptions options) /// will not be run. /// /// - /// If setting an externally created model should be called first. + /// If setting an externally created model should be called first. /// /// /// The model to be used. @@ -296,6 +297,58 @@ public virtual DbContextOptionsBuilder ReplaceService where TImplementation : TService => WithOption(e => e.WithReplacedService(typeof(TService), typeof(TImplementation))); + /// + /// + /// Adds instances to those registered on the context. + /// + /// + /// Interceptors can be used to view, change, or suppress operations taken by Entity Framework. + /// See the specific implementations of for details. For example, 'IDbCommandInterceptor'. + /// + /// + /// A single interceptor instance can implement multiple different interceptor interfaces. I will be registered as + /// an interceptor for all interfaces that it implements. + /// + /// + /// Extensions can also register multiple s in the internal service provider. + /// If both injected and application interceptors are found, then the injected interceptors are run in the + /// order that they are resolved from the service provider, and then the application interceptors are run + /// in the order that they were added to the context. + /// + /// + /// Calling this method multiple times will result in all interceptors in every call being added to the context. + /// Interceptors added in a previous call are not overriden by interceptors added in a later call. + /// + /// + /// The interceptors to add. + /// The same builder instance so that multiple calls can be chained. + public virtual DbContextOptionsBuilder AddInterceptors([NotNull] IEnumerable interceptors) + => WithOption(e => e.WithInterceptors(Check.NotNull(interceptors, nameof(interceptors)))); + + /// + /// + /// Adds instances to those registered on the context. + /// + /// + /// Interceptors can be used to view, change, or suppress operations taken by Entity Framework. + /// See the specific implementations of for details. For example, 'IDbCommandInterceptor'. + /// + /// + /// Extensions can also register multiple s in the internal service provider. + /// If both injected and application interceptors are found, then the injected interceptors are run in the + /// order that they are resolved from the service provider, and then the application interceptors are run + /// in the order that they were added to the context. + /// + /// + /// Calling this method multiple times will result in all interceptors in every call being added to the context. + /// Interceptors added in a previous call are not overriden by interceptors added in a later call. + /// + /// + /// The interceptors to add. + /// The same builder instance so that multiple calls can be chained. + public virtual DbContextOptionsBuilder AddInterceptors([NotNull] params IInterceptor[] interceptors) + => AddInterceptors((IEnumerable)interceptors); + /// /// /// Adds the given extension to the options. If an existing extension of the same type already exists, it will be replaced. diff --git a/src/EFCore/Diagnostics/IInterceptor.cs b/src/EFCore/Diagnostics/IInterceptor.cs new file mode 100644 index 00000000000..840beec2b5a --- /dev/null +++ b/src/EFCore/Diagnostics/IInterceptor.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. + +namespace Microsoft.EntityFrameworkCore.Diagnostics +{ + /// + /// + /// The base interface for all Entity Framework interceptors. + /// + /// + /// Interceptors can be used to view, change, or suppress operations taken by Entity Framework. + /// See the specific implementations of this interface for details. For example, 'IDbCommandInterceptor'. + /// + /// + /// Use + /// to register application interceptors. + /// + /// + /// Extensions can also register multiple s in the internal service provider. + /// If both injected and application interceptors are found, then the injected interceptors are run in the + /// order that they are resolved from the service provider, and then the application interceptors are run + /// in the order that they were added to the context. + /// + /// + public interface IInterceptor + { + } +} diff --git a/src/EFCore/Diagnostics/IInterceptorResolver.cs b/src/EFCore/Diagnostics/IInterceptorResolver.cs new file mode 100644 index 00000000000..94db6a84d8e --- /dev/null +++ b/src/EFCore/Diagnostics/IInterceptorResolver.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.Collections.Generic; +using JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Diagnostics +{ + /// + /// + /// A service to resolve a single /> from all those registered on + /// the or in the internal service provider. + /// + /// + /// This type is typically used by database providers (and other extensions). It is generally + /// not used in application code. + /// + /// + /// Instances should be registered on the internal service provider as multiple + /// interfaces. + /// + /// + public interface IInterceptorResolver + { + /// + /// The interceptor type. + /// + Type InterceptorType { get; } + + /// + /// + /// Resolves a single /> from all those registered on + /// the or in the internal service provider. + /// + /// + /// The interceptors to combine. + /// The combined interceptor. + IInterceptor ResolveInterceptor([NotNull] IReadOnlyList interceptors); + } +} diff --git a/src/EFCore/Diagnostics/IInterceptors.cs b/src/EFCore/Diagnostics/IInterceptors.cs index 537a6cd3f8e..3a31b6c6889 100644 --- a/src/EFCore/Diagnostics/IInterceptors.cs +++ b/src/EFCore/Diagnostics/IInterceptors.cs @@ -7,18 +7,15 @@ namespace Microsoft.EntityFrameworkCore.Diagnostics { /// /// - /// Base interface for all interceptor definitions. - /// - /// - /// Rather than implementing this interface directly, non-relational providers that need to add interceptors should inherit - /// from . Relational providers should inherit from 'RelationalInterceptors'. + /// A service that resolves a single from all those registered on + /// the or in the internal service provider. /// /// /// This type is typically used by database providers (and other extensions). It is generally /// not used in application code. /// /// - /// The service lifetime is . This means that each + /// The service lifetime is . This means that each /// instance will use its own instance of this service. /// The implementation may depend on other services registered with any lifetime. /// The implementation does not need to be thread-safe. @@ -26,5 +23,13 @@ namespace Microsoft.EntityFrameworkCore.Diagnostics /// public interface IInterceptors { + /// + /// Resolves a single from all those registered on + /// the or in the internal service provider. + /// + /// The interceptor type to resolve. + /// The resolved interceptor, which may be null if none are registered. + TInterceptor Resolve() + where TInterceptor : class, IInterceptor; } } diff --git a/src/EFCore/Diagnostics/InterceptionResult.cs b/src/EFCore/Diagnostics/InterceptionResult.cs index cb9e23b709f..c08e78508a9 100644 --- a/src/EFCore/Diagnostics/InterceptionResult.cs +++ b/src/EFCore/Diagnostics/InterceptionResult.cs @@ -1,13 +1,11 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using JetBrains.Annotations; - namespace Microsoft.EntityFrameworkCore.Diagnostics { /// /// - /// Represents a result from an interceptor such as an 'IDbCommandInterceptor' to allow + /// Represents a result from an such as an 'IDbConnectionInterceptor' to allow /// suppression of the normal operation being intercepted. /// /// @@ -16,24 +14,9 @@ namespace Microsoft.EntityFrameworkCore.Diagnostics /// Typically the interceptor should return the value passed in. /// However, returning some other non-null value will cause the operation being intercepted to /// be suppressed; that is, the operation is not executed. - /// The value is then used as a substitute return value for the operation that was suppressed. /// /// - /// The new result to use. - public readonly struct InterceptionResult + public readonly struct InterceptionResult { - /// - /// Creates a new instance. - /// - /// The result to use. - public InterceptionResult([CanBeNull] TResult result) - { - Result = result; - } - - /// - /// The result. - /// - public TResult Result { get; } } } diff --git a/src/EFCore/Diagnostics/InterceptionResult`.cs b/src/EFCore/Diagnostics/InterceptionResult`.cs new file mode 100644 index 00000000000..7f6a195ea0e --- /dev/null +++ b/src/EFCore/Diagnostics/InterceptionResult`.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using JetBrains.Annotations; + +namespace Microsoft.EntityFrameworkCore.Diagnostics +{ + /// + /// + /// Represents a result from an such as an 'IDbCommandInterceptor' to allow + /// suppression of the normal operation being intercepted. + /// + /// + /// A value of this type is passed to all interceptor methods that are called before the operation + /// being intercepted is executed. + /// Typically the interceptor should return the value passed in. + /// However, returning some other non-null value will cause the operation being intercepted to + /// be suppressed; that is, the operation is not executed. + /// The value is then used as a substitute return value for the operation that was suppressed. + /// + /// + /// The new result to use. + public readonly struct InterceptionResult + { + /// + /// Creates a new instance. + /// + /// The result to use. + public InterceptionResult([CanBeNull] TResult result) + { + Result = result; + } + + /// + /// The result. + /// + public TResult Result { get; } + } +} diff --git a/src/EFCore/Diagnostics/InterceptorResolver.cs b/src/EFCore/Diagnostics/InterceptorResolver.cs new file mode 100644 index 00000000000..4e3e1e5b223 --- /dev/null +++ b/src/EFCore/Diagnostics/InterceptorResolver.cs @@ -0,0 +1,72 @@ +// 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.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Utilities; + +namespace Microsoft.EntityFrameworkCore.Diagnostics +{ + /// + /// Abstract base class for implementations of the service. + /// + /// The interceptor type. + public abstract class InterceptorResolver : IInterceptorResolver + where TInterceptor : class, IInterceptor + { + private TInterceptor _interceptor; + private bool _resolved; + + /// + /// The interceptor type. + /// + public virtual Type InterceptorType => typeof(TInterceptor); + + /// + /// + /// Resolves a single /> from all those registered on + /// the or in the internal service provider. + /// + /// + /// The interceptors to combine. + /// The combined interceptor. + public virtual IInterceptor ResolveInterceptor(IReadOnlyList interceptors) + { + Check.NotNull(interceptors, nameof(interceptors)); + + if (!_resolved) + { + if (interceptors.Count == 1) + { + _interceptor = interceptors[0] as TInterceptor; + } + else if (interceptors.Count > 1) + { + var filtered = interceptors.OfType().ToList(); + + if (filtered.Count == 1) + { + _interceptor = filtered[0]; + } + else if (filtered.Count > 1) + { + _interceptor = CreateChain(filtered); + } + } + + _resolved = true; + } + + return _interceptor; + } + + /// + /// Must be implemented by the inheriting type to create a single interceptor from the given list. + /// + /// The interceptors to combine. + /// The combined interceptor. + protected abstract TInterceptor CreateChain([NotNull] IEnumerable interceptors); + } +} diff --git a/src/EFCore/Diagnostics/Interceptors.cs b/src/EFCore/Diagnostics/Interceptors.cs deleted file mode 100644 index ab727a8eb11..00000000000 --- a/src/EFCore/Diagnostics/Interceptors.cs +++ /dev/null @@ -1,44 +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 JetBrains.Annotations; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.EntityFrameworkCore.Diagnostics -{ - /// - /// - /// Base implementation for all interceptors. - /// - /// - /// Non-relational providers that need to add interceptors should inherit from this class. - /// Relational providers should inherit from 'RelationalInterceptors'. - /// - /// - /// This type is typically used by database providers (and other extensions). It is generally - /// not used in application code. - /// - /// - /// The service lifetime is . This means that each - /// instance will use its own instance of this service. - /// The implementation may depend on other services registered with any lifetime. - /// The implementation does not need to be thread-safe. - /// - /// - public class Interceptors : IInterceptors - { - /// - /// Creates a new instance using the given dependencies. - /// - /// The dependencies for this service. - public Interceptors([NotNull] InterceptorsDependencies dependencies) - { - Dependencies = dependencies; - } - - /// - /// The dependencies for this service. - /// - protected virtual InterceptorsDependencies Dependencies { get; } - } -} diff --git a/src/EFCore/Diagnostics/InterceptorsDependencies.cs b/src/EFCore/Diagnostics/InterceptorsDependencies.cs deleted file mode 100644 index d5b3c127038..00000000000 --- a/src/EFCore/Diagnostics/InterceptorsDependencies.cs +++ /dev/null @@ -1,73 +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.Collections; -using System.Collections.Generic; -using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Utilities; -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.EntityFrameworkCore.Diagnostics -{ - /// - /// - /// Service dependencies parameter class for - /// - /// - /// This type is typically used by database providers (and other extensions). It is generally - /// not used in application code. - /// - /// - /// Do not construct instances of this class directly from either provider or application code as the - /// constructor signature may change as new dependencies are added. Instead, use this type in - /// your constructor so that an instance will be created and injected automatically by the - /// dependency injection container. To create an instance with some dependent services replaced, - /// first resolve the object from the dependency injection container, then replace selected - /// services using the 'With...' methods. Do not call the constructor at any point in this process. - /// - /// - /// The service lifetime is . This means that each - /// instance will use its own instance of this service. - /// The implementation may depend on other services registered with any lifetime. - /// The implementation does not need to be thread-safe. - /// - /// - public sealed class InterceptorsDependencies - { - /// - /// - /// Creates the service dependencies parameter object for a . - /// - /// - /// Do not call this constructor directly from either provider or application code as it may change - /// as new dependencies are added. Instead, use this type in your constructor so that an instance - /// will be created and injected automatically by the dependency injection container. To create - /// an instance with some dependent services replaced, first resolve the object from the dependency - /// injection container, then replace selected services using the 'With...' methods. Do not call - /// the constructor at any point in this process. - /// - /// - /// The scoped internal service provider currently in use. - public InterceptorsDependencies([NotNull] IServiceProvider serviceProvider) - { - Check.NotNull(serviceProvider, nameof(serviceProvider)); - - ServiceProvider = serviceProvider; - } - - /// - /// The scoped internal service provider currently in use. - /// - public IServiceProvider ServiceProvider { get; } - - /// - /// Clones this dependency parameter object with one service replaced. - /// - /// A replacement for the current dependency of this type. - /// A new parameter object with the given service replaced. - public InterceptorsDependencies With([NotNull] IServiceProvider serviceProvider) - => new InterceptorsDependencies(serviceProvider); - } -} diff --git a/src/EFCore/Diagnostics/Internal/Interceptors.cs b/src/EFCore/Diagnostics/Internal/Interceptors.cs new file mode 100644 index 00000000000..c0e7b16f3f9 --- /dev/null +++ b/src/EFCore/Diagnostics/Internal/Interceptors.cs @@ -0,0 +1,86 @@ +// 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.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.EntityFrameworkCore.Diagnostics.Internal +{ + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public class Interceptors : IInterceptors + { + private readonly IServiceProvider _serviceProvider; + private readonly IEnumerable _injectedInterceptors; + private readonly Dictionary _resolvers; + private CoreOptionsExtension _coreOptionsExtension; + private List _interceptors; + + /// + /// 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 Interceptors([NotNull] IServiceProvider serviceProvider, + [NotNull] IEnumerable injectedInterceptors, + [NotNull] IEnumerable interceptorResolvers) + { + _serviceProvider = serviceProvider; + _injectedInterceptors = injectedInterceptors; + _resolvers = interceptorResolvers.ToDictionary(i => i.InterceptorType); + } + + private IReadOnlyList RegisteredInterceptors + { + get + { + if (_interceptors == null) + { + var interceptors = _injectedInterceptors.ToList(); + var appInterceptors = CoreOptionsExtension?.Interceptors; + if (appInterceptors != null) + { + interceptors.AddRange(appInterceptors); + } + + _interceptors = interceptors; + } + + return _interceptors; + } + } + + /// + /// 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 TInterceptor Resolve() + where TInterceptor : class, IInterceptor + => (TInterceptor)_resolvers[typeof(TInterceptor)].ResolveInterceptor(RegisteredInterceptors); + + /// + /// We resolve this lazily because loggers are created very early in the initialization + /// process where is not yet available from D.I. + /// This means those loggers can't do interception, but that's okay because nothing + /// else is ready for them to do interception anyway. + /// + private CoreOptionsExtension CoreOptionsExtension + => _coreOptionsExtension ??= _serviceProvider + .GetService() + .Extensions + .OfType() + .FirstOrDefault(); + + } +} diff --git a/src/EFCore/Infrastructure/CoreOptionsExtension.cs b/src/EFCore/Infrastructure/CoreOptionsExtension.cs index e94c6a813a4..5a3a32ce512 100644 --- a/src/EFCore/Infrastructure/CoreOptionsExtension.cs +++ b/src/EFCore/Infrastructure/CoreOptionsExtension.cs @@ -41,14 +41,13 @@ public class CoreOptionsExtension : IDbContextOptionsExtension private int? _maxPoolSize; private bool _serviceProviderCachingEnabled = true; private DbContextOptionsExtensionInfo _info; + private IEnumerable _interceptors; private WarningsConfiguration _warningsConfiguration = new WarningsConfiguration() .TryWithExplicit(CoreEventId.ManyServiceProvidersCreatedWarning, WarningBehavior.Throw) .TryWithExplicit(CoreEventId.LazyLoadOnDisposedContextWarning, WarningBehavior.Throw) - .TryWithExplicit(CoreEventId.DetachedLazyLoadingWarning, WarningBehavior.Throw) - // This is relational client eval warning. Yes, this is ugly and error-prone, but it will be removed before 3.0 ships - .TryWithExplicit(CoreEventId.RelationalBaseId + 500, WarningBehavior.Throw); + .TryWithExplicit(CoreEventId.DetachedLazyLoadingWarning, WarningBehavior.Throw); /// /// Creates a new set of options with everything set to default values. @@ -74,6 +73,7 @@ protected CoreOptionsExtension([NotNull] CoreOptionsExtension copyFrom) _queryTrackingBehavior = copyFrom.QueryTrackingBehavior; _maxPoolSize = copyFrom.MaxPoolSize; _serviceProviderCachingEnabled = copyFrom.ServiceProviderCachingEnabled; + _interceptors = copyFrom.Interceptors?.ToList(); if (copyFrom._replacedServices != null) { @@ -279,6 +279,25 @@ public virtual CoreOptionsExtension WithServiceProviderCachingEnabled(bool servi 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 WithInterceptors([NotNull] IEnumerable interceptors) + { + Check.NotNull(interceptors, nameof(interceptors)); + + var clone = Clone(); + + clone._interceptors = _interceptors == null + ? interceptors + : _interceptors.Concat(interceptors); + + return clone; + } + /// /// The option set from the method. /// @@ -342,6 +361,8 @@ public virtual CoreOptionsExtension WithServiceProviderCachingEnabled(bool servi /// public virtual int? MaxPoolSize => _maxPoolSize; + public virtual IEnumerable Interceptors => _interceptors; + /// /// Adds the services required to make the selected options work. This is used when there /// is no external and EF is maintaining its own service diff --git a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs index fc327f28a3c..45dba7de07b 100644 --- a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs +++ b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs @@ -9,6 +9,7 @@ using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; @@ -287,8 +288,7 @@ public virtual EntityFrameworkServicesBuilder TryAddCoreServices() .AddDependencyScoped() .AddDependencyScoped() .AddDependencyScoped() - .AddDependencyScoped() - .AddDependencyScoped(); + .AddDependencyScoped(); ServiceCollectionMap.TryAddSingleton( new RegisteredServices(ServiceCollectionMap.ServiceCollection.Select(s => s.ServiceType))); diff --git a/src/EFCore/Storage/IDbContextTransaction.cs b/src/EFCore/Storage/IDbContextTransaction.cs index 133e71bfd10..69ee085eae7 100644 --- a/src/EFCore/Storage/IDbContextTransaction.cs +++ b/src/EFCore/Storage/IDbContextTransaction.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Infrastructure; namespace Microsoft.EntityFrameworkCore.Storage @@ -31,5 +33,19 @@ public interface IDbContextTransaction : IDisposable /// Discards all changes made to the database in the current transaction. /// void Rollback(); + + /// + /// Commits all changes made to the database in the current transaction asynchronously. + /// + /// The cancellation token. + /// A representing the asynchronous operation. + Task CommitAsync(CancellationToken cancellationToken = default); + + /// + /// Discards all changes made to the database in the current transaction asynchronously. + /// + /// The cancellation token. + /// A representing the asynchronous operation. + Task RollbackAsync(CancellationToken cancellationToken = default); } } diff --git a/test/EFCore.InMemory.FunctionalTests/InterceptionInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/InterceptionInMemoryTest.cs new file mode 100644 index 00000000000..abcc5a0ef24 --- /dev/null +++ b/test/EFCore.InMemory.FunctionalTests/InterceptionInMemoryTest.cs @@ -0,0 +1,14 @@ +// 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. + +namespace Microsoft.EntityFrameworkCore +{ + // Currently no non-relational interceptors + public class InterceptionInMemoryTest : InterceptionTestBase + { + public InterceptionInMemoryTest(InterceptionFixtureBase fixture) + : base(fixture) + { + } + } +} diff --git a/test/EFCore.Relational.Specification.Tests/InterceptionTestBase.cs b/test/EFCore.Relational.Specification.Tests/CommandInterceptionTestBase.cs similarity index 91% rename from test/EFCore.Relational.Specification.Tests/InterceptionTestBase.cs rename to test/EFCore.Relational.Specification.Tests/CommandInterceptionTestBase.cs index ecb34752862..f3d9cef3e16 100644 --- a/test/EFCore.Relational.Specification.Tests/InterceptionTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/CommandInterceptionTestBase.cs @@ -4,7 +4,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.ComponentModel.DataAnnotations.Schema; using System.Data.Common; using System.Linq; using System.Threading; @@ -12,23 +11,17 @@ using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage; -using Microsoft.EntityFrameworkCore.TestUtilities; -using Microsoft.Extensions.DependencyInjection; using Xunit; namespace Microsoft.EntityFrameworkCore { - public abstract class InterceptionTestBase - where TBuilder : RelationalDbContextOptionsBuilder - where TExtension : RelationalOptionsExtension, new() + public abstract class CommandInterceptionTestBase : InterceptionTestBase { - protected InterceptionTestBase(InterceptionFixtureBase fixture) + protected CommandInterceptionTestBase(InterceptionFixtureBase fixture) + : base(fixture) { - Fixture = fixture; } - protected InterceptionFixtureBase Fixture { get; } - [ConditionalTheory] [InlineData(false, false)] [InlineData(true, false)] @@ -1139,13 +1132,10 @@ protected class ThrowingReaderCommandInterceptor : DbCommandInterceptor public virtual async Task Intercept_query_with_one_app_and_one_injected_interceptor(bool async) { var appInterceptor = new ResultReplacingReaderCommandInterceptor(); - using (var context = CreateContext(appInterceptor, typeof(MutatingReaderCommandInterceptor))) + var injectedInterceptor = new MutatingReaderCommandInterceptor(); + using (var context = CreateContext(appInterceptor, injectedInterceptor)) { - await TestCompoisteQueryInterceptors( - context, - appInterceptor, - (MutatingReaderCommandInterceptor)context.GetService>().Single(), - async); + await TestCompoisteQueryInterceptors(context, appInterceptor, injectedInterceptor, async); } } @@ -1172,7 +1162,7 @@ public virtual async Task Intercept_scalar_with_one_app_and_one_injected_interce { using (var context = CreateContext( new ResultReplacingScalarCommandInterceptor(), - typeof(MutatingScalarCommandInterceptor))) + new MutatingScalarCommandInterceptor())) { await TestCompositeScalarInterceptors(context, async); } @@ -1200,7 +1190,7 @@ public virtual async Task Intercept_non_query_one_app_and_one_injected_intercept { using (var context = CreateContext( new ResultReplacingNonQueryCommandInterceptor(), - typeof(MutatingNonQueryCommandInterceptor))) + new MutatingNonQueryCommandInterceptor())) { await TestCompositeNonQueryInterceptors(context, async); } @@ -1225,18 +1215,12 @@ private static async Task TestCompositeNonQueryInterceptors(UniverseContext cont [InlineData(true)] public virtual async Task Intercept_query_with_two_injected_interceptors(bool async) { - using (var context = CreateContext( - null, - typeof(MutatingReaderCommandInterceptor), - typeof(ResultReplacingReaderCommandInterceptor))) - { - var injectedInterceptors = context.GetService>().ToList(); + var injectedInterceptor1 = new MutatingReaderCommandInterceptor(); + var injectedInterceptor2 = new ResultReplacingReaderCommandInterceptor(); - await TestCompoisteQueryInterceptors( - context, - injectedInterceptors.OfType().Single(), - injectedInterceptors.OfType().Single(), - async); + using (var context = CreateContext(null,injectedInterceptor1, injectedInterceptor2)) + { + await TestCompoisteQueryInterceptors(context, injectedInterceptor2, injectedInterceptor1, async); } } @@ -1247,8 +1231,7 @@ public virtual async Task Intercept_scalar_with_two_injected_interceptors(bool a { using (var context = CreateContext( null, - typeof(MutatingScalarCommandInterceptor), - typeof(ResultReplacingScalarCommandInterceptor))) + new MutatingScalarCommandInterceptor(), new ResultReplacingScalarCommandInterceptor())) { await TestCompositeScalarInterceptors(context, async); } @@ -1261,8 +1244,7 @@ public virtual async Task Intercept_non_query_with_two_injected_interceptors(boo { using (var context = CreateContext( null, - typeof(MutatingNonQueryCommandInterceptor), - typeof(ResultReplacingNonQueryCommandInterceptor))) + new MutatingNonQueryCommandInterceptor(), new ResultReplacingNonQueryCommandInterceptor())) { await TestCompositeNonQueryInterceptors(context, async); } @@ -1274,8 +1256,10 @@ public virtual async Task Intercept_non_query_with_two_injected_interceptors(boo public virtual async Task Intercept_query_with_explicitly_composed_app_interceptor(bool async) { using (var context = CreateContext( - DbCommandInterceptor.CreateChain( - new MutatingReaderCommandInterceptor(), new ResultReplacingReaderCommandInterceptor()))) + new IInterceptor[] + { + new MutatingReaderCommandInterceptor(), new ResultReplacingReaderCommandInterceptor() + })) { var results = async ? await context.Set().ToListAsync() @@ -1306,8 +1290,10 @@ private static void AssertCompositeResults(List results) public virtual async Task Intercept_scalar_with_explicitly_composed_app_interceptor(bool async) { using (var context = CreateContext( - DbCommandInterceptor.CreateChain( - new MutatingScalarCommandInterceptor(), new ResultReplacingScalarCommandInterceptor()))) + new IInterceptor[] + { + new MutatingScalarCommandInterceptor(), new ResultReplacingScalarCommandInterceptor() + })) { await TestCompositeScalarInterceptors(context, async); } @@ -1319,8 +1305,10 @@ public virtual async Task Intercept_scalar_with_explicitly_composed_app_intercep public virtual async Task Intercept_non_query_with_explicitly_composed_app_interceptor(bool async) { using (var context = CreateContext( - DbCommandInterceptor.CreateChain( - new MutatingNonQueryCommandInterceptor(), new ResultReplacingNonQueryCommandInterceptor()))) + new IInterceptor[] + { + new MutatingNonQueryCommandInterceptor(), new ResultReplacingNonQueryCommandInterceptor() + })) { await TestCompositeNonQueryInterceptors(context, async); } @@ -1600,6 +1588,20 @@ public Task CommandFailedAsync( return Task.CompletedTask; } + public InterceptionResult? DataReaderDisposing( + DbCommand command, + DataReaderDisposingEventData eventData, + InterceptionResult? result) + { + Assert.NotNull(eventData.DataReader); + Assert.Same(Context, eventData.Context); + Assert.Equal(CommandText, command.CommandText); + Assert.Equal(CommandId, eventData.CommandId); + Assert.Equal(ConnectionId, eventData.ConnectionId); + + return result; + } + protected virtual void AssertExecuting(DbCommand command, CommandEventData eventData) { Assert.NotNull(eventData.Context); @@ -1638,111 +1640,5 @@ protected virtual void AssertFailed(DbCommand command, CommandErrorEventData eve FailedCalled = true; } } - - protected class Singularity - { - [DatabaseGenerated(DatabaseGeneratedOption.None)] - public int Id { get; set; } - - public string Type { get; set; } - } - - protected class Brane - { - [DatabaseGenerated(DatabaseGeneratedOption.None)] - public int Id { get; set; } - - public string Type { get; set; } - } - - public class UniverseContext : PoolableDbContext - { - public UniverseContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder - .Entity() - .HasData( - new Singularity - { - Id = 77, Type = "Black Hole" - }, - new Singularity - { - Id = 88, Type = "Bing Bang" - }); - - modelBuilder - .Entity() - .HasData( - new Brane - { - Id = 77, Type = "Black Hole?" - }, - new Brane - { - Id = 88, Type = "Bing Bang?" - }); - } - } - - protected void AssertSql(string expected, string actual) - => Assert.Equal( - expected, - actual.Replace("\r", string.Empty).Replace("\n", " ")); - - protected (DbContext, TInterceptor) CreateContext(bool inject) - where TInterceptor : class, IDbCommandInterceptor, new() - { - var interceptor = inject ? null : new TInterceptor(); - - var context = inject - ? CreateContext(null, typeof(TInterceptor)) - : CreateContext(interceptor); - - if (inject) - { - interceptor = (TInterceptor)context.GetService>().Single(); - } - - return (context, interceptor); - } - - public UniverseContext CreateContext( - IDbCommandInterceptor appInterceptor, - params Type[] injectedInterceptorTypes) - => new UniverseContext( - Fixture.AddRelationalOptions( - b => - { - if (appInterceptor != null) - { - b.CommandInterceptor(appInterceptor); - } - }, - injectedInterceptorTypes)); - - public abstract class InterceptionFixtureBase : SharedStoreFixtureBase - { - protected virtual IServiceCollection InjectInterceptors( - IServiceCollection serviceCollection, - Type[] injectedInterceptorTypes) - { - foreach (var interceptorType in injectedInterceptorTypes) - { - serviceCollection.AddScoped(typeof(IDbCommandInterceptor), interceptorType); - } - - return serviceCollection; - } - - public abstract DbContextOptions AddRelationalOptions( - Action> relationalBuilder, - Type[] injectedInterceptorTypes); - } } } diff --git a/test/EFCore.Relational.Specification.Tests/ConnectionInterceptionTestBase.cs b/test/EFCore.Relational.Specification.Tests/ConnectionInterceptionTestBase.cs new file mode 100644 index 00000000000..a9baea36923 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/ConnectionInterceptionTestBase.cs @@ -0,0 +1,487 @@ +// 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.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public abstract class ConnectionInterceptionTestBase : InterceptionTestBase + { + protected ConnectionInterceptionTestBase(InterceptionFixtureBase fixture) + : base(fixture) + { + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_connection_passively(bool async) + { + var (context, interceptor) = CreateContext(); + using (context) + { + // Test infrastructure uses an open connection, so close it first. + var connection = context.Database.GetDbConnection(); + var startedOpen = connection.State == ConnectionState.Open; + if (startedOpen) + { + connection.Close(); + } + + if (async) + { + await context.Database.OpenConnectionAsync(); + } + else + { + context.Database.OpenConnection(); + } + + AssertNormalOpen(context, interceptor, async); + + interceptor.Reset(); + + if (async) + { + await context.Database.CloseConnectionAsync(); + } + else + { + context.Database.CloseConnection(); + } + + AssertNormalClose(context, interceptor, async); + + if (startedOpen) + { + connection.Open(); + } + } + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_connection_to_override_opening(bool async) + { + var (context, interceptor) = CreateContext(); + using (context) + { + // Test infrastructure uses an open connection, so close it first. + var connection = context.Database.GetDbConnection(); + var startedOpen = connection.State == ConnectionState.Open; + if (startedOpen) + { + connection.Close(); + } + + if (async) + { + await context.Database.OpenConnectionAsync(); + } + else + { + context.Database.OpenConnection(); + } + + AssertNormalOpen(context, interceptor, async); + + interceptor.Reset(); + + if (async) + { + await context.Database.CloseConnectionAsync(); + } + else + { + context.Database.CloseConnection(); + } + + AssertNormalClose(context, interceptor, async); + + if (startedOpen) + { + connection.Open(); + } + } + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_connection_with_multiple_interceptors(bool async) + { + var interceptor1 = new ConnectionInterceptor(); + var interceptor2 = new ConnectionOverridingInterceptor(); + var interceptor3 = new ConnectionInterceptor(); + var interceptor4 = new ConnectionOverridingInterceptor(); + using (var context = CreateContext( + new IInterceptor[] + { + new NoOpConnectionInterceptor(), interceptor1, interceptor2 + }, + new IInterceptor[] + { + interceptor3, interceptor4, new NoOpConnectionInterceptor() + })) + { + // Test infrastructure uses an open connection, so close it first. + var connection = context.Database.GetDbConnection(); + var startedOpen = connection.State == ConnectionState.Open; + if (startedOpen) + { + connection.Close(); + } + + if (async) + { + await context.Database.OpenConnectionAsync(); + } + else + { + context.Database.OpenConnection(); + } + + AssertNormalOpen(context, interceptor1, async); + AssertNormalOpen(context, interceptor2, async); + AssertNormalOpen(context, interceptor3, async); + AssertNormalOpen(context, interceptor4, async); + + interceptor1.Reset(); + interceptor2.Reset(); + interceptor3.Reset(); + interceptor4.Reset(); + + if (async) + { + await context.Database.CloseConnectionAsync(); + } + else + { + context.Database.CloseConnection(); + } + + AssertNormalClose(context, interceptor1, async); + AssertNormalClose(context, interceptor2, async); + AssertNormalClose(context, interceptor3, async); + AssertNormalClose(context, interceptor4, async); + + if (startedOpen) + { + connection.Open(); + } + } + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_connection_that_throws_on_open(bool async) + { + var interceptor = new ConnectionInterceptor(); + + using (var context = CreateBadUniverse(new DbContextOptionsBuilder().AddInterceptors(interceptor))) + { + try + { + if (async) + { + await context.Database.OpenConnectionAsync(); + } + else + { + context.Database.OpenConnection(); + } + + Assert.False(true); + } + catch (Exception exception) + { + Assert.Same(interceptor.Exception, exception); + } + + AssertErrorOnOpen(context, interceptor, async); + } + } + + protected abstract BadUniverseContext CreateBadUniverse(DbContextOptionsBuilder optionsBuilder); + + protected class BadUniverseContext : UniverseContext + { + public BadUniverseContext(DbContextOptions options) + : base(options) + { + } + } + + protected class NoOpConnectionInterceptor : DbConnectionInterceptor + { + } + + protected class ConnectionOverridingInterceptor : ConnectionInterceptor + { + public override InterceptionResult? ConnectionOpening( + DbConnection connection, + ConnectionEventData eventData, + InterceptionResult? result) + { + base.ConnectionOpening(connection, eventData, result); + + if (result == null) + { + connection.Open(); + } + + return new InterceptionResult(); + } + + public override async Task ConnectionOpeningAsync( + DbConnection connection, + ConnectionEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + await base.ConnectionOpeningAsync(connection, eventData, result, cancellationToken); + + if (result == null) + { + await connection.OpenAsync(cancellationToken); + } + + return new InterceptionResult(); + } + } + + protected class ConnectionInterceptor : IDbConnectionInterceptor + { + public DbContext Context { get; set; } + public Exception Exception { get; set; } + public Guid ConnectionId { get; set; } + public bool AsyncCalled { get; set; } + public bool SyncCalled { get; set; } + public bool OpeningCalled { get; set; } + public bool OpenedCalled { get; set; } + public bool ClosingCalled { get; set; } + public bool ClosedCalled { get; set; } + public bool FailedCalled { get; set; } + + public void Reset() + { + Context = null; + Exception = null; + ConnectionId = default; + AsyncCalled = false; + SyncCalled = false; + OpeningCalled = false; + OpenedCalled = false; + ClosingCalled = false; + ClosedCalled = false; + FailedCalled = false; + } + + public virtual InterceptionResult? ConnectionOpening( + DbConnection connection, + ConnectionEventData eventData, + InterceptionResult? result) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertOpening(eventData); + + return result; + } + + public virtual Task ConnectionOpeningAsync( + DbConnection connection, + ConnectionEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertOpening(eventData); + + return Task.FromResult(result); + } + + public virtual void ConnectionOpened( + DbConnection connection, + ConnectionEndEventData eventData) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertOpened(eventData); + } + + public virtual Task ConnectionOpenedAsync( + DbConnection connection, + ConnectionEndEventData eventData, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertOpened(eventData); + + return Task.CompletedTask; + } + + public virtual InterceptionResult? ConnectionClosing( + DbConnection connection, + ConnectionEventData eventData, + InterceptionResult? result) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertClosing(eventData); + + return result; + } + + public virtual Task ConnectionClosingAsync( + DbConnection connection, + ConnectionEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertClosing(eventData); + + return Task.FromResult(result); + } + + public virtual void ConnectionClosed( + DbConnection connection, + ConnectionEndEventData eventData) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertClosed(eventData); + } + + public virtual Task ConnectionClosedAsync( + DbConnection connection, + ConnectionEndEventData eventData, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertClosed(eventData); + + return Task.CompletedTask; + } + + public virtual void ConnectionFailed( + DbConnection connection, + ConnectionErrorEventData eventData) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertFailed(eventData); + } + + public virtual Task ConnectionFailedAsync( + DbConnection connection, + ConnectionErrorEventData eventData, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertFailed(eventData); + + return Task.CompletedTask; + } + + protected virtual void AssertOpening(ConnectionEventData eventData) + { + Assert.NotNull(eventData.Context); + Assert.NotEqual(default, eventData.ConnectionId); + + Context = eventData.Context; + ConnectionId = eventData.ConnectionId; + OpeningCalled = true; + } + + protected virtual void AssertOpened(ConnectionEndEventData eventData) + { + Assert.Same(Context, eventData.Context); + Assert.Equal(ConnectionId, eventData.ConnectionId); + + OpenedCalled = true; + } + + protected virtual void AssertClosing(ConnectionEventData eventData) + { + Assert.NotNull(eventData.Context); + Assert.NotEqual(default, eventData.ConnectionId); + + Context = eventData.Context; + ConnectionId = eventData.ConnectionId; + ClosingCalled = true; + } + + protected virtual void AssertClosed(ConnectionEndEventData eventData) + { + Assert.Same(Context, eventData.Context); + Assert.Equal(ConnectionId, eventData.ConnectionId); + + ClosedCalled = true; + } + + protected virtual void AssertFailed(ConnectionErrorEventData eventData) + { + Assert.Same(Context, eventData.Context); + Assert.Equal(ConnectionId, eventData.ConnectionId); + Assert.NotNull(eventData.Exception); + + Exception = eventData.Exception; + FailedCalled = true; + } + } + + private static void AssertNormalOpen(DbContext context, ConnectionInterceptor interceptor, bool async) + { + Assert.Equal(async, interceptor.AsyncCalled); + Assert.NotEqual(async, interceptor.SyncCalled); + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.OpeningCalled); + Assert.True(interceptor.OpenedCalled); + Assert.False(interceptor.ClosedCalled); + Assert.False(interceptor.ClosingCalled); + Assert.False(interceptor.FailedCalled); + Assert.Same(context, interceptor.Context); + } + + private static void AssertNormalClose(DbContext context, ConnectionInterceptor interceptor, bool async) + { + Assert.Equal(async, interceptor.AsyncCalled); + Assert.NotEqual(async, interceptor.SyncCalled); + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.False(interceptor.OpeningCalled); + Assert.False(interceptor.OpenedCalled); + Assert.True(interceptor.ClosedCalled); + Assert.True(interceptor.ClosingCalled); + Assert.False(interceptor.FailedCalled); + Assert.Same(context, interceptor.Context); + } + + private static void AssertErrorOnOpen(DbContext context, ConnectionInterceptor interceptor, bool async) + { + Assert.Equal(async, interceptor.AsyncCalled); + Assert.NotEqual(async, interceptor.SyncCalled); + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.OpeningCalled); + Assert.False(interceptor.OpenedCalled); + Assert.False(interceptor.ClosedCalled); + Assert.False(interceptor.ClosingCalled); + Assert.True(interceptor.FailedCalled); + Assert.Same(context, interceptor.Context); + } + } +} diff --git a/test/EFCore.Relational.Specification.Tests/MigrationsTestBase.cs b/test/EFCore.Relational.Specification.Tests/MigrationsTestBase.cs index 35aa6076c4a..7e79736b8bb 100644 --- a/test/EFCore.Relational.Specification.Tests/MigrationsTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/MigrationsTestBase.cs @@ -333,7 +333,7 @@ public virtual async Task Can_execute_operations() } finally { - db.Database.CloseConnection(); + await db.Database.CloseConnectionAsync(); } } } diff --git a/test/EFCore.Relational.Specification.Tests/Query/AsyncFromSqlQueryTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/AsyncFromSqlQueryTestBase.cs index f6757af4852..becd7c57b54 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/AsyncFromSqlQueryTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/AsyncFromSqlQueryTestBase.cs @@ -383,7 +383,7 @@ public virtual async Task Include_does_not_close_user_opened_connection_for_empt Assert.Empty(query); Assert.Equal(ConnectionState.Open, connection.State); - context.Database.CloseConnection(); + await context.Database.CloseConnectionAsync(); Assert.Equal(ConnectionState.Closed, connection.State); } diff --git a/test/EFCore.Relational.Specification.Tests/TransactionInterceptionTestBase.cs b/test/EFCore.Relational.Specification.Tests/TransactionInterceptionTestBase.cs new file mode 100644 index 00000000000..c23ab34b9d5 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/TransactionInterceptionTestBase.cs @@ -0,0 +1,914 @@ +// 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.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Storage; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public abstract class TransactionInterceptionTestBase : InterceptionTestBase + { + protected TransactionInterceptionTestBase(InterceptionFixtureBase fixture) + : base(fixture) + { + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_BeginTransaction(bool async) + { + var (context, interceptor) = CreateContext(); + using (context) + { + using (var _ = async + ? await context.Database.BeginTransactionAsync() + : context.Database.BeginTransaction()) + { + AssertBeginTransaction(context, interceptor, async); + } + } + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_BeginTransaction_with_isolation_level(bool async) + { + var (context, interceptor) = CreateContext(); + using (context) + { + using (var _ = async + ? await context.Database.BeginTransactionAsync(IsolationLevel.ReadUncommitted) + : context.Database.BeginTransaction(IsolationLevel.ReadUncommitted)) + { + AssertBeginTransaction(context, interceptor, async); + } + } + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_BeginTransaction_to_suppress(bool async) + { + var (context, interceptor) = CreateContext(); + using (context) + { + using (var _ = async + ? await context.Database.BeginTransactionAsync() + : context.Database.BeginTransaction()) + { + AssertBeginTransaction(context, interceptor, async); + + // Throws if a real transaction has been created + using (context.Database.GetDbConnection().BeginTransaction()) + { + } + } + } + } + + protected class SuppressingTransactionInterceptor : TransactionInterceptor + { + public override InterceptionResult? TransactionStarting( + DbConnection connection, + TransactionStartingEventData eventData, + InterceptionResult? result) + { + base.TransactionStarting(connection, eventData, result); + + return new InterceptionResult(new FakeDbTransaction(connection, eventData.IsolationLevel)); + } + + public override async Task?> TransactionStartingAsync( + DbConnection connection, + TransactionStartingEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + await base.TransactionStartingAsync(connection, eventData, result, cancellationToken); + + return new InterceptionResult(new FakeDbTransaction(connection, eventData.IsolationLevel)); + } + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_BeginTransaction_to_wrap(bool async) + { + var (context, interceptor) = CreateContext(); + using (context) + { + using (var transaction = async + ? await context.Database.BeginTransactionAsync() + : context.Database.BeginTransaction()) + { + AssertBeginTransaction(context, interceptor, async); + + Assert.IsType(transaction.GetDbTransaction()); + } + } + } + + protected class WrappingTransactionInterceptor : TransactionInterceptor + { + public override DbTransaction TransactionStarted( + DbConnection connection, + TransactionEndEventData eventData, + DbTransaction result) + { + result = base.TransactionStarted(connection, eventData, result); + + return new WrappedDbTransaction(result); + } + + public override async Task TransactionStartedAsync( + DbConnection connection, + TransactionEndEventData eventData, + DbTransaction result, + CancellationToken cancellationToken = default) + { + result = await base.TransactionStartedAsync(connection, eventData, result, cancellationToken); + + return new WrappedDbTransaction(result); + } + + public override DbTransaction TransactionUsed( + DbConnection connection, + TransactionEventData eventData, + DbTransaction result) + { + result = base.TransactionUsed(connection, eventData, result); + + return new WrappedDbTransaction(result); + } + + public override async Task TransactionUsedAsync( + DbConnection connection, + TransactionEventData eventData, + DbTransaction result, + CancellationToken cancellationToken = default) + { + result = await base.TransactionUsedAsync(connection, eventData, result, cancellationToken); + + return new WrappedDbTransaction(result); + } + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_UseTransaction(bool async) + { + var (context, interceptor) = CreateContext(); + using (context) + { + using (var transaction = context.Database.GetDbConnection().BeginTransaction()) + { + var contextTransaction = async + ? await context.Database.UseTransactionAsync(transaction) + : context.Database.UseTransaction(transaction); + + AssertUseTransaction(context, contextTransaction, interceptor, async); + } + } + } + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_UseTransaction_to_wrap(bool async) + { + var (context, interceptor) = CreateContext(); + using (context) + { + using (var transaction = context.Database.GetDbConnection().BeginTransaction()) + { + var contextTransaction = async + ? await context.Database.UseTransactionAsync(transaction) + : context.Database.UseTransaction(transaction); + + Assert.IsType(contextTransaction.GetDbTransaction()); + + AssertUseTransaction(context, contextTransaction, interceptor, async); + } + } + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_Commit(bool async) + { + var (context, interceptor) = CreateContext(); + using (context) + { + using (var contextTransaction = async + ? await context.Database.BeginTransactionAsync() + : context.Database.BeginTransaction()) + { + interceptor.Reset(); + + if (async) + { + await contextTransaction.CommitAsync(); + } + else + { + contextTransaction.Commit(); + } + + AssertCommit(context, contextTransaction, interceptor, async); + } + } + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_Commit_to_suppress(bool async) + { + var (context, interceptor) = CreateContext(); + using (context) + { + using (var contextTransaction = async + ? await context.Database.BeginTransactionAsync() + : context.Database.BeginTransaction()) + { + interceptor.Reset(); + + if (async) + { + await contextTransaction.CommitAsync(); + } + else + { + contextTransaction.Commit(); + } + + // Will throw if Commit was already called + contextTransaction.GetDbTransaction().Commit(); + + AssertCommit(context, contextTransaction, interceptor, async); + } + } + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_Rollback(bool async) + { + var (context, interceptor) = CreateContext(); + using (context) + { + using (var contextTransaction = async + ? await context.Database.BeginTransactionAsync() + : context.Database.BeginTransaction()) + { + interceptor.Reset(); + + if (async) + { + await contextTransaction.RollbackAsync(); + } + else + { + contextTransaction.Rollback(); + } + + AssertRollBack(context, contextTransaction, interceptor, async); + } + } + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_Rollback_to_suppress(bool async) + { + var (context, interceptor) = CreateContext(); + using (context) + { + using (var contextTransaction = async + ? await context.Database.BeginTransactionAsync() + : context.Database.BeginTransaction()) + { + interceptor.Reset(); + + if (async) + { + await contextTransaction.RollbackAsync(); + } + else + { + contextTransaction.Rollback(); + } + + // Will throw if Commit was already called + contextTransaction.GetDbTransaction().Commit(); + + AssertRollBack(context, contextTransaction, interceptor, async); + } + } + } + + protected class CommitSuppressingTransactionInterceptor : TransactionInterceptor + { + public override InterceptionResult? TransactionCommitting( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult? result) + { + base.TransactionCommitting(transaction, eventData, result); + + return new InterceptionResult(); + } + + public override async Task TransactionCommittingAsync( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + await base.TransactionCommittingAsync(transaction, eventData, result, cancellationToken); + + return new InterceptionResult(); + } + + public override InterceptionResult? TransactionRollingBack( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult? result) + { + base.TransactionRollingBack(transaction, eventData, result); + + return new InterceptionResult(); + } + + public override async Task TransactionRollingBackAsync( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + await base.TransactionRollingBackAsync(transaction, eventData, result, cancellationToken); + + return new InterceptionResult(); + } + } + + [ConditionalTheory] + [InlineData(false, true)] + [InlineData(true, true)] + [InlineData(false, false)] + [InlineData(true, false)] + public virtual async Task Intercept_error_on_commit_or_rollback(bool async, bool commit) + { + var (context, interceptor) = CreateContext(); + using (context) + { + using (var contextTransaction = async + ? await context.Database.BeginTransactionAsync() + : context.Database.BeginTransaction()) + { + interceptor.Reset(); + + contextTransaction.GetDbTransaction().Commit(); + + try + { + if (async) + { + if (commit) + { + await contextTransaction.CommitAsync(); + } + else + { + await contextTransaction.RollbackAsync(); + } + } + else + { + if (commit) + { + contextTransaction.Commit(); + } + else + { + contextTransaction.Rollback(); + } + } + + Assert.False(true); + } + catch (Exception exception) + { + Assert.Same(exception, interceptor.Exception); + } + + AssertError(context, interceptor, async); + } + } + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_connection_with_multiple_interceptors(bool async) + { + var interceptor1 = new TransactionInterceptor(); + var interceptor2 = new WrappingTransactionInterceptor(); + var interceptor3 = new TransactionInterceptor(); + var interceptor4 = new WrappingTransactionInterceptor(); + using (var context = CreateContext( + new IInterceptor[] + { + new NoOpTransactionInterceptor(), interceptor1, interceptor2 + }, + new IInterceptor[] + { + interceptor3, interceptor4, new NoOpTransactionInterceptor() + })) + { + using (var contextTransaction = async + ? await context.Database.BeginTransactionAsync() + : context.Database.BeginTransaction()) + { + + Assert.IsType(contextTransaction.GetDbTransaction()); + + AssertBeginTransaction(context, interceptor1, async); + AssertBeginTransaction(context, interceptor2, async); + AssertBeginTransaction(context, interceptor3, async); + AssertBeginTransaction(context, interceptor4, async); + } + } + } + + protected class NoOpTransactionInterceptor : DbConnectionInterceptor + { + } + + private class WrappedDbTransaction : DbTransaction + { + private readonly DbTransaction _transaction; + + public WrappedDbTransaction(DbTransaction transaction) + { + _transaction = transaction; + } + + public override void Commit() => _transaction.Commit(); + public override void Rollback() => _transaction.Rollback(); + protected override DbConnection DbConnection => _transaction.Connection; + public override IsolationLevel IsolationLevel => _transaction.IsolationLevel; + protected override void Dispose(bool disposing) => _transaction.Dispose(); + } + + private class FakeDbTransaction : DbTransaction + { + public FakeDbTransaction(DbConnection dbConnection, IsolationLevel isolationLevel) + { + DbConnection = dbConnection; + IsolationLevel = isolationLevel == IsolationLevel.Unspecified + ? IsolationLevel.Snapshot + : isolationLevel; + } + + public override void Commit() + { + } + + public override void Rollback() + { + } + + protected override DbConnection DbConnection { get; } + + public override IsolationLevel IsolationLevel { get; } + } + + private static void AssertBeginTransaction(DbContext context, TransactionInterceptor interceptor, bool async) + { + Assert.Equal(async, interceptor.AsyncCalled); + Assert.NotEqual(async, interceptor.SyncCalled); + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.StartingCalled); + Assert.True(interceptor.StartedCalled); + Assert.False(interceptor.UsedCalled); + Assert.False(interceptor.CommittingCalled); + Assert.False(interceptor.CommittedCalled); + Assert.False(interceptor.RollingBackCalled); + Assert.False(interceptor.RolledBackCalled); + Assert.False(interceptor.FailedCalled); + Assert.Same(context, interceptor.Context); + } + + private static void AssertUseTransaction( + DbContext context, + IDbContextTransaction contextTransaction, + TransactionInterceptor interceptor, + bool async) + { + Assert.Equal(async, interceptor.AsyncCalled); + Assert.NotEqual(async, interceptor.SyncCalled); + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.UsedCalled); + Assert.False(interceptor.StartingCalled); + Assert.False(interceptor.CommittingCalled); + Assert.False(interceptor.CommittedCalled); + Assert.False(interceptor.RollingBackCalled); + Assert.False(interceptor.RolledBackCalled); + Assert.False(interceptor.StartedCalled); + Assert.False(interceptor.FailedCalled); + Assert.Same(context, interceptor.Context); + Assert.Equal(contextTransaction.TransactionId, interceptor.TransactionId); + } + + private static void AssertCommit( + DbContext context, + IDbContextTransaction contextTransaction, + TransactionInterceptor interceptor, + bool async) + { + Assert.Equal(async, interceptor.AsyncCalled); + Assert.NotEqual(async, interceptor.SyncCalled); + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.CommittingCalled); + Assert.True(interceptor.CommittedCalled); + Assert.False(interceptor.RollingBackCalled); + Assert.False(interceptor.RolledBackCalled); + Assert.False(interceptor.UsedCalled); + Assert.False(interceptor.StartingCalled); + Assert.False(interceptor.StartedCalled); + Assert.False(interceptor.FailedCalled); + Assert.Same(context, interceptor.Context); + Assert.Equal(contextTransaction.TransactionId, interceptor.TransactionId); + } + + private static void AssertRollBack( + DbContext context, + IDbContextTransaction contextTransaction, + TransactionInterceptor interceptor, + bool async) + { + Assert.Equal(async, interceptor.AsyncCalled); + Assert.NotEqual(async, interceptor.SyncCalled); + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.False(interceptor.CommittingCalled); + Assert.False(interceptor.CommittedCalled); + Assert.True(interceptor.RollingBackCalled); + Assert.True(interceptor.RolledBackCalled); + Assert.False(interceptor.UsedCalled); + Assert.False(interceptor.StartingCalled); + Assert.False(interceptor.StartedCalled); + Assert.False(interceptor.FailedCalled); + Assert.Same(context, interceptor.Context); + Assert.Equal(contextTransaction.TransactionId, interceptor.TransactionId); + } + + private static void AssertError( + DbContext context, + TransactionInterceptor interceptor, + bool async) + { + Assert.Equal(async, interceptor.AsyncCalled); + Assert.NotEqual(async, interceptor.SyncCalled); + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.FailedCalled); + Assert.Same(context, interceptor.Context); + } + + protected class TransactionInterceptor : IDbTransactionInterceptor + { + public DbContext Context { get; set; } + public Exception Exception { get; set; } + public Guid TransactionId { get; set; } + public Guid ConnectionId { get; set; } + public IsolationLevel IsolationLevel { get; set; } + public bool AsyncCalled { get; set; } + public bool SyncCalled { get; set; } + public bool StartingCalled { get; set; } + public bool StartedCalled { get; set; } + public bool UsedCalled { get; set; } + public bool CommittingCalled { get; set; } + public bool CommittedCalled { get; set; } + public bool RollingBackCalled { get; set; } + public bool RolledBackCalled { get; set; } + public bool FailedCalled { get; set; } + + public void Reset() + { + Context = null; + Exception = null; + ConnectionId = default; + AsyncCalled = false; + SyncCalled = false; + StartingCalled = false; + StartedCalled = false; + UsedCalled = false; + CommittingCalled = false; + CommittedCalled = false; + RollingBackCalled = false; + RolledBackCalled = false; + FailedCalled = false; + } + + protected virtual void AssertStarting(DbConnection connection, TransactionStartingEventData eventData) + { + Assert.NotNull(eventData.Context); + Assert.NotEqual(default, eventData.ConnectionId); + Assert.NotEqual(default, eventData.TransactionId); + + Context = eventData.Context; + TransactionId = eventData.TransactionId; + ConnectionId = eventData.ConnectionId; + IsolationLevel = eventData.IsolationLevel; + + StartingCalled = true; + } + + protected virtual void AssertStarted(DbConnection connection, TransactionEndEventData eventData) + { + Assert.Same(Context, eventData.Context); + Assert.Equal(TransactionId, eventData.TransactionId); + Assert.Equal(ConnectionId, eventData.ConnectionId); + + if (IsolationLevel == IsolationLevel.Unspecified) + { + Assert.NotEqual(IsolationLevel.Unspecified, eventData.Transaction.IsolationLevel); + } + else + { + Assert.Equal(IsolationLevel, eventData.Transaction.IsolationLevel); + } + + StartedCalled = true; + } + + protected virtual void AssertCommitting(TransactionEventData eventData) + { + Assert.NotNull(eventData.Context); + Assert.NotEqual(default, eventData.ConnectionId); + Assert.NotEqual(default, eventData.TransactionId); + + Context = eventData.Context; + TransactionId = eventData.TransactionId; + ConnectionId = eventData.ConnectionId; + + CommittingCalled = true; + } + + protected virtual void AssertCommitted(TransactionEndEventData eventData) + { + Assert.Same(Context, eventData.Context); + Assert.Equal(TransactionId, eventData.TransactionId); + Assert.Equal(ConnectionId, eventData.ConnectionId); + + CommittedCalled = true; + } + + protected virtual void AssertRollingBack(TransactionEventData eventData) + { + Assert.NotNull(eventData.Context); + Assert.NotEqual(default, eventData.ConnectionId); + Assert.NotEqual(default, eventData.TransactionId); + + Context = eventData.Context; + TransactionId = eventData.TransactionId; + ConnectionId = eventData.ConnectionId; + + RollingBackCalled = true; + } + + protected virtual void AssertRolledBack(TransactionEndEventData eventData) + { + Assert.Same(Context, eventData.Context); + Assert.Equal(TransactionId, eventData.TransactionId); + Assert.Equal(ConnectionId, eventData.ConnectionId); + + RolledBackCalled = true; + } + + protected virtual void AssertFailed(TransactionErrorEventData eventData) + { + Assert.Same(Context, eventData.Context); + Assert.Equal(TransactionId, eventData.TransactionId); + Assert.Equal(ConnectionId, eventData.ConnectionId); + Assert.NotNull(eventData.Exception); + + Exception = eventData.Exception; + FailedCalled = true; + } + + protected virtual void AssertUsed(DbConnection connection, TransactionEventData eventData) + { + Assert.NotNull(eventData.Context); + Assert.NotEqual(default, eventData.ConnectionId); + Assert.NotEqual(default, eventData.TransactionId); + + Context = eventData.Context; + TransactionId = eventData.TransactionId; + ConnectionId = eventData.ConnectionId; + UsedCalled = true; + } + + public virtual InterceptionResult? TransactionStarting( + DbConnection connection, + TransactionStartingEventData eventData, + InterceptionResult? result) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertStarting(connection, eventData); + + return result; + } + + public virtual DbTransaction TransactionStarted( + DbConnection connection, + TransactionEndEventData eventData, + DbTransaction result) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertStarted(connection, eventData); + + return result; + } + + public virtual Task?> TransactionStartingAsync( + DbConnection connection, + TransactionStartingEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertStarting(connection, eventData); + + return Task.FromResult(result); + } + + public virtual Task TransactionStartedAsync( + DbConnection connection, + TransactionEndEventData eventData, + DbTransaction result, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertStarted(connection, eventData); + + return Task.FromResult(result); + } + + public virtual DbTransaction TransactionUsed( + DbConnection connection, + TransactionEventData eventData, + DbTransaction result) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertUsed(connection, eventData); + + return result; + } + + public virtual Task TransactionUsedAsync( + DbConnection connection, + TransactionEventData eventData, + DbTransaction result, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertUsed(connection, eventData); + + return Task.FromResult(result); + } + + public virtual InterceptionResult? TransactionCommitting( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult? result) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertCommitting(eventData); + + return result; + } + + public virtual void TransactionCommitted( + DbTransaction transaction, + TransactionEndEventData eventData) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertCommitted(eventData); + } + + public virtual Task TransactionCommittingAsync( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertCommitting(eventData); + + return Task.FromResult(result); + } + + public virtual Task TransactionCommittedAsync( + DbTransaction transaction, + TransactionEndEventData eventData, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertCommitted(eventData); + + return Task.CompletedTask; + } + + public virtual InterceptionResult? TransactionRollingBack( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult? result) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertRollingBack(eventData); + + return result; + } + + public virtual void TransactionRolledBack( + DbTransaction transaction, + TransactionEndEventData eventData) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertRolledBack(eventData); + } + + public virtual Task TransactionRollingBackAsync( + DbTransaction transaction, + TransactionEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertRollingBack(eventData); + + return Task.FromResult(result); + } + + public virtual Task TransactionRolledBackAsync( + DbTransaction transaction, + TransactionEndEventData eventData, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertRolledBack(eventData); + + return Task.CompletedTask; + } + + public virtual void TransactionFailed( + DbTransaction transaction, + TransactionErrorEventData eventData) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertFailed(eventData); + } + + public virtual Task TransactionFailedAsync( + DbTransaction transaction, + TransactionErrorEventData eventData, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertFailed(eventData); + + return Task.CompletedTask; + } + } + } +} diff --git a/test/EFCore.Relational.Tests/Infrastructure/RelationalInterceptorsDependenciesDependenciesTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalInterceptorsDependenciesDependenciesTest.cs deleted file mode 100644 index 70d91d3ee58..00000000000 --- a/test/EFCore.Relational.Tests/Infrastructure/RelationalInterceptorsDependenciesDependenciesTest.cs +++ /dev/null @@ -1,18 +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.Diagnostics; -using Microsoft.EntityFrameworkCore.TestUtilities; -using Xunit; - -namespace Microsoft.EntityFrameworkCore.Infrastructure -{ - public class RelationalInterceptorsDependenciesDependenciesTest - { - [Fact] - public void Can_use_With_methods_to_clone_and_replace_service() - { - RelationalTestHelpers.Instance.TestDependenciesClone(); - } - } -} diff --git a/test/EFCore.Relational.Tests/RelationalConnectionTest.cs b/test/EFCore.Relational.Tests/RelationalConnectionTest.cs index b479521076e..9383f185ae9 100644 --- a/test/EFCore.Relational.Tests/RelationalConnectionTest.cs +++ b/test/EFCore.Relational.Tests/RelationalConnectionTest.cs @@ -482,7 +482,7 @@ public async Task Existing_connection_can_be_opened_and_closed_externally_async( Assert.Equal(1, dbConnection.OpenCount); - connection.Close(); + await connection.CloseAsync(); Assert.Equal(1, dbConnection.OpenCount); Assert.Equal(1, dbConnection.CloseCount); @@ -494,7 +494,7 @@ public async Task Existing_connection_can_be_opened_and_closed_externally_async( Assert.Equal(1, dbConnection.OpenCount); Assert.Equal(1, dbConnection.CloseCount); - connection.Close(); + await connection.CloseAsync(); Assert.Equal(1, dbConnection.OpenCount); Assert.Equal(1, dbConnection.CloseCount); @@ -506,7 +506,7 @@ public async Task Existing_connection_can_be_opened_and_closed_externally_async( Assert.Equal(2, dbConnection.OpenCount); Assert.Equal(1, dbConnection.CloseCount); - connection.Close(); + await connection.CloseAsync(); Assert.Equal(2, dbConnection.OpenCount); Assert.Equal(2, dbConnection.CloseCount); @@ -520,7 +520,7 @@ public async Task Existing_connection_can_be_opened_and_closed_externally_async( dbConnection.SetState(ConnectionState.Closed); - connection.Close(); + await connection.CloseAsync(); Assert.Equal(2, dbConnection.OpenCount); Assert.Equal(2, dbConnection.CloseCount); @@ -539,7 +539,7 @@ public async Task Existing_connection_can_be_opened_and_closed_externally_async( dbConnection.SetState(ConnectionState.Closed); - connection.Close(); + await connection.CloseAsync(); Assert.Equal(4, dbConnection.OpenCount); Assert.Equal(2, dbConnection.CloseCount); diff --git a/test/EFCore.Relational.Tests/RelationalDatabaseFacadeExtensionsTest.cs b/test/EFCore.Relational.Tests/RelationalDatabaseFacadeExtensionsTest.cs index 2514b6d80a1..5267a497016 100644 --- a/test/EFCore.Relational.Tests/RelationalDatabaseFacadeExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/RelationalDatabaseFacadeExtensionsTest.cs @@ -70,16 +70,26 @@ public async Task Can_open_the_underlying_connection(bool async) } } - [ConditionalFact] - public void Can_close_the_underlying_connection() + [ConditionalTheory] + [InlineData(true)] + [InlineData(false)] + public async Task Can_close_the_underlying_connection(bool async) { var dbConnection = new FakeDbConnection("A=B"); var context = RelationalTestHelpers.Instance.CreateContext(); ((FakeRelationalConnection)context.GetService()).UseConnection(dbConnection); - context.Database.OpenConnection(); - context.Database.CloseConnection(); + if (async) + { + await context.Database.OpenConnectionAsync(); + await context.Database.CloseConnectionAsync(); + } + else + { + context.Database.OpenConnection(); + context.Database.CloseConnection(); + } Assert.Equal(1, dbConnection.CloseCount); } diff --git a/test/EFCore.Relational.Tests/RelationalEventIdTest.cs b/test/EFCore.Relational.Tests/RelationalEventIdTest.cs index baf43c1dfd1..81912548095 100644 --- a/test/EFCore.Relational.Tests/RelationalEventIdTest.cs +++ b/test/EFCore.Relational.Tests/RelationalEventIdTest.cs @@ -142,13 +142,14 @@ private class FakeRelationalConnection : IRelationalConnection { public string ConnectionString => throw new NotImplementedException(); public DbConnection DbConnection => new FakeDbConnection(); + public DbContext Context => null; public Guid ConnectionId => Guid.NewGuid(); public int? CommandTimeout { get; set; } + public Task CloseAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); public bool IsMultipleActiveResultSetsEnabled => throw new NotImplementedException(); public IDbContextTransaction CurrentTransaction => throw new NotImplementedException(); public Transaction EnlistedTransaction { get; } public void EnlistTransaction(Transaction transaction) => throw new NotImplementedException(); - public SemaphoreSlim Semaphore => throw new NotImplementedException(); public IDbContextTransaction BeginTransaction(System.Data.IsolationLevel isolationLevel) => throw new NotImplementedException(); public IDbContextTransaction BeginTransaction() => throw new NotImplementedException(); @@ -170,6 +171,8 @@ public Task OpenAsync(CancellationToken cancellationToken, bool errorsExpe public void ResetState() => throw new NotImplementedException(); public void RollbackTransaction() => throw new NotImplementedException(); public IDbContextTransaction UseTransaction(DbTransaction transaction) => throw new NotImplementedException(); + public Task UseTransactionAsync( + DbTransaction transaction, CancellationToken cancellationToken = default) => throw new NotImplementedException(); } private class FakeDbConnection : DbConnection diff --git a/test/EFCore.Relational.Tests/Storage/RelationalTransactionExtensionsTest.cs b/test/EFCore.Relational.Tests/Storage/RelationalTransactionExtensionsTest.cs index 77e38069abc..3e97b56ca24 100644 --- a/test/EFCore.Relational.Tests/Storage/RelationalTransactionExtensionsTest.cs +++ b/test/EFCore.Relational.Tests/Storage/RelationalTransactionExtensionsTest.cs @@ -3,6 +3,8 @@ using System; using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Internal; @@ -29,6 +31,7 @@ public void GetDbTransaction_returns_the_DbTransaction() var transaction = new RelationalTransaction( connection, dbTransaction, + new Guid(), new DiagnosticsLogger( loggerFactory, new LoggingOptions(), @@ -54,20 +57,11 @@ private class NonRelationalTransaction : IDbContextTransaction { public Guid TransactionId { get; } = Guid.NewGuid(); - public void Commit() - { - throw new NotImplementedException(); - } - - public void Dispose() - { - throw new NotImplementedException(); - } - - public void Rollback() - { - throw new NotImplementedException(); - } + public void Commit() => throw new NotImplementedException(); + public void Dispose() => throw new NotImplementedException(); + public void Rollback() => throw new NotImplementedException(); + public Task CommitAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task RollbackAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); } private const string ConnectionString = "Fake Connection String"; diff --git a/test/EFCore.Relational.Tests/TestUtilities/FakeDiagnosticsLogger.cs b/test/EFCore.Relational.Tests/TestUtilities/FakeDiagnosticsLogger.cs index 2bad1258bb1..d2f0f18e182 100644 --- a/test/EFCore.Relational.Tests/TestUtilities/FakeDiagnosticsLogger.cs +++ b/test/EFCore.Relational.Tests/TestUtilities/FakeDiagnosticsLogger.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.Extensions.Logging; diff --git a/test/EFCore.Relational.Tests/TestUtilities/FakeProvider/FakeRelationalConnection.cs b/test/EFCore.Relational.Tests/TestUtilities/FakeProvider/FakeRelationalConnection.cs index f6bddf023dc..24994d6fb10 100644 --- a/test/EFCore.Relational.Tests/TestUtilities/FakeProvider/FakeRelationalConnection.cs +++ b/test/EFCore.Relational.Tests/TestUtilities/FakeProvider/FakeRelationalConnection.cs @@ -33,7 +33,12 @@ public FakeRelationalConnection(IDbContextOptions options = null) new DiagnosticListener("FakeDiagnosticListener"), new TestRelationalLoggingDefinitions()), new NamedConnectionStringResolver(options ?? CreateOptions()), - new RelationalTransactionFactory(new RelationalTransactionFactoryDependencies()))) + new RelationalTransactionFactory(new RelationalTransactionFactoryDependencies()), + new CurrentDbContext(new FakeDbContext()))) + { + } + + private class FakeDbContext : DbContext { } diff --git a/test/EFCore.Specification.Tests/InterceptionTestBase.cs b/test/EFCore.Specification.Tests/InterceptionTestBase.cs new file mode 100644 index 00000000000..c65f02a87f9 --- /dev/null +++ b/test/EFCore.Specification.Tests/InterceptionTestBase.cs @@ -0,0 +1,139 @@ +// 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.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public abstract class InterceptionTestBase + { + protected InterceptionTestBase(InterceptionFixtureBase fixture) + { + Fixture = fixture; + } + + protected InterceptionFixtureBase Fixture { get; } + + protected class Singularity + { + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + public string Type { get; set; } + } + + protected class Brane + { + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + + public string Type { get; set; } + } + + public class UniverseContext : PoolableDbContext + { + public UniverseContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder + .Entity() + .HasData( + new Singularity + { + Id = 77, Type = "Black Hole" + }, + new Singularity + { + Id = 88, Type = "Bing Bang" + }); + + modelBuilder + .Entity() + .HasData( + new Brane + { + Id = 77, Type = "Black Hole?" + }, + new Brane + { + Id = 88, Type = "Bing Bang?" + }); + } + } + + protected void AssertSql(string expected, string actual) + => Assert.Equal( + expected, + actual.Replace("\r", string.Empty).Replace("\n", " ")); + + protected (DbContext, TInterceptor) CreateContext(bool inject = false) + where TInterceptor : class, IInterceptor, new() + { + var interceptor = new TInterceptor(); + + var context = inject ? CreateContext(null, interceptor) : CreateContext(interceptor); + + return (context, interceptor); + } + + public UniverseContext CreateContext(IInterceptor appInterceptor, string connectionString) + => new UniverseContext( + Fixture.CreateOptions( + new[] + { + appInterceptor + }, Enumerable.Empty())); + + public UniverseContext CreateContext(IInterceptor appInterceptor, params IInterceptor[] injectedInterceptors) + => new UniverseContext( + Fixture.CreateOptions( + new[] + { + appInterceptor + }, injectedInterceptors)); + + public UniverseContext CreateContext( + IEnumerable appInterceptors, + IEnumerable injectedInterceptors = null) + => new UniverseContext(Fixture.CreateOptions(appInterceptors, injectedInterceptors ?? Enumerable.Empty())); + + public abstract class InterceptionFixtureBase : SharedStoreFixtureBase + { + public virtual DbContextOptions CreateOptions( + IEnumerable appInterceptors, + IEnumerable injectedInterceptors) + => base.AddOptions( + TestStore + .AddProviderOptions( + new DbContextOptionsBuilder() + .AddInterceptors(appInterceptors) + .UseInternalServiceProvider( + InjectInterceptors(new ServiceCollection(), injectedInterceptors) + .BuildServiceProvider()))) + .EnableDetailedErrors() + .Options; + + protected virtual IServiceCollection InjectInterceptors( + IServiceCollection serviceCollection, + IEnumerable injectedInterceptors) + { + foreach (var interceptor in injectedInterceptors) + { + serviceCollection.AddSingleton(interceptor); + } + + return serviceCollection; + } + } + } +} diff --git a/test/EFCore.Specification.Tests/TestUtilities/TestLogger`.cs b/test/EFCore.Specification.Tests/TestUtilities/TestLogger`.cs index b5499345566..cfa717af60f 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/TestLogger`.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/TestLogger`.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; namespace Microsoft.EntityFrameworkCore.TestUtilities { diff --git a/test/EFCore.SqlServer.FunctionalTests/InterceptionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/CommandInterceptionSqlServerTest.cs similarity index 50% rename from test/EFCore.SqlServer.FunctionalTests/InterceptionSqlServerTest.cs rename to test/EFCore.SqlServer.FunctionalTests/CommandInterceptionSqlServerTest.cs index ecd7074257a..b602ded7c12 100644 --- a/test/EFCore.SqlServer.FunctionalTests/InterceptionSqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/CommandInterceptionSqlServerTest.cs @@ -1,23 +1,19 @@ // 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.Collections.Generic; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace Microsoft.EntityFrameworkCore { - public class InterceptionSqlServerTest - : InterceptionTestBase, - IClassFixture + public class CommandInterceptionSqlServerTest + : CommandInterceptionTestBase, IClassFixture { - private const string DatabaseName = "Interception"; - - public InterceptionSqlServerTest(InterceptionSqlServerFixture fixture) + public CommandInterceptionSqlServerTest(InterceptionSqlServerFixture fixture) : base(fixture) { } @@ -51,26 +47,14 @@ public override async Task Intercept_query_to_replace_execution(bool asy public class InterceptionSqlServerFixture : InterceptionFixtureBase { - protected override string StoreName => DatabaseName; + protected override string StoreName => "CommandInterception"; protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; - public override DbContextOptions AddRelationalOptions( - Action> relationalBuilder, - Type[] injectedInterceptorTypes) - => AddOptions( - ((SqlServerTestStore)TestStore) - .AddProviderOptions( - new DbContextOptionsBuilder() - .UseInternalServiceProvider( - InjectInterceptors( - new ServiceCollection() - .AddEntityFrameworkSqlServer(), - injectedInterceptorTypes) - .BuildServiceProvider()), - relationalBuilder)) - .EnableDetailedErrors() - .Options; + protected override IServiceCollection InjectInterceptors( + IServiceCollection serviceCollection, + IEnumerable injectedInterceptors) + => base.InjectInterceptors(serviceCollection.AddEntityFrameworkSqlServer(), injectedInterceptors); } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/ConnectionInterceptionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/ConnectionInterceptionSqlServerTest.cs new file mode 100644 index 00000000000..178a1e6e5ad --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/ConnectionInterceptionSqlServerTest.cs @@ -0,0 +1,35 @@ +// 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.Collections.Generic; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public class ConnectionInterceptionSqlServerTest + : ConnectionInterceptionTestBase, IClassFixture + { + public ConnectionInterceptionSqlServerTest(InterceptionSqlServerFixture fixture) + : base(fixture) + { + } + + public class InterceptionSqlServerFixture : InterceptionFixtureBase + { + protected override string StoreName => "ConnectionInterception"; + + protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; + + protected override IServiceCollection InjectInterceptors( + IServiceCollection serviceCollection, + IEnumerable injectedInterceptors) + => base.InjectInterceptors(serviceCollection.AddEntityFrameworkSqlServer(), injectedInterceptors); + } + + protected override BadUniverseContext CreateBadUniverse(DbContextOptionsBuilder optionsBuilder) + => new BadUniverseContext(optionsBuilder.UseSqlServer("Database = IIzBad").Options); + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/ExecutionStrategyTest.cs b/test/EFCore.SqlServer.FunctionalTests/ExecutionStrategyTest.cs index 3ad761f139c..f48cbd455a0 100644 --- a/test/EFCore.SqlServer.FunctionalTests/ExecutionStrategyTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/ExecutionStrategyTest.cs @@ -366,7 +366,14 @@ public async Task Retries_only_on_true_execution_failure(bool realFailure, bool if (openConnection) { - context.Database.CloseConnection(); + if (async) + { + context.Database.CloseConnection(); + } + else + { + await context.Database.CloseConnectionAsync(); + } } Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); @@ -465,7 +472,14 @@ public async Task Retries_OpenConnection_on_execution_failure(bool async) Assert.Equal(2, connection.OpenCount); - context.Database.CloseConnection(); + if (async) + { + context.Database.CloseConnection(); + } + else + { + await context.Database.CloseConnectionAsync(); + } Assert.Equal(ConnectionState.Closed, context.Database.GetDbConnection().State); } diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestRelationalTransaction.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestRelationalTransaction.cs index 13518350202..908d06cd7c2 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestRelationalTransaction.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/TestRelationalTransaction.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Data.Common; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Storage; @@ -12,6 +13,7 @@ public class TestRelationalTransactionFactory : IRelationalTransactionFactory public RelationalTransaction Create( IRelationalConnection connection, DbTransaction transaction, + Guid transactionId, IDiagnosticsLogger logger, bool transactionOwned) => new TestRelationalTransaction(connection, transaction, logger, transactionOwned); @@ -26,7 +28,7 @@ public TestRelationalTransaction( DbTransaction transaction, IDiagnosticsLogger logger, bool transactionOwned) - : base(connection, transaction, logger, transactionOwned) + : base(connection, transaction, new Guid(), logger, transactionOwned) { _testConnection = (TestSqlServerConnection)connection; } diff --git a/test/EFCore.SqlServer.FunctionalTests/TransactionInterceptionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/TransactionInterceptionSqlServerTest.cs new file mode 100644 index 00000000000..48df00cf086 --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/TransactionInterceptionSqlServerTest.cs @@ -0,0 +1,32 @@ +// 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.Collections.Generic; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public class TransactionInterceptionSqlServerTest + : TransactionInterceptionTestBase, IClassFixture + { + public TransactionInterceptionSqlServerTest(InterceptionSqlServerFixture fixture) + : base(fixture) + { + } + + public class InterceptionSqlServerFixture : InterceptionFixtureBase + { + protected override string StoreName => "TransactionInterception"; + + protected override ITestStoreFactory TestStoreFactory => SqlServerTestStoreFactory.Instance; + + protected override IServiceCollection InjectInterceptors( + IServiceCollection serviceCollection, + IEnumerable injectedInterceptors) + => base.InjectInterceptors(serviceCollection.AddEntityFrameworkSqlServer(), injectedInterceptors); + } + } +} diff --git a/test/EFCore.SqlServer.Tests/SqlServerConnectionTest.cs b/test/EFCore.SqlServer.Tests/SqlServerConnectionTest.cs index 1b08da8a5c0..5c3a9aa69a7 100644 --- a/test/EFCore.SqlServer.Tests/SqlServerConnectionTest.cs +++ b/test/EFCore.SqlServer.Tests/SqlServerConnectionTest.cs @@ -75,10 +75,9 @@ public void Master_connection_string_none_default_command_timeout() public static RelationalConnectionDependencies CreateDependencies(DbContextOptions options = null) { - options = options - ?? new DbContextOptionsBuilder() - .UseSqlServer(@"Server=(localdb)\MSSQLLocalDB;Database=SqlServerConnectionTest") - .Options; + options ??= new DbContextOptionsBuilder() + .UseSqlServer(@"Server=(localdb)\MSSQLLocalDB;Database=SqlServerConnectionTest") + .Options; return new RelationalConnectionDependencies( options, @@ -93,7 +92,12 @@ public static RelationalConnectionDependencies CreateDependencies(DbContextOptio new DiagnosticListener("FakeDiagnosticListener"), new SqlServerLoggingDefinitions()), new NamedConnectionStringResolver(options), - new RelationalTransactionFactory(new RelationalTransactionFactoryDependencies())); + new RelationalTransactionFactory(new RelationalTransactionFactoryDependencies()), + new CurrentDbContext(new FakeDbContext())); + } + + private class FakeDbContext : DbContext + { } } } diff --git a/test/EFCore.Sqlite.FunctionalTests/InterceptionSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/CommandInterceptionSqliteTest.cs similarity index 51% rename from test/EFCore.Sqlite.FunctionalTests/InterceptionSqliteTest.cs rename to test/EFCore.Sqlite.FunctionalTests/CommandInterceptionSqliteTest.cs index c5bd55e3091..78bf91975e7 100644 --- a/test/EFCore.Sqlite.FunctionalTests/InterceptionSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/CommandInterceptionSqliteTest.cs @@ -1,23 +1,19 @@ // 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.Collections.Generic; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Sqlite.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.TestUtilities; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace Microsoft.EntityFrameworkCore { - public class InterceptionSqliteTest - : InterceptionTestBase, - IClassFixture + public class CommandInterceptionSqliteTest + : CommandInterceptionTestBase, IClassFixture { - private const string DatabaseName = "Interception"; - - public InterceptionSqliteTest(InterceptionSqliteFixture fixture) + public CommandInterceptionSqliteTest(InterceptionSqliteFixture fixture) : base(fixture) { } @@ -51,26 +47,14 @@ public override async Task Intercept_query_to_replace_execution(bool asy public class InterceptionSqliteFixture : InterceptionFixtureBase { - protected override string StoreName => DatabaseName; + protected override string StoreName => "CommandInterception"; protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; - public override DbContextOptions AddRelationalOptions( - Action> relationalBuilder, - Type[] injectedInterceptorTypes) - => AddOptions( - ((SqliteTestStore)TestStore) - .AddProviderOptions( - new DbContextOptionsBuilder() - .UseInternalServiceProvider( - InjectInterceptors( - new ServiceCollection() - .AddEntityFrameworkSqlite(), - injectedInterceptorTypes) - .BuildServiceProvider()), - relationalBuilder)) - .EnableDetailedErrors() - .Options; + protected override IServiceCollection InjectInterceptors( + IServiceCollection serviceCollection, + IEnumerable injectedInterceptors) + => base.InjectInterceptors(serviceCollection.AddEntityFrameworkSqlite(), injectedInterceptors); } } } diff --git a/test/EFCore.Sqlite.FunctionalTests/ConnectionInterceptionSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/ConnectionInterceptionSqliteTest.cs new file mode 100644 index 00000000000..2086e714663 --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/ConnectionInterceptionSqliteTest.cs @@ -0,0 +1,35 @@ +// 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.Collections.Generic; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public class ConnectionInterceptionSqliteTest + : ConnectionInterceptionTestBase, IClassFixture + { + public ConnectionInterceptionSqliteTest(InterceptionSqliteFixture fixture) + : base(fixture) + { + } + + protected override BadUniverseContext CreateBadUniverse(DbContextOptionsBuilder optionsBuilder) + => new BadUniverseContext(optionsBuilder.UseSqlite("Data Source=file:data.db?mode=invalidmode").Options); + + public class InterceptionSqliteFixture : InterceptionFixtureBase + { + protected override string StoreName => "ConnectionInterception"; + + protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; + + protected override IServiceCollection InjectInterceptors( + IServiceCollection serviceCollection, + IEnumerable injectedInterceptors) + => base.InjectInterceptors(serviceCollection.AddEntityFrameworkSqlite(), injectedInterceptors); + } + } +} diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/BadDataSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/BadDataSqliteTest.cs index 54a1f497384..f3a699a13dc 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/BadDataSqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/BadDataSqliteTest.cs @@ -374,6 +374,7 @@ public void RollbackTransaction() public string ConnectionString { get; } public DbConnection DbConnection { get; } + public DbContext Context => null; public Guid ConnectionId { get; } public int? CommandTimeout { get; set; } public bool Open(bool errorsExpected = false) => true; @@ -382,6 +383,9 @@ public Task OpenAsync(CancellationToken cancellationToken, bool errorsExpe throw new NotImplementedException(); public bool Close() => true; + + public Task CloseAsync(CancellationToken cancellationToken = default) => Task.FromResult(true); + public bool IsMultipleActiveResultSetsEnabled { get; } public IDbContextTransaction BeginTransaction(System.Data.IsolationLevel isolationLevel) => throw new NotImplementedException(); @@ -390,6 +394,9 @@ public Task BeginTransactionAsync( public IDbContextTransaction UseTransaction(DbTransaction transaction) => throw new NotImplementedException(); + public Task UseTransactionAsync( + DbTransaction transaction, CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public void Dispose() { } diff --git a/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteDatabaseCleaner.cs b/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteDatabaseCleaner.cs index 3745a667204..84b4db66f5c 100644 --- a/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteDatabaseCleaner.cs +++ b/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteDatabaseCleaner.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Diagnostics.Internal; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Internal; using Microsoft.EntityFrameworkCore.Scaffolding; @@ -31,7 +32,6 @@ protected override IDatabaseModelFactory CreateDatabaseModelFactory(ILoggerFacto .AddSingleton(typeof(IDiagnosticsLogger<>), typeof(DiagnosticsLogger<>)) .AddSingleton() .AddSingleton() - .AddSingleton() .AddLogging(); new SqliteDesignTimeServices().ConfigureDesignTimeServices(services); diff --git a/test/EFCore.Sqlite.FunctionalTests/TransactionInterceptionSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/TransactionInterceptionSqliteTest.cs new file mode 100644 index 00000000000..f231967150a --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/TransactionInterceptionSqliteTest.cs @@ -0,0 +1,32 @@ +// 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.Collections.Generic; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public class TransactionInterceptionSqliteTest + : TransactionInterceptionTestBase, IClassFixture + { + public TransactionInterceptionSqliteTest(InterceptionSqliteFixture fixture) + : base(fixture) + { + } + + public class InterceptionSqliteFixture : InterceptionFixtureBase + { + protected override string StoreName => "TransactionInterception"; + + protected override ITestStoreFactory TestStoreFactory => SqliteTestStoreFactory.Instance; + + protected override IServiceCollection InjectInterceptors( + IServiceCollection serviceCollection, + IEnumerable injectedInterceptors) + => base.InjectInterceptors(serviceCollection.AddEntityFrameworkSqlite(), injectedInterceptors); + } + } +} diff --git a/test/EFCore.Tests/DatabaseFacadeTest.cs b/test/EFCore.Tests/DatabaseFacadeTest.cs index e91e4c04b36..5ed1063ad5b 100644 --- a/test/EFCore.Tests/DatabaseFacadeTest.cs +++ b/test/EFCore.Tests/DatabaseFacadeTest.cs @@ -179,6 +179,8 @@ private class FakeDbContextTransaction : IDbContextTransaction public Guid TransactionId { get; } public void Commit() => throw new NotImplementedException(); public void Rollback() => throw new NotImplementedException(); + public Task CommitAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public Task RollbackAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); } [ConditionalFact] diff --git a/test/EFCore.Tests/Infrastructure/InterceptorsDependenciesTest.cs b/test/EFCore.Tests/Infrastructure/InterceptorsDependenciesTest.cs deleted file mode 100644 index 20bcc5ff669..00000000000 --- a/test/EFCore.Tests/Infrastructure/InterceptorsDependenciesTest.cs +++ /dev/null @@ -1,18 +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.Diagnostics; -using Microsoft.EntityFrameworkCore.TestUtilities; -using Xunit; - -namespace Microsoft.EntityFrameworkCore.Infrastructure -{ - public class InterceptorsDependenciesTest - { - [Fact] - public void Can_use_With_methods_to_clone_and_replace_service() - { - InMemoryTestHelpers.Instance.TestDependenciesClone(); - } - } -} diff --git a/test/EFCore.Tests/TestUtilities/TestInMemoryTransactionManager.cs b/test/EFCore.Tests/TestUtilities/TestInMemoryTransactionManager.cs index f81d659b56e..95ead109a4a 100644 --- a/test/EFCore.Tests/TestUtilities/TestInMemoryTransactionManager.cs +++ b/test/EFCore.Tests/TestUtilities/TestInMemoryTransactionManager.cs @@ -62,6 +62,20 @@ public void Rollback() { TransactionManager._currentTransaction = null; } + + public Task CommitAsync(CancellationToken cancellationToken = default) + { + TransactionManager._currentTransaction = null; + + return Task.CompletedTask; + } + + public Task RollbackAsync(CancellationToken cancellationToken = default) + { + TransactionManager._currentTransaction = null; + + return Task.CompletedTask; + } } } }