diff --git a/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs b/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs index 6c81a6b8bb9..78267278c5f 100644 --- a/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs +++ b/src/EFCore.Design/Design/DesignTimeServiceCollectionExtensions.cs @@ -56,6 +56,7 @@ public static IServiceCollection AddEntityFrameworkDesignTimeServices( .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -65,6 +66,7 @@ public static IServiceCollection AddEntityFrameworkDesignTimeServices( .AddSingleton() .AddSingleton() .AddSingleton(typeof(IDiagnosticsLogger<>), typeof(DiagnosticsLogger<>)) + .AddSingleton() .AddSingleton(new DiagnosticListener(DbLoggerCategory.Name)) .AddSingleton() .AddSingleton() diff --git a/src/EFCore.Relational/Diagnostics/CompositeDbCommandInterceptor.cs b/src/EFCore.Relational/Diagnostics/CompositeDbCommandInterceptor.cs new file mode 100644 index 00000000000..07bbd5ffe7d --- /dev/null +++ b/src/EFCore.Relational/Diagnostics/CompositeDbCommandInterceptor.cs @@ -0,0 +1,318 @@ +// 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 +{ + public class CompositeDbCommandInterceptor : IDbCommandInterceptor + { + private readonly List _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) + : this((IReadOnlyList)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) + { + Check.NotNull(interceptors, nameof(interceptors)); + + _interceptors = interceptors.ToList(); + } + + /// + /// 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.Count; 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.Count; 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.Count; 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.Count; 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.Count; 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.Count; 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.Count; 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.Count; 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.Count; 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.Count; 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.Count; 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.Count; i++) + { + result = await _interceptors[i].NonQueryExecutedAsync(command, eventData, result, cancellationToken); + } + + return result; + } + } +} diff --git a/src/EFCore.Relational/Diagnostics/DbCommandInterceptor.cs b/src/EFCore.Relational/Diagnostics/DbCommandInterceptor.cs new file mode 100644 index 00000000000..224c0098dca --- /dev/null +++ b/src/EFCore.Relational/Diagnostics/DbCommandInterceptor.cs @@ -0,0 +1,331 @@ +// 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 DbCommandInterceptor : IDbCommandInterceptor + { + /// + /// Called just before EF intends to call . + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 execute the command as normal. + /// If non-null, then command execution 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? ReaderExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + { + return result; + } + + /// + /// Called just before EF intends to call . + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 execute the command as normal. + /// If non-null, then command execution 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? ScalarExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) => result; + + /// + /// Called just before EF intends to call . + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 execute the command as normal. + /// If non-null, then command execution 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? NonQueryExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + => result; + + /// + /// Called just before EF intends to call . + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 execute the command as normal. + /// If the result is non-null value, then command execution 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?> ReaderExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + => Task.FromResult(result); + + /// + /// Called just before EF intends to call . + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 execute the command as normal. + /// If the result is non-null value, then command execution 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?> ScalarExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + => Task.FromResult(result); + + /// + /// Called just before EF intends to call . + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 execute the command as normal. + /// If the result is non-null value, then command execution 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?> NonQueryExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + => Task.FromResult(result); + + /// + /// + /// Called immediately after EF calls . + /// + /// + /// This method is still called if an interceptor suppressed execution of a command in . + /// In this case, is the result returned by . + /// + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 DbDataReader ReaderExecuted( + DbCommand command, + CommandExecutedEventData eventData, + DbDataReader result) + => result; + + /// + /// + /// Called immediately after EF calls . + /// + /// + /// This method is still called if an interceptor suppressed execution of a command in . + /// In this case, is the result returned by . + /// + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 object ScalarExecuted( + DbCommand command, + CommandExecutedEventData eventData, + object result) + => result; + + /// + /// + /// Called immediately after EF calls . + /// + /// + /// This method is still called if an interceptor suppressed execution of a command in . + /// In this case, is the result returned by . + /// + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 int NonQueryExecuted( + DbCommand command, + CommandExecutedEventData eventData, + int result) + => result; + + /// + /// + /// Called immediately after EF calls . + /// + /// + /// This method is still called if an interceptor suppressed execution of a command in . + /// In this case, is the result returned by . + /// + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 proving 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 ReaderExecutedAsync( + DbCommand command, + CommandExecutedEventData eventData, + DbDataReader result, + CancellationToken cancellationToken = default) + => Task.FromResult(result); + + /// + /// + /// Called immediately after EF calls . + /// + /// + /// This method is still called if an interceptor suppressed execution of a command in . + /// In this case, is the result returned by . + /// + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 proving 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 ScalarExecutedAsync( + DbCommand command, + CommandExecutedEventData eventData, + object result, + CancellationToken cancellationToken = default) + => Task.FromResult(result); + + /// + /// + /// Called immediately after EF calls . + /// + /// + /// This method is still called if an interceptor suppressed execution of a command in . + /// In this case, is the result returned by . + /// + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 proving 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 NonQueryExecutedAsync( + DbCommand command, + CommandExecutedEventData eventData, + int result, + CancellationToken cancellationToken = default) + => Task.FromResult(result); + } +} diff --git a/src/EFCore.Relational/Diagnostics/IDbCommandInterceptor.cs b/src/EFCore.Relational/Diagnostics/IDbCommandInterceptor.cs new file mode 100644 index 00000000000..ced429666b7 --- /dev/null +++ b/src/EFCore.Relational/Diagnostics/IDbCommandInterceptor.cs @@ -0,0 +1,338 @@ +// 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; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace Microsoft.EntityFrameworkCore.Diagnostics +{ + /// + /// + /// Allows interception of commands sent to a relational database. + /// + /// + /// Command interceptors can be used to view, change, or suppress execution of the , and + /// to modify the result before it is returned to EF. + /// + /// + /// Consider inheriting from if not implementing all methods. + /// + /// + /// Use to register + /// an application interceptor. + /// + /// + /// Multiple interceptors can be composed using the . + /// + /// + /// 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. + /// + /// + public interface IDbCommandInterceptor + { + /// + /// Called just before EF intends to call . + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 execute the command as normal. + /// If non-null, then command execution 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? ReaderExecuting( + [NotNull] DbCommand command, + [NotNull] CommandEventData eventData, + InterceptionResult? result); + + /// + /// Called just before EF intends to call . + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 execute the command as normal. + /// If non-null, then command execution 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? ScalarExecuting( + [NotNull] DbCommand command, + [NotNull] CommandEventData eventData, + InterceptionResult? result); + + /// + /// Called just before EF intends to call . + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 execute the command as normal. + /// If non-null, then command execution 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? NonQueryExecuting( + [NotNull] DbCommand command, + [NotNull] CommandEventData eventData, + InterceptionResult? result); + + /// + /// Called just before EF intends to call . + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 execute the command as normal. + /// If the result is non-null value, then command execution 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?> ReaderExecutingAsync( + [NotNull] DbCommand command, + [NotNull] CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default); + + /// + /// Called just before EF intends to call . + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 execute the command as normal. + /// If the result is non-null value, then command execution 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?> ScalarExecutingAsync( + [NotNull] DbCommand command, + [NotNull] CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default); + + /// + /// Called just before EF intends to call . + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 execute the command as normal. + /// If the result is non-null value, then command execution 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?> NonQueryExecutingAsync( + [NotNull] DbCommand command, + [NotNull] CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default); + + /// + /// + /// Called immediately after EF calls . + /// + /// + /// This method is still called if an interceptor suppressed execution of a command in . + /// In this case, is the result returned by . + /// + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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. + /// + DbDataReader ReaderExecuted( + [NotNull] DbCommand command, + [NotNull] CommandExecutedEventData eventData, + [NotNull] DbDataReader result); + + /// + /// + /// Called immediately after EF calls . + /// + /// + /// This method is still called if an interceptor suppressed execution of a command in . + /// In this case, is the result returned by . + /// + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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. + /// + object ScalarExecuted( + [NotNull] DbCommand command, + [NotNull] CommandExecutedEventData eventData, + [CanBeNull] object result); + + /// + /// + /// Called immediately after EF calls . + /// + /// + /// This method is still called if an interceptor suppressed execution of a command in . + /// In this case, is the result returned by . + /// + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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. + /// + int NonQueryExecuted( + [NotNull] DbCommand command, + [NotNull] CommandExecutedEventData eventData, + int result); + + /// + /// + /// Called immediately after EF calls . + /// + /// + /// This method is still called if an interceptor suppressed execution of a command in . + /// In this case, is the result returned by . + /// + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 proving 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 ReaderExecutedAsync( + [NotNull] DbCommand command, + [NotNull] CommandExecutedEventData eventData, + [NotNull] DbDataReader result, + CancellationToken cancellationToken = default); + + /// + /// + /// Called immediately after EF calls . + /// + /// + /// This method is still called if an interceptor suppressed execution of a command in . + /// In this case, is the result returned by . + /// + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 proving 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 ScalarExecutedAsync( + [NotNull] DbCommand command, + [NotNull] CommandExecutedEventData eventData, + [CanBeNull] object result, + CancellationToken cancellationToken = default); + + /// + /// + /// Called immediately after EF calls . + /// + /// + /// This method is still called if an interceptor suppressed execution of a command in . + /// In this case, is the result returned by . + /// + /// + /// The command. + /// Contextual information about the command and execution. + /// + /// 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 proving 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 NonQueryExecutedAsync( + [NotNull] DbCommand command, + [NotNull] CommandExecutedEventData eventData, + int result, + CancellationToken cancellationToken = default); + } +} diff --git a/src/EFCore.Relational/Diagnostics/IRelationalInterceptors.cs b/src/EFCore.Relational/Diagnostics/IRelationalInterceptors.cs new file mode 100644 index 00000000000..b7d8935173c --- /dev/null +++ b/src/EFCore.Relational/Diagnostics/IRelationalInterceptors.cs @@ -0,0 +1,34 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using 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/RelationalInterceptors.cs b/src/EFCore.Relational/Diagnostics/RelationalInterceptors.cs new file mode 100644 index 00000000000..6dc34d71a19 --- /dev/null +++ b/src/EFCore.Relational/Diagnostics/RelationalInterceptors.cs @@ -0,0 +1,110 @@ +// 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(); + var injectedCount = injectedInterceptors.Count; + var appInterceptor = FindAppInterceptor(); + + if (injectedCount == 0) + { + _interceptor = appInterceptor; + } + else + { + if (appInterceptor == null) + { + _interceptor = injectedCount == 1 + ? injectedInterceptors[0] + : new CompositeDbCommandInterceptor(injectedInterceptors); + } + else + { + injectedInterceptors.Add(appInterceptor); + _interceptor = new CompositeDbCommandInterceptor(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 IDbCommandInterceptor FindAppInterceptor() + => Dependencies + .ServiceProvider + .GetService() + .Extensions + .OfType() + .FirstOrDefault() + ?.CommandInterceptor; + } +} diff --git a/src/EFCore.Relational/Diagnostics/RelationalInterceptorsDependencies.cs b/src/EFCore.Relational/Diagnostics/RelationalInterceptorsDependencies.cs new file mode 100644 index 00000000000..c285b191dc5 --- /dev/null +++ b/src/EFCore.Relational/Diagnostics/RelationalInterceptorsDependencies.cs @@ -0,0 +1,70 @@ +// 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 2e6021a739f..ee9ae454bb8 100644 --- a/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs +++ b/src/EFCore.Relational/Diagnostics/RelationalLoggerExtensions.cs @@ -9,16 +9,16 @@ using System.Globalization; using System.Linq.Expressions; using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using System.Transactions; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Diagnostics.Internal; -using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations.Internal; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage.Internal; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.EntityFrameworkCore.Update; using Microsoft.Extensions.Logging; @@ -42,22 +42,320 @@ public static class RelationalLoggerExtensions /// /// The diagnostics logger to use. /// The database command object. - /// Represents the method that will be called to execute the command. /// The correlation ID associated with the given . /// The correlation ID associated with the being used. - /// Indicates whether or not this is an async operation. /// The time that execution began. - public static void CommandExecuting( + /// An intercepted result, or null if the result was not intercepted. + public static InterceptionResult? CommandReaderExecuting( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] DbCommand command, + Guid commandId, + Guid connectionId, + DateTimeOffset startTime) + { + var definition = RelationalResources.LogExecutingCommand(diagnostics); + + LogCommandExecuting(diagnostics, command, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastCommandExecuting( + diagnostics, + command, + DbCommandMethod.ExecuteReader, + commandId, + connectionId, + false, + startTime, + definition, + diagnosticSourceEnabled); + + if (interceptor != null) + { + return interceptor.ReaderExecuting(command, eventData, null); + } + } + + return null; + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The database command object. + /// The correlation ID associated with the given . + /// The correlation ID associated with the being used. + /// The time that execution began. + /// An intercepted result, or null if the result was not intercepted. + public static InterceptionResult? CommandScalarExecuting( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] DbCommand command, + Guid commandId, + Guid connectionId, + DateTimeOffset startTime) + { + var definition = RelationalResources.LogExecutingCommand(diagnostics); + + LogCommandExecuting(diagnostics, command, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastCommandExecuting( + diagnostics, + command, + DbCommandMethod.ExecuteScalar, + commandId, + connectionId, + false, + startTime, + definition, + diagnosticSourceEnabled); + + if (interceptor != null) + { + return interceptor.ScalarExecuting(command, eventData, null); + } + } + + return null; + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The database command object. + /// The correlation ID associated with the given . + /// The correlation ID associated with the being used. + /// The time that execution began. + /// An intercepted result, or null if the result was not intercepted. + public static InterceptionResult? CommandNonQueryExecuting( [NotNull] this IDiagnosticsLogger diagnostics, [NotNull] DbCommand command, - DbCommandMethod executeMethod, Guid commandId, Guid connectionId, - bool async, DateTimeOffset startTime) { var definition = RelationalResources.LogExecutingCommand(diagnostics); + LogCommandExecuting(diagnostics, command, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastCommandExecuting( + diagnostics, + command, + DbCommandMethod.ExecuteNonQuery, + commandId, + connectionId, + false, + startTime, + definition, + diagnosticSourceEnabled); + + if (interceptor != null) + { + return interceptor.NonQueryExecuting(command, eventData, null); + } + } + + return null; + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The database command object. + /// The correlation ID associated with the given . + /// The correlation ID associated with the being used. + /// The time that execution began. + /// The cancellation token. + /// An intercepted result, or null if the result was not intercepted. + public static Task?> CommandReaderExecutingAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] DbCommand command, + Guid commandId, + Guid connectionId, + DateTimeOffset startTime, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogExecutingCommand(diagnostics); + + LogCommandExecuting(diagnostics, command, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastCommandExecuting( + diagnostics, + command, + DbCommandMethod.ExecuteReader, + commandId, + connectionId, + true, + startTime, + definition, + diagnosticSourceEnabled); + + if (interceptor != null) + { + return interceptor.ReaderExecutingAsync(command, eventData, null, cancellationToken); + } + } + + return Task.FromResult?>(null); + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The database command object. + /// The correlation ID associated with the given . + /// The correlation ID associated with the being used. + /// The time that execution began. + /// The cancellation token. + /// An intercepted result, or null if the result was not intercepted. + public static Task?> CommandScalarExecutingAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] DbCommand command, + Guid commandId, + Guid connectionId, + DateTimeOffset startTime, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogExecutingCommand(diagnostics); + + LogCommandExecuting(diagnostics, command, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastCommandExecuting( + diagnostics, + command, + DbCommandMethod.ExecuteScalar, + commandId, + connectionId, + true, + startTime, + definition, + diagnosticSourceEnabled); + + if (interceptor != null) + { + return interceptor.ScalarExecutingAsync(command, eventData, null, cancellationToken); + } + } + + return Task.FromResult?>(null); + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The database command object. + /// The correlation ID associated with the given . + /// The correlation ID associated with the being used. + /// The time that execution began. + /// The cancellation token. + /// An intercepted result, or null if the result was not intercepted. + public static Task?> CommandNonQueryExecutingAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] DbCommand command, + Guid commandId, + Guid connectionId, + DateTimeOffset startTime, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogExecutingCommand(diagnostics); + + LogCommandExecuting(diagnostics, command, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastCommandExecuting( + diagnostics, + command, + DbCommandMethod.ExecuteNonQuery, + commandId, + connectionId, + true, + startTime, + definition, + diagnosticSourceEnabled); + + if (interceptor != null) + { + return interceptor.NonQueryExecutingAsync(command, eventData, null, cancellationToken); + } + } + + return Task.FromResult?>(null); + } + + private static CommandEventData BroadcastCommandExecuting( + IDiagnosticsLogger diagnostics, + DbCommand command, + DbCommandMethod executeMethod, + Guid commandId, + Guid connectionId, + bool async, + DateTimeOffset startTime, + EventDefinition definition, + bool diagnosticSourceEnabled) + { + var eventData = new CommandEventData( + definition, + CommandExecuting, + command, + executeMethod, + commandId, + connectionId, + async, + ShouldLogParameterValues(diagnostics, command), + startTime); + + if (diagnosticSourceEnabled) + { + diagnostics.DiagnosticSource.Write( + definition.EventId.Name, + eventData); + } + + return eventData; + } + + private static void LogCommandExecuting( + IDiagnosticsLogger diagnostics, + DbCommand command, + EventDefinition definition) + { var warningBehavior = definition.GetLogBehavior(diagnostics); if (warningBehavior != WarningBehavior.Ignore) { @@ -70,22 +368,6 @@ public static void CommandExecuting( Environment.NewLine, command.CommandText.TrimEnd()); } - - if (diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name)) - { - diagnostics.DiagnosticSource.Write( - definition.EventId.Name, - new CommandEventData( - definition, - CommandExecuting, - command, - executeMethod, - commandId, - connectionId, - async, - ShouldLogParameterValues(diagnostics, command), - startTime)); - } } private static string CommandExecuting(EventDefinitionBase definition, EventData payload) @@ -111,26 +393,361 @@ private static bool ShouldLogParameterValues( /// /// The diagnostics logger to use. /// The database command object. - /// Represents the method that will be called to execute the command. /// The correlation ID associated with the given . /// The correlation ID associated with the being used. /// The return value from the underlying method execution. - /// Indicates whether or not this is an async command. /// The time that execution began. /// The duration of the command execution, not including consuming results. - public static void CommandExecuted( + /// The result of execution, which may have been modified by an interceptor. + public static DbDataReader CommandReaderExecuted( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] DbCommand command, + Guid commandId, + Guid connectionId, + [CanBeNull] DbDataReader methodResult, + DateTimeOffset startTime, + TimeSpan duration) + { + var definition = RelationalResources.LogExecutedCommand(diagnostics); + + LogCommandExecuted(diagnostics, command, duration, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastCommandExecuted( + diagnostics, + command, + DbCommandMethod.ExecuteReader, + commandId, + connectionId, + methodResult, + false, + startTime, + duration, + definition, + diagnosticSourceEnabled); + + if (interceptor != null) + { + return interceptor.ReaderExecuted(command, eventData, methodResult); + } + } + + return methodResult; + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The database command object. + /// The correlation ID associated with the given . + /// The correlation ID associated with the being used. + /// The return value from the underlying method execution. + /// The time that execution began. + /// The duration of the command execution, not including consuming results. + /// The result of execution, which may have been modified by an interceptor. + public static object CommandScalarExecuted( [NotNull] this IDiagnosticsLogger diagnostics, [NotNull] DbCommand command, - DbCommandMethod executeMethod, Guid commandId, Guid connectionId, [CanBeNull] object methodResult, - bool async, DateTimeOffset startTime, TimeSpan duration) { var definition = RelationalResources.LogExecutedCommand(diagnostics); + LogCommandExecuted(diagnostics, command, duration, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastCommandExecuted( + diagnostics, + command, + DbCommandMethod.ExecuteScalar, + commandId, + connectionId, + methodResult, + false, + startTime, + duration, + definition, + diagnosticSourceEnabled); + + if (interceptor != null) + { + return interceptor.ScalarExecuted(command, eventData, methodResult); + } + } + + return methodResult; + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The database command object. + /// The correlation ID associated with the given . + /// The correlation ID associated with the being used. + /// The return value from the underlying method execution. + /// The time that execution began. + /// The duration of the command execution, not including consuming results. + /// The result of execution, which may have been modified by an interceptor. + public static int CommandNonQueryExecuted( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] DbCommand command, + Guid commandId, + Guid connectionId, + int methodResult, + DateTimeOffset startTime, + TimeSpan duration) + { + var definition = RelationalResources.LogExecutedCommand(diagnostics); + + LogCommandExecuted(diagnostics, command, duration, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastCommandExecuted( + diagnostics, + command, + DbCommandMethod.ExecuteNonQuery, + commandId, + connectionId, + methodResult, + false, + startTime, + duration, + definition, + diagnosticSourceEnabled); + + if (interceptor != null) + { + return interceptor.NonQueryExecuted(command, eventData, methodResult); + } + } + + return methodResult; + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The database command object. + /// The correlation ID associated with the given . + /// The correlation ID associated with the being used. + /// The return value from the underlying method execution. + /// The time that execution began. + /// The duration of the command execution, not including consuming results. + /// The cancellation token. + /// The result of execution, which may have been modified by an interceptor. + public static Task CommandReaderExecutedAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] DbCommand command, + Guid commandId, + Guid connectionId, + [CanBeNull] DbDataReader methodResult, + DateTimeOffset startTime, + TimeSpan duration, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogExecutedCommand(diagnostics); + + LogCommandExecuted(diagnostics, command, duration, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastCommandExecuted( + diagnostics, + command, + DbCommandMethod.ExecuteReader, + commandId, + connectionId, + methodResult, + true, + startTime, + duration, + definition, + diagnosticSourceEnabled); + + if (interceptor != null) + { + return interceptor.ReaderExecutedAsync(command, eventData, methodResult, cancellationToken); + } + } + + return Task.FromResult(methodResult); + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The database command object. + /// The correlation ID associated with the given . + /// The correlation ID associated with the being used. + /// The return value from the underlying method execution. + /// The time that execution began. + /// The duration of the command execution, not including consuming results. + /// The cancellation token. + /// The result of execution, which may have been modified by an interceptor. + public static Task CommandScalarExecutedAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] DbCommand command, + Guid commandId, + Guid connectionId, + [CanBeNull] object methodResult, + DateTimeOffset startTime, + TimeSpan duration, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogExecutedCommand(diagnostics); + + LogCommandExecuted(diagnostics, command, duration, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastCommandExecuted( + diagnostics, + command, + DbCommandMethod.ExecuteScalar, + commandId, + connectionId, + methodResult, + true, + startTime, + duration, + definition, + diagnosticSourceEnabled); + + if (interceptor != null) + { + return interceptor.ScalarExecutedAsync(command, eventData, methodResult, cancellationToken); + } + } + + return Task.FromResult(methodResult); + } + + /// + /// Logs for the event. + /// + /// The diagnostics logger to use. + /// The database command object. + /// The correlation ID associated with the given . + /// The correlation ID associated with the being used. + /// The return value from the underlying method execution. + /// The time that execution began. + /// The duration of the command execution, not including consuming results. + /// The cancellation token. + /// The result of execution, which may have been modified by an interceptor. + public static Task CommandNonQueryExecutedAsync( + [NotNull] this IDiagnosticsLogger diagnostics, + [NotNull] DbCommand command, + Guid commandId, + Guid connectionId, + int methodResult, + DateTimeOffset startTime, + TimeSpan duration, + CancellationToken cancellationToken = default) + { + var definition = RelationalResources.LogExecutedCommand(diagnostics); + + LogCommandExecuted(diagnostics, command, duration, definition); + + var diagnosticSourceEnabled = diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name); + var interceptor = (diagnostics.Interceptors as IRelationalInterceptors)?.CommandInterceptor; + + if (interceptor != null + || diagnosticSourceEnabled) + { + var eventData = BroadcastCommandExecuted( + diagnostics, + command, + DbCommandMethod.ExecuteNonQuery, + commandId, + connectionId, + methodResult, + true, + startTime, + duration, + definition, + diagnosticSourceEnabled); + + if (interceptor != null) + { + return interceptor.NonQueryExecutedAsync(command, eventData, methodResult, cancellationToken); + } + } + + return Task.FromResult(methodResult); + } + + private static CommandExecutedEventData BroadcastCommandExecuted( + IDiagnosticsLogger diagnostics, + DbCommand command, + DbCommandMethod executeMethod, + Guid commandId, + Guid connectionId, + object methodResult, + bool async, + DateTimeOffset startTime, + TimeSpan duration, + EventDefinition definition, + bool diagnosticSourceEnabled) + { + var eventData = new CommandExecutedEventData( + definition, + CommandExecuted, + command, + executeMethod, + commandId, + connectionId, + methodResult, + async, + ShouldLogParameterValues(diagnostics, command), + startTime, + duration); + + if (diagnosticSourceEnabled) + { + diagnostics.DiagnosticSource.Write( + definition.EventId.Name, + eventData); + } + + return eventData; + } + + private static void LogCommandExecuted( + IDiagnosticsLogger diagnostics, + DbCommand command, + TimeSpan duration, + EventDefinition definition) + { var warningBehavior = definition.GetLogBehavior(diagnostics); if (warningBehavior != WarningBehavior.Ignore) { @@ -144,24 +761,6 @@ public static void CommandExecuted( Environment.NewLine, command.CommandText.TrimEnd()); } - - if (diagnostics.DiagnosticSource.IsEnabled(definition.EventId.Name)) - { - diagnostics.DiagnosticSource.Write( - definition.EventId.Name, - new CommandExecutedEventData( - definition, - CommandExecuted, - command, - executeMethod, - commandId, - connectionId, - methodResult, - async, - ShouldLogParameterValues(diagnostics, command), - startTime, - duration)); - } } private static string CommandExecuted(EventDefinitionBase definition, EventData payload) diff --git a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs index 5d47e256781..558f02c3b21 100644 --- a/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/EntityFrameworkRelationalServicesBuilder.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.EntityFrameworkCore.Migrations.Internal; @@ -67,6 +68,7 @@ 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) }, @@ -77,6 +79,7 @@ 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(IRelationalTypeMappingSourcePlugin), new ServiceCharacteristics(ServiceLifetime.Singleton, multipleRegistrations: true) }, // New Query Pipeline @@ -150,6 +153,8 @@ public override EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd(); TryAdd(); + TryAdd(); + TryAdd(p => p.GetService()); // New Query pipeline TryAdd(); @@ -172,6 +177,7 @@ 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 5c61d862cf4..3ad01268d6c 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalDbContextOptionsBuilder.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalDbContextOptionsBuilder.cs @@ -4,6 +4,7 @@ using System; using System.ComponentModel; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; @@ -103,7 +104,22 @@ public virtual TBuilder UseRelationalNulls(bool useRelationalNulls = true) /// A function that returns a new instance of an execution strategy. public virtual TBuilder ExecutionStrategy( [NotNull] Func getExecutionStrategy) - => WithOption(e => (TExtension)e.WithExecutionStrategyFactory(Check.NotNull(getExecutionStrategy, nameof(getExecutionStrategy)))); + => 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 diff --git a/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs b/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs index bf4e626cd56..ed7ba805f00 100644 --- a/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs +++ b/src/EFCore.Relational/Infrastructure/RelationalOptionsExtension.cs @@ -40,6 +40,7 @@ public abstract class RelationalOptionsExtension : IDbContextOptionsExtension private string _migrationsHistoryTableSchema; private Func _executionStrategyFactory; private string _logFragment; + private IDbCommandInterceptor _commandInterceptor; /// /// Creates a new set of options with everything set to default values. @@ -66,6 +67,7 @@ protected RelationalOptionsExtension([NotNull] RelationalOptionsExtension copyFr _migrationsHistoryTableName = copyFrom._migrationsHistoryTableName; _migrationsHistoryTableSchema = copyFrom._migrationsHistoryTableSchema; _executionStrategyFactory = copyFrom._executionStrategyFactory; + _commandInterceptor = copyFrom._commandInterceptor; } /// @@ -304,6 +306,27 @@ 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/Storage/RelationalCommand.cs b/src/EFCore.Relational/Storage/RelationalCommand.cs index 28adda56d14..6f477571c65 100644 --- a/src/EFCore.Relational/Storage/RelationalCommand.cs +++ b/src/EFCore.Relational/Storage/RelationalCommand.cs @@ -82,7 +82,6 @@ public virtual int ExecuteNonQuery( parameterValues, logger); - /// /// Asynchronously executes the command with no results. /// @@ -208,14 +207,6 @@ protected virtual object Execute( var startTime = DateTimeOffset.UtcNow; var stopwatch = Stopwatch.StartNew(); - logger?.CommandExecuting( - dbCommand, - executeMethod, - commandId, - connection.ConnectionId, - async: false, - startTime: startTime); - object result; var readerOpen = false; try @@ -223,45 +214,71 @@ protected virtual object Execute( switch (executeMethod) { case DbCommandMethod.ExecuteNonQuery: - { - result = dbCommand.ExecuteNonQuery(); + var nonQueryResult = (logger?.CommandNonQueryExecuting( + dbCommand, + commandId, + connection.ConnectionId, + startTime: startTime) + ?? new InterceptionResult(dbCommand.ExecuteNonQuery())).Result; + + result = logger?.CommandNonQueryExecuted( + dbCommand, + commandId, + connection.ConnectionId, + nonQueryResult, + startTime, + stopwatch.Elapsed) + ?? nonQueryResult; break; - } case DbCommandMethod.ExecuteScalar: - { - result = dbCommand.ExecuteScalar(); - + var scalarResult = (logger?.CommandScalarExecuting( + dbCommand, + commandId, + connection.ConnectionId, + startTime: startTime) + ?? new InterceptionResult(dbCommand.ExecuteScalar())).Result; + + result = logger?.CommandScalarExecuted( + dbCommand, + commandId, + connection.ConnectionId, + scalarResult, + startTime, + stopwatch.Elapsed) + ?? scalarResult; break; - } case DbCommandMethod.ExecuteReader: - { - result - = new RelationalDataReader( - connection, + var reader = (logger?.CommandReaderExecuting( + dbCommand, + commandId, + connection.ConnectionId, + startTime: startTime) + ?? new InterceptionResult(dbCommand.ExecuteReader())).Result; + + if (logger != null) + { + reader = logger?.CommandReaderExecuted( dbCommand, - dbCommand.ExecuteReader(), commandId, - logger); - readerOpen = true; + connection.ConnectionId, + reader, + startTime, + stopwatch.Elapsed); + } + + result = new RelationalDataReader( + connection, + dbCommand, + reader, + commandId, + logger); + readerOpen = true; break; - } default: - { throw new NotSupportedException(); - } } - - logger?.CommandExecuted( - dbCommand, - executeMethod, - commandId, - connection.ConnectionId, - result, - false, - startTime, - stopwatch.Elapsed); } catch (Exception exception) { @@ -317,14 +334,6 @@ protected virtual async Task ExecuteAsync( var startTime = DateTimeOffset.UtcNow; var stopwatch = Stopwatch.StartNew(); - logger?.CommandExecuting( - dbCommand, - executeMethod, - commandId, - connection.ConnectionId, - async: true, - startTime: startTime); - object result; var readerOpen = false; try @@ -332,44 +341,100 @@ protected virtual async Task ExecuteAsync( switch (executeMethod) { case DbCommandMethod.ExecuteNonQuery: - { - result = await dbCommand.ExecuteNonQueryAsync(cancellationToken); + var nonQueryResult = logger == null + ? null + : await logger.CommandNonQueryExecutingAsync( + dbCommand, + commandId, + connection.ConnectionId, + startTime: startTime, + cancellationToken); + + var nonQueryValue = nonQueryResult.HasValue + ? nonQueryResult.Value.Result + : await dbCommand.ExecuteNonQueryAsync(cancellationToken); + if (logger != null) + { + nonQueryValue = await logger.CommandNonQueryExecutedAsync( + dbCommand, + commandId, + connection.ConnectionId, + nonQueryValue, + startTime, + stopwatch.Elapsed, + cancellationToken); + } + + result = nonQueryValue; break; - } case DbCommandMethod.ExecuteScalar: - { - result = await dbCommand.ExecuteScalarAsync(cancellationToken); + var scalarResult = logger == null + ? null + : await logger.CommandScalarExecutingAsync( + dbCommand, + commandId, + connection.ConnectionId, + startTime: startTime, + cancellationToken); + var scalarValue = scalarResult.HasValue + ? scalarResult.Value.Result + : await dbCommand.ExecuteScalarAsync(cancellationToken); + + if (logger != null) + { + scalarValue = await logger.CommandScalarExecutedAsync( + dbCommand, + commandId, + connection.ConnectionId, + scalarValue, + startTime, + stopwatch.Elapsed, + cancellationToken); + } + + result = scalarValue; break; - } case DbCommandMethod.ExecuteReader: - { + var readerResult = logger == null + ? null + : await logger.CommandReaderExecutingAsync( + dbCommand, + commandId, + connection.ConnectionId, + startTime: startTime, + cancellationToken); + + var reader = readerResult.HasValue + ? readerResult.Value.Result + : await dbCommand.ExecuteReaderAsync(cancellationToken); + + if (logger != null) + { + reader = await logger.CommandReaderExecutedAsync( + dbCommand, + commandId, + connection.ConnectionId, + reader, + startTime, + stopwatch.Elapsed, + cancellationToken); + } + + readerOpen = true; + result = new RelationalDataReader( connection, dbCommand, - await dbCommand.ExecuteReaderAsync(cancellationToken), + reader, commandId, logger); - readerOpen = true; break; - } default: - { throw new NotSupportedException(); - } } - - logger?.CommandExecuted( - dbCommand, - executeMethod, - commandId, - connection.ConnectionId, - result, - true, - startTime, - stopwatch.Elapsed); } catch (Exception exception) { diff --git a/src/EFCore/Diagnostics/IDiagnosticsLogger`.cs b/src/EFCore/Diagnostics/IDiagnosticsLogger`.cs index 2ea173fc326..ac7fccec166 100644 --- a/src/EFCore/Diagnostics/IDiagnosticsLogger`.cs +++ b/src/EFCore/Diagnostics/IDiagnosticsLogger`.cs @@ -19,13 +19,18 @@ namespace Microsoft.EntityFrameworkCore.Diagnostics /// sensitive data or not can be made. /// /// - /// The service lifetime is . This means a single instance - /// is used by many instances. The implementation must be thread-safe. - /// This service cannot depend on services registered as . + /// 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 IDiagnosticsLogger : IDiagnosticsLogger where TLoggerCategory : LoggerCategory, new() { + /// + /// Holds registered interceptors, if any. + /// + IInterceptors Interceptors { get; } } } diff --git a/src/EFCore/Diagnostics/IInterceptors.cs b/src/EFCore/Diagnostics/IInterceptors.cs new file mode 100644 index 00000000000..537a6cd3f8e --- /dev/null +++ b/src/EFCore/Diagnostics/IInterceptors.cs @@ -0,0 +1,30 @@ +// 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 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'. + /// + /// + /// 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 IInterceptors + { + } +} diff --git a/src/EFCore/Diagnostics/InterceptionResult.cs b/src/EFCore/Diagnostics/InterceptionResult.cs new file mode 100644 index 00000000000..cb9e23b709f --- /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 interceptor 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/Interceptors.cs b/src/EFCore/Diagnostics/Interceptors.cs new file mode 100644 index 00000000000..ab727a8eb11 --- /dev/null +++ b/src/EFCore/Diagnostics/Interceptors.cs @@ -0,0 +1,44 @@ +// 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 new file mode 100644 index 00000000000..d5b3c127038 --- /dev/null +++ b/src/EFCore/Diagnostics/InterceptorsDependencies.cs @@ -0,0 +1,73 @@ +// 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/Infrastructure/EntityFrameworkServicesBuilder.cs b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs index e87e66be8da..e2cbcf94b12 100644 --- a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs +++ b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs @@ -96,6 +96,7 @@ public static readonly IDictionary CoreServices { typeof(IProviderConventionSetBuilder), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IConventionSetBuilder), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IDiagnosticsLogger<>), new ServiceCharacteristics(ServiceLifetime.Scoped) }, + { typeof(IInterceptors), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(ILoggerFactory), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IEntityGraphAttacher), new ServiceCharacteristics(ServiceLifetime.Scoped) }, { typeof(IKeyPropagator), new ServiceCharacteristics(ServiceLifetime.Scoped) }, @@ -236,6 +237,7 @@ public virtual EntityFrameworkServicesBuilder TryAddCoreServices() TryAdd(); TryAdd(); TryAdd(typeof(IDiagnosticsLogger<>), typeof(DiagnosticsLogger<>)); + TryAdd(); TryAdd(); TryAdd(); TryAdd(p => p.GetService()); @@ -283,7 +285,8 @@ 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/Internal/DiagnosticsLogger.cs b/src/EFCore/Internal/DiagnosticsLogger.cs index 3a0d72193bb..9f329670b96 100644 --- a/src/EFCore/Internal/DiagnosticsLogger.cs +++ b/src/EFCore/Internal/DiagnosticsLogger.cs @@ -17,9 +17,10 @@ namespace Microsoft.EntityFrameworkCore.Internal /// doing so can result in application failures when updating to a new Entity Framework Core release. /// /// - /// The service lifetime is . This means a single instance - /// is used by many instances. The implementation must be thread-safe. - /// This service cannot depend on services registered as . + /// 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 DiagnosticsLogger : IDiagnosticsLogger @@ -35,12 +36,14 @@ public DiagnosticsLogger( [NotNull] ILoggerFactory loggerFactory, [NotNull] ILoggingOptions loggingOptions, [NotNull] DiagnosticSource diagnosticSource, - [NotNull] LoggingDefinitions loggingDefinitions) + [NotNull] LoggingDefinitions loggingDefinitions, + [CanBeNull] IInterceptors interceptors = null) { DiagnosticSource = diagnosticSource; Definitions = loggingDefinitions; Logger = loggerFactory.CreateLogger(new TLoggerCategory()); Options = loggingOptions; + Interceptors = interceptors; } /// @@ -59,6 +62,14 @@ public DiagnosticsLogger( /// public virtual ILogger Logger { get; } + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual IInterceptors Interceptors { get; } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in diff --git a/test/EFCore.Relational.Specification.Tests/InterceptionTestBase.cs b/test/EFCore.Relational.Specification.Tests/InterceptionTestBase.cs new file mode 100644 index 00000000000..bca0f7ab266 --- /dev/null +++ b/test/EFCore.Relational.Specification.Tests/InterceptionTestBase.cs @@ -0,0 +1,1726 @@ +// 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 System.ComponentModel.DataAnnotations.Schema; +using System.Data.Common; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +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() + { + protected InterceptionTestBase(InterceptionFixtureBase fixture) + { + Fixture = fixture; + } + + protected InterceptionFixtureBase Fixture { get; } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task Intercept_query_passively(bool async, bool inject) + { + var (context, interceptor) = CreateContext(inject); + using (context) + { + List results; + if (async) + { + results = await context.Set().ToListAsync(); + Assert.True(interceptor.AsyncCalled); + } + else + { + results = context.Set().ToList(); + Assert.True(interceptor.SyncCalled); + } + + Assert.Equal(2, results.Count); + Assert.Equal(77, results[0].Id); + Assert.Equal(88, results[1].Id); + Assert.Equal("Black Hole", results[0].Type); + Assert.Equal("Bing Bang", results[1].Type); + + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.ExecutingCalled); + Assert.True(interceptor.ExecutedCalled); + } + + return interceptor.CommandText; + } + + protected class PassiveReaderCommandInterceptor : CommandInterceptorBase + { + public PassiveReaderCommandInterceptor() + : base(DbCommandMethod.ExecuteReader) + { + } + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task Intercept_scalar_passively(bool async, bool inject) + { + var (context, interceptor) = CreateContext(inject); + using (context) + { + var sql = "SELECT 1"; + + var command = context.GetService().Create().Append(sql).Build(); + var connection = context.GetService(); + var logger = context.GetService>(); + + if (async) + { + Assert.Equal(1, Convert.ToInt32(await command.ExecuteScalarAsync(connection, null, logger))); + Assert.True(interceptor.AsyncCalled); + } + else + { + Assert.Equal(1, Convert.ToInt32(command.ExecuteScalar(connection, null, logger))); + Assert.True(interceptor.SyncCalled); + } + + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.ExecutingCalled); + Assert.True(interceptor.ExecutedCalled); + + AssertSql(sql, interceptor.CommandText); + } + } + + protected class PassiveScalarCommandInterceptor : CommandInterceptorBase + { + public PassiveScalarCommandInterceptor() + : base(DbCommandMethod.ExecuteScalar) + { + } + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task Intercept_non_query_passively(bool async, bool inject) + { + var (context, interceptor) = CreateContext(inject); + using (context) + { + using (context.Database.BeginTransaction()) + { + var nonQuery = "DELETE FROM Singularity WHERE Id = 77"; + + if (async) + { + Assert.Equal(1, await context.Database.ExecuteSqlRawAsync(nonQuery)); + Assert.True(interceptor.AsyncCalled); + } + else + { + Assert.Equal(1, context.Database.ExecuteSqlRaw(nonQuery)); + Assert.True(interceptor.SyncCalled); + } + + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.ExecutingCalled); + Assert.True(interceptor.ExecutedCalled); + + AssertSql(nonQuery, interceptor.CommandText); + } + } + } + + protected class PassiveNonQueryCommandInterceptor : CommandInterceptorBase + { + public PassiveNonQueryCommandInterceptor() + : base(DbCommandMethod.ExecuteNonQuery) + { + } + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task Intercept_query_to_suppress_execution(bool async, bool inject) + { + var (context, interceptor) = CreateContext(inject); + using (context) + { + List results; + + if (async) + { + results = await context.Set().ToListAsync(); + Assert.True(interceptor.AsyncCalled); + } + else + { + results = context.Set().ToList(); + Assert.True(interceptor.SyncCalled); + } + + Assert.Equal(3, results.Count); + Assert.Equal(977, results[0].Id); + Assert.Equal(988, results[1].Id); + Assert.Equal(999, results[2].Id); + Assert.Equal("<977>", results[0].Type); + Assert.Equal("<988>", results[1].Type); + Assert.Equal("<999>", results[2].Type); + + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.ExecutingCalled); + Assert.True(interceptor.ExecutedCalled); + } + + return interceptor.CommandText; + } + + protected class SuppressingReaderCommandInterceptor : CommandInterceptorBase + { + public SuppressingReaderCommandInterceptor() + : base(DbCommandMethod.ExecuteReader) + { + } + + public override InterceptionResult? ReaderExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + { + base.ReaderExecuting(command, eventData, result); + + return new InterceptionResult(new FakeDbDataReader()); + } + + public override Task?> ReaderExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + base.ReaderExecutingAsync(command, eventData, result, cancellationToken); + + return Task.FromResult?>(new InterceptionResult(new FakeDbDataReader())); + } + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task Intercept_scalar_to_suppress_execution(bool async, bool inject) + { + var (context, interceptor) = CreateContext(inject); + using (context) + { + var sql = "SELECT 1"; + + var command = context.GetService().Create().Append(sql).Build(); + var connection = context.GetService(); + var logger = context.GetService>(); + + if (async) + { + Assert.Equal( + SuppressingScalarCommandInterceptor.InterceptedResult, + await command.ExecuteScalarAsync(connection, null, logger)); + + Assert.True(interceptor.AsyncCalled); + } + else + { + Assert.Equal( + SuppressingScalarCommandInterceptor.InterceptedResult, + command.ExecuteScalar(connection, null, logger)); + + Assert.True(interceptor.SyncCalled); + } + + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.ExecutingCalled); + Assert.True(interceptor.ExecutedCalled); + + AssertSql(sql, interceptor.CommandText); + } + } + + protected class SuppressingScalarCommandInterceptor : CommandInterceptorBase + { + public SuppressingScalarCommandInterceptor() + : base(DbCommandMethod.ExecuteScalar) + { + } + + public const string InterceptedResult = "Bet you weren't expecting a string!"; + + public override InterceptionResult? ScalarExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + { + base.ScalarExecuting(command, eventData, result); + + return new InterceptionResult(InterceptedResult); + } + + public override Task?> ScalarExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + base.ScalarExecutingAsync(command, eventData, result, cancellationToken); + + return Task.FromResult?>( + new InterceptionResult(InterceptedResult)); + } + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task Intercept_non_query_to_suppress_execution(bool async, bool inject) + { + var (context, interceptor) = CreateContext(inject); + using (context) + { + using (context.Database.BeginTransaction()) + { + var nonQuery = "DELETE FROM Singularity WHERE Id = 77"; + + if (async) + { + Assert.Equal(2, await context.Database.ExecuteSqlRawAsync(nonQuery)); + Assert.True(interceptor.AsyncCalled); + } + else + { + Assert.Equal(2, context.Database.ExecuteSqlRaw(nonQuery)); + Assert.True(interceptor.SyncCalled); + } + + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.ExecutingCalled); + Assert.True(interceptor.ExecutedCalled); + + AssertSql(nonQuery, interceptor.CommandText); + } + } + } + + protected class SuppressingNonQueryCommandInterceptor : CommandInterceptorBase + { + public SuppressingNonQueryCommandInterceptor() + : base(DbCommandMethod.ExecuteNonQuery) + { + } + + public override InterceptionResult? NonQueryExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + { + base.NonQueryExecuting(command, eventData, result); + + return new InterceptionResult(2); + } + + public override Task?> NonQueryExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + base.NonQueryExecutingAsync(command, eventData, result, cancellationToken); + + return Task.FromResult?>(new InterceptionResult(2)); + } + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task Intercept_query_to_mutate_command(bool async, bool inject) + { + var (context, interceptor) = CreateContext(inject); + using (context) + { + List results; + + if (async) + { + results = await context.Set().ToListAsync(); + Assert.True(interceptor.AsyncCalled); + } + else + { + results = context.Set().ToList(); + Assert.True(interceptor.SyncCalled); + } + + Assert.Equal(2, results.Count); + Assert.Equal(77, results[0].Id); + Assert.Equal(88, results[1].Id); + Assert.Equal("Black Hole?", results[0].Type); + Assert.Equal("Bing Bang?", results[1].Type); + + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.ExecutingCalled); + Assert.True(interceptor.ExecutedCalled); + } + + return interceptor.CommandText; + } + + protected class MutatingReaderCommandInterceptor : CommandInterceptorBase + { + public MutatingReaderCommandInterceptor() + : base(DbCommandMethod.ExecuteReader) + { + } + + public override InterceptionResult? ReaderExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + { + MutateQuery(command); + + return base.ReaderExecuting(command, eventData, result); + } + + public override Task?> ReaderExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + MutateQuery(command); + + return base.ReaderExecutingAsync(command, eventData, result, cancellationToken); + } + + private static void MutateQuery(DbCommand command) + => command.CommandText = command.CommandText.Replace("Singularity", "Brane"); + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task Intercept_scalar_to_mutate_command(bool async, bool inject) + { + var (context, interceptor) = CreateContext(inject); + using (context) + { + var sql = "SELECT 1"; + + var command = context.GetService().Create().Append(sql).Build(); + var connection = context.GetService(); + var logger = context.GetService>(); + + if (async) + { + Assert.Equal(2, Convert.ToInt32(await command.ExecuteScalarAsync(connection, null, logger))); + Assert.True(interceptor.AsyncCalled); + } + else + { + Assert.Equal(2, Convert.ToInt32(command.ExecuteScalar(connection, null, logger))); + Assert.True(interceptor.SyncCalled); + } + + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.ExecutingCalled); + Assert.True(interceptor.ExecutedCalled); + + AssertSql(MutatingScalarCommandInterceptor.MutatedSql, interceptor.CommandText); + } + } + + protected class MutatingScalarCommandInterceptor : CommandInterceptorBase + { + public MutatingScalarCommandInterceptor() + : base(DbCommandMethod.ExecuteScalar) + { + } + + public const string MutatedSql = "SELECT 2"; + + public override InterceptionResult? ScalarExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + { + command.CommandText = MutatedSql; + + return base.ScalarExecuting(command, eventData, result); + } + + public override Task?> ScalarExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + command.CommandText = MutatedSql; + + return base.ScalarExecutingAsync(command, eventData, result, cancellationToken); + } + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task Intercept_non_query_to_mutate_command(bool async, bool inject) + { + var (context, interceptor) = CreateContext(inject); + using (context) + { + using (context.Database.BeginTransaction()) + { + var nonQuery = "DELETE FROM Singularity WHERE Id = 77"; + + if (async) + { + Assert.Equal(0, await context.Database.ExecuteSqlRawAsync(nonQuery)); + Assert.True(interceptor.AsyncCalled); + } + else + { + Assert.Equal(0, context.Database.ExecuteSqlRaw(nonQuery)); + Assert.True(interceptor.SyncCalled); + } + + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.ExecutingCalled); + Assert.True(interceptor.ExecutedCalled); + + AssertSql(MutatingNonQueryCommandInterceptor.MutatedSql, interceptor.CommandText); + } + } + } + + protected class MutatingNonQueryCommandInterceptor : CommandInterceptorBase + { + public const string MutatedSql = "DELETE FROM Singularity WHERE Id = 78"; + + public MutatingNonQueryCommandInterceptor() + : base(DbCommandMethod.ExecuteNonQuery) + { + } + + public override InterceptionResult? NonQueryExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + { + command.CommandText = MutatedSql; + + return base.NonQueryExecuting(command, eventData, result); + } + + public override Task?> NonQueryExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + command.CommandText = MutatedSql; + + return base.NonQueryExecutingAsync(command, eventData, result, cancellationToken); + } + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task Intercept_query_to_replace_execution(bool async, bool inject) + { + var (context, interceptor) = CreateContext(inject); + using (context) + { + List results; + + if (async) + { + results = await context.Set().ToListAsync(); + Assert.True(interceptor.AsyncCalled); + } + else + { + results = context.Set().ToList(); + Assert.True(interceptor.SyncCalled); + } + + Assert.Equal(2, results.Count); + Assert.Equal(77, results[0].Id); + Assert.Equal(88, results[1].Id); + Assert.Equal("Black Hole?", results[0].Type); + Assert.Equal("Bing Bang?", results[1].Type); + + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.ExecutingCalled); + Assert.True(interceptor.ExecutedCalled); + } + + return interceptor.CommandText; + } + + protected class QueryReplacingReaderCommandInterceptor : CommandInterceptorBase + { + public QueryReplacingReaderCommandInterceptor() + : base(DbCommandMethod.ExecuteReader) + { + } + + public override InterceptionResult? ReaderExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + { + base.ReaderExecuting(command, eventData, result); + + // Note: this DbCommand will not get disposed...can be problematic on some providers + return new InterceptionResult(CreateNewCommand(command).ExecuteReader()); + } + + public override async Task?> ReaderExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + await base.ReaderExecutingAsync(command, eventData, result, cancellationToken); + + // Note: this DbCommand will not get disposed...can be problematic on some providers + return new InterceptionResult(await CreateNewCommand(command).ExecuteReaderAsync(cancellationToken)); + } + + private static DbCommand CreateNewCommand(DbCommand command) + { + var newCommand = command.Connection.CreateCommand(); + newCommand.CommandText = command.CommandText.Replace("Singularity", "Brane"); + + return newCommand; + } + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task Intercept_scalar_to_replace_execution(bool async, bool inject) + { + var (context, interceptor) = CreateContext(inject); + using (context) + { + var sql = "SELECT 1"; + + var command = context.GetService().Create().Append(sql).Build(); + var connection = context.GetService(); + var logger = context.GetService>(); + + if (async) + { + Assert.Equal(2, Convert.ToInt32(await command.ExecuteScalarAsync(connection, null, logger))); + Assert.True(interceptor.AsyncCalled); + } + else + { + Assert.Equal(2, Convert.ToInt32(command.ExecuteScalar(connection, null, logger))); + Assert.True(interceptor.SyncCalled); + } + + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.ExecutingCalled); + Assert.True(interceptor.ExecutedCalled); + + AssertSql(sql, interceptor.CommandText); + } + } + + protected class QueryReplacingScalarCommandInterceptor : CommandInterceptorBase + { + public QueryReplacingScalarCommandInterceptor() + : base(DbCommandMethod.ExecuteScalar) + { + } + + public override InterceptionResult? ScalarExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + { + base.ScalarExecuting(command, eventData, result); + + // Note: this DbCommand will not get disposed...can be problematic on some providers + return new InterceptionResult(CreateNewCommand(command).ExecuteScalar()); + } + + public override async Task?> ScalarExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + await base.ScalarExecutingAsync(command, eventData, result, cancellationToken); + + // Note: this DbCommand will not get disposed...can be problematic on some providers + return new InterceptionResult(await CreateNewCommand(command).ExecuteScalarAsync(cancellationToken)); + } + + private static DbCommand CreateNewCommand(DbCommand command) + { + var newCommand = command.Connection.CreateCommand(); + newCommand.CommandText = "SELECT 2"; + + return newCommand; + } + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task Intercept_non_query_to_replace_execution(bool async, bool inject) + { + var (context, interceptor) = CreateContext(inject); + using (context) + { + using (context.Database.BeginTransaction()) + { + var nonQuery = "DELETE FROM Singularity WHERE Id = 78"; + + if (async) + { + Assert.Equal(1, await context.Database.ExecuteSqlRawAsync(nonQuery)); + Assert.True(interceptor.AsyncCalled); + } + else + { + Assert.Equal(1, context.Database.ExecuteSqlRaw(nonQuery)); + Assert.True(interceptor.SyncCalled); + } + + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.ExecutingCalled); + Assert.True(interceptor.ExecutedCalled); + + AssertSql(nonQuery, interceptor.CommandText); + } + } + } + + protected class QueryReplacingNonQueryCommandInterceptor : CommandInterceptorBase + { + public QueryReplacingNonQueryCommandInterceptor() + : base(DbCommandMethod.ExecuteNonQuery) + { + } + + public override InterceptionResult? NonQueryExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + { + base.NonQueryExecuting(command, eventData, result); + + // Note: this DbCommand will not get disposed...can be problematic on some providers + return new InterceptionResult(CreateNewCommand(command).ExecuteNonQuery()); + } + + public override async Task?> NonQueryExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + await base.NonQueryExecutingAsync(command, eventData, result, cancellationToken); + + // Note: this DbCommand will not get disposed...can be problematic on some providers + return new InterceptionResult(await CreateNewCommand(command).ExecuteNonQueryAsync(cancellationToken)); + } + + private DbCommand CreateNewCommand(DbCommand command) + { + var newCommand = command.Connection.CreateCommand(); + newCommand.Transaction = command.Transaction; + newCommand.CommandText = "DELETE FROM Singularity WHERE Id = 77"; + + return newCommand; + } + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task Intercept_query_to_replace_result(bool async, bool inject) + { + var (context, interceptor) = CreateContext(inject); + using (context) + { + List results; + + if (async) + { + results = await context.Set().ToListAsync(); + Assert.True(interceptor.AsyncCalled); + } + else + { + results = context.Set().ToList(); + Assert.True(interceptor.SyncCalled); + } + + Assert.Equal(5, results.Count); + Assert.Equal(77, results[0].Id); + Assert.Equal(88, results[1].Id); + Assert.Equal(977, results[2].Id); + Assert.Equal(988, results[3].Id); + Assert.Equal(999, results[4].Id); + Assert.Equal("Black Hole", results[0].Type); + Assert.Equal("Bing Bang", results[1].Type); + Assert.Equal("<977>", results[2].Type); + Assert.Equal("<988>", results[3].Type); + Assert.Equal("<999>", results[4].Type); + + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.ExecutingCalled); + Assert.True(interceptor.ExecutedCalled); + } + + return interceptor.CommandText; + } + + protected class ResultReplacingReaderCommandInterceptor : CommandInterceptorBase + { + public ResultReplacingReaderCommandInterceptor() + : base(DbCommandMethod.ExecuteReader) + { + } + + public override DbDataReader ReaderExecuted( + DbCommand command, + CommandExecutedEventData eventData, + DbDataReader result) + { + base.ReaderExecuted(command, eventData, result); + + return new CompositeFakeDbDataReader(result, new FakeDbDataReader()); + } + + public override Task ReaderExecutedAsync( + DbCommand command, + CommandExecutedEventData eventData, + DbDataReader result, + CancellationToken cancellationToken = default) + { + base.ReaderExecutedAsync(command, eventData, result, cancellationToken); + + return Task.FromResult(new CompositeFakeDbDataReader(result, new FakeDbDataReader())); + } + } + + private class CompositeFakeDbDataReader : FakeDbDataReader + { + private readonly DbDataReader _firstReader; + private readonly DbDataReader _secondReader; + private bool _movedToSecond; + + public CompositeFakeDbDataReader(DbDataReader firstReader, DbDataReader secondReader) + { + _firstReader = firstReader; + _secondReader = secondReader; + } + + public override void Close() + { + _firstReader.Close(); + _secondReader.Close(); + } + + protected override void Dispose(bool disposing) + { + _firstReader.Dispose(); + _secondReader.Dispose(); + + base.Dispose(disposing); + } + + public override bool Read() + { + if (_movedToSecond) + { + return _secondReader.Read(); + } + + if (!_firstReader.Read()) + { + _movedToSecond = true; + return _secondReader.Read(); + } + + return true; + } + + public override int GetInt32(int ordinal) + => _movedToSecond + ? _secondReader.GetInt32(ordinal) + : _firstReader.GetInt32(ordinal); + + public override string GetString(int ordinal) + => _movedToSecond + ? _secondReader.GetString(ordinal) + : _firstReader.GetString(ordinal); + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task Intercept_scalar_to_replace_result(bool async, bool inject) + { + var (context, interceptor) = CreateContext(inject); + using (context) + { + var sql = "SELECT 1"; + + var command = context.GetService().Create().Append(sql).Build(); + var connection = context.GetService(); + var logger = context.GetService>(); + + if (async) + { + Assert.Equal( + ResultReplacingScalarCommandInterceptor.InterceptedResult, + await command.ExecuteScalarAsync(connection, null, logger)); + + Assert.True(interceptor.AsyncCalled); + } + else + { + Assert.Equal( + ResultReplacingScalarCommandInterceptor.InterceptedResult, + command.ExecuteScalar(connection, null, logger)); + + Assert.True(interceptor.SyncCalled); + } + + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.ExecutingCalled); + Assert.True(interceptor.ExecutedCalled); + + AssertSql(sql, interceptor.CommandText); + } + } + + protected class ResultReplacingScalarCommandInterceptor : CommandInterceptorBase + { + public const string InterceptedResult = "Bet you weren't expecting a string!"; + + public ResultReplacingScalarCommandInterceptor() + : base(DbCommandMethod.ExecuteScalar) + { + } + + public override object ScalarExecuted( + DbCommand command, + CommandExecutedEventData eventData, + object result) + { + base.ScalarExecuted(command, eventData, result); + + return InterceptedResult; + } + + public override Task ScalarExecutedAsync( + DbCommand command, + CommandExecutedEventData eventData, + object result, + CancellationToken cancellationToken = default) + { + base.ScalarExecutedAsync(command, eventData, result, cancellationToken); + + return Task.FromResult(InterceptedResult); + } + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task Intercept_non_query_to_replaceresult(bool async, bool inject) + { + var (context, interceptor) = CreateContext(inject); + using (context) + { + using (context.Database.BeginTransaction()) + { + var nonQuery = "DELETE FROM Singularity WHERE Id = 78"; + + if (async) + { + Assert.Equal(7, await context.Database.ExecuteSqlRawAsync(nonQuery)); + Assert.True(interceptor.AsyncCalled); + } + else + { + Assert.Equal(7, context.Database.ExecuteSqlRaw(nonQuery)); + Assert.True(interceptor.SyncCalled); + } + + Assert.NotEqual(interceptor.AsyncCalled, interceptor.SyncCalled); + Assert.True(interceptor.ExecutingCalled); + Assert.True(interceptor.ExecutedCalled); + + AssertSql(nonQuery, interceptor.CommandText); + } + } + } + + protected class ResultReplacingNonQueryCommandInterceptor : CommandInterceptorBase + { + public ResultReplacingNonQueryCommandInterceptor() + : base(DbCommandMethod.ExecuteNonQuery) + { + } + + public override int NonQueryExecuted( + DbCommand command, + CommandExecutedEventData eventData, + int result) + { + base.NonQueryExecuted(command, eventData, result); + + return 7; + } + + public override async Task NonQueryExecutedAsync( + DbCommand command, + CommandExecutedEventData eventData, + int result, + CancellationToken cancellationToken = default) + { + await base.NonQueryExecutedAsync(command, eventData, result, cancellationToken); + + return 7; + } + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task Intercept_query_to_throw(bool async, bool inject) + { + using (var context = CreateContext(new ThrowingReaderCommandInterceptor())) + { + var exception = async + ? await Assert.ThrowsAsync(() => context.Set().ToListAsync()) + : Assert.Throws(() => context.Set().ToList()); + + Assert.Equal("Bang!", exception.Message); + } + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task Intercept_scalar_to_throw(bool async, bool inject) + { + using (var context = CreateContext(new ThrowingReaderCommandInterceptor())) + { + var command = context.GetService().Create().Append("SELECT 1").Build(); + var connection = context.GetService(); + var logger = context.GetService>(); + + var exception = async + ? await Assert.ThrowsAsync(() => command.ExecuteScalarAsync(connection, null, logger)) + : Assert.Throws(() => command.ExecuteScalar(connection, null, logger)); + + Assert.Equal("Bang!", exception.Message); + } + } + + [ConditionalTheory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(true, true)] + public virtual async Task Intercept_non_query_to_throw(bool async, bool inject) + { + using (var context = CreateContext(new ThrowingReaderCommandInterceptor())) + { + using (context.Database.BeginTransaction()) + { + var nonQuery = "DELETE FROM Singularity WHERE Id = 77"; + + var exception = async + ? await Assert.ThrowsAsync(() => context.Database.ExecuteSqlRawAsync(nonQuery)) + : Assert.Throws(() => context.Database.ExecuteSqlRaw(nonQuery)); + + Assert.Equal("Bang!", exception.Message); + } + } + } + + protected class ThrowingReaderCommandInterceptor : DbCommandInterceptor + { + public override InterceptionResult? ReaderExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + { + throw new Exception("Bang!"); + } + + public override Task?> ReaderExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + throw new Exception("Bang!"); + } + + public override InterceptionResult? ScalarExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + { + throw new Exception("Bang!"); + } + + public override Task?> ScalarExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + throw new Exception("Bang!"); + } + + public override InterceptionResult? NonQueryExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + { + throw new Exception("Bang!"); + } + + public override Task?> NonQueryExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + throw new Exception("Bang!"); + } + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + 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))) + { + await TestCompoisteQueryInterceptors( + context, + appInterceptor, + (MutatingReaderCommandInterceptor)context.GetService>().Single(), + async); + } + } + + private static async Task TestCompoisteQueryInterceptors( + UniverseContext context, + ResultReplacingReaderCommandInterceptor interceptor1, + MutatingReaderCommandInterceptor interceptor2, + bool async) + { + List results; + + if (async) + { + results = await context.Set().ToListAsync(); + Assert.True(interceptor1.AsyncCalled); + Assert.True(interceptor2.AsyncCalled); + } + else + { + results = context.Set().ToList(); + Assert.True(interceptor1.SyncCalled); + Assert.True(interceptor2.SyncCalled); + } + + AssertCompositeResults(results); + + Assert.NotEqual(interceptor1.AsyncCalled, interceptor1.SyncCalled); + Assert.NotEqual(interceptor2.AsyncCalled, interceptor2.SyncCalled); + Assert.True(interceptor1.ExecutingCalled); + Assert.True(interceptor2.ExecutingCalled); + Assert.True(interceptor1.ExecutedCalled); + Assert.True(interceptor2.ExecutedCalled); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_scalar_with_one_app_and_one_injected_interceptor(bool async) + { + using (var context = CreateContext( + new ResultReplacingScalarCommandInterceptor(), + typeof(MutatingScalarCommandInterceptor))) + { + await TestCompositeScalarInterceptors(context, async); + } + } + + private static async Task TestCompositeScalarInterceptors(UniverseContext context, bool async) + { + var command = context.GetService().Create().Append("SELECT 1").Build(); + var connection = context.GetService(); + var logger = context.GetService>(); + + Assert.Equal( + ResultReplacingScalarCommandInterceptor.InterceptedResult, + async + ? await command.ExecuteScalarAsync(connection, null, logger) + : command.ExecuteScalar(connection, null, logger)); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_non_query_one_app_and_one_injected_interceptor(bool async) + { + using (var context = CreateContext( + new ResultReplacingNonQueryCommandInterceptor(), + typeof(MutatingNonQueryCommandInterceptor))) + { + await TestCompositeNonQueryInterceptors(context, async); + } + } + + private static async Task TestCompositeNonQueryInterceptors(UniverseContext context, bool async) + { + using (context.Database.BeginTransaction()) + { + var nonQuery = "DELETE FROM Singularity WHERE Id = 78"; + + Assert.Equal( + 7, + async + ? await context.Database.ExecuteSqlRawAsync(nonQuery) + : context.Database.ExecuteSqlRaw(nonQuery)); + } + } + + [ConditionalTheory] + [InlineData(false)] + [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(); + + await TestCompoisteQueryInterceptors( + context, + injectedInterceptors.OfType().Single(), + injectedInterceptors.OfType().Single(), + async); + } + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_scalar_with_two_injected_interceptors(bool async) + { + using (var context = CreateContext( + null, + typeof(MutatingScalarCommandInterceptor), + typeof(ResultReplacingScalarCommandInterceptor))) + { + await TestCompositeScalarInterceptors(context, async); + } + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_non_query_with_two_injected_interceptors(bool async) + { + using (var context = CreateContext( + null, + typeof(MutatingNonQueryCommandInterceptor), + typeof(ResultReplacingNonQueryCommandInterceptor))) + { + await TestCompositeNonQueryInterceptors(context, async); + } + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_query_with_explicitly_composed_app_interceptor(bool async) + { + using (var context = CreateContext( + new CompositeDbCommandInterceptor( + new MutatingReaderCommandInterceptor(), new ResultReplacingReaderCommandInterceptor()))) + { + var results = async + ? await context.Set().ToListAsync() + : context.Set().ToList(); + + AssertCompositeResults(results); + } + } + + private static void AssertCompositeResults(List results) + { + Assert.Equal(5, results.Count); + Assert.Equal(77, results[0].Id); + Assert.Equal(88, results[1].Id); + Assert.Equal(977, results[2].Id); + Assert.Equal(988, results[3].Id); + Assert.Equal(999, results[4].Id); + Assert.Equal("Black Hole?", results[0].Type); + Assert.Equal("Bing Bang?", results[1].Type); + Assert.Equal("<977>", results[2].Type); + Assert.Equal("<988>", results[3].Type); + Assert.Equal("<999>", results[4].Type); + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_scalar_with_explicitly_composed_app_interceptor(bool async) + { + using (var context = CreateContext( + new CompositeDbCommandInterceptor( + new MutatingScalarCommandInterceptor(), new ResultReplacingScalarCommandInterceptor()))) + { + await TestCompositeScalarInterceptors(context, async); + } + } + + [ConditionalTheory] + [InlineData(false)] + [InlineData(true)] + public virtual async Task Intercept_non_query_with_explicitly_composed_app_interceptor(bool async) + { + using (var context = CreateContext( + new CompositeDbCommandInterceptor( + new MutatingNonQueryCommandInterceptor(), new ResultReplacingNonQueryCommandInterceptor()))) + { + await TestCompositeNonQueryInterceptors(context, async); + } + } + + private class FakeDbDataReader : DbDataReader + { + private int _index; + + private readonly int[] _ints = + { + 977, 988, 999 + }; + + private readonly string[] _strings = + { + "<977>", "<988>", "<999>" + }; + + public override bool Read() + => _index++ < _ints.Length; + + public override int GetInt32(int ordinal) + => _ints[_index - 1]; + + public override bool IsDBNull(int ordinal) + => false; + + public override string GetString(int ordinal) + => _strings[_index - 1]; + + public override bool GetBoolean(int ordinal) => throw new NotImplementedException(); + public override byte GetByte(int ordinal) => throw new NotImplementedException(); + + public override long GetBytes(int ordinal, long dataOffset, byte[] buffer, int bufferOffset, int length) + => throw new NotImplementedException(); + + public override char GetChar(int ordinal) => throw new NotImplementedException(); + + public override long GetChars(int ordinal, long dataOffset, char[] buffer, int bufferOffset, int length) + => throw new NotImplementedException(); + + public override string GetDataTypeName(int ordinal) => throw new NotImplementedException(); + public override DateTime GetDateTime(int ordinal) => throw new NotImplementedException(); + public override decimal GetDecimal(int ordinal) => throw new NotImplementedException(); + public override double GetDouble(int ordinal) => throw new NotImplementedException(); + public override Type GetFieldType(int ordinal) => throw new NotImplementedException(); + public override float GetFloat(int ordinal) => throw new NotImplementedException(); + public override Guid GetGuid(int ordinal) => throw new NotImplementedException(); + public override short GetInt16(int ordinal) => throw new NotImplementedException(); + public override long GetInt64(int ordinal) => throw new NotImplementedException(); + public override string GetName(int ordinal) => throw new NotImplementedException(); + public override int GetOrdinal(string name) => throw new NotImplementedException(); + public override object GetValue(int ordinal) => throw new NotImplementedException(); + public override int GetValues(object[] values) => throw new NotImplementedException(); + public override int FieldCount { get; } + public override object this[int ordinal] => throw new NotImplementedException(); + public override object this[string name] => throw new NotImplementedException(); + public override int RecordsAffected { get; } + public override bool HasRows { get; } + public override bool IsClosed { get; } + public override bool NextResult() => throw new NotImplementedException(); + public override int Depth { get; } + public override IEnumerator GetEnumerator() => throw new NotImplementedException(); + } + + protected abstract class CommandInterceptorBase : IDbCommandInterceptor + { + private readonly DbCommandMethod _commandMethod; + + protected CommandInterceptorBase(DbCommandMethod commandMethod) + { + _commandMethod = commandMethod; + } + + public string CommandText { get; set; } + public Guid CommandId { get; set; } + public Guid ConnectionId { get; set; } + public bool AsyncCalled { get; set; } + public bool SyncCalled { get; set; } + public bool ExecutingCalled { get; set; } + public bool ExecutedCalled { get; set; } + + public virtual InterceptionResult? ReaderExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertExecuting(command, eventData); + + return result; + } + + public virtual InterceptionResult? ScalarExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertExecuting(command, eventData); + + return result; + } + + public virtual InterceptionResult? NonQueryExecuting( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertExecuting(command, eventData); + + return result; + } + + public virtual Task?> ReaderExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertExecuting(command, eventData); + + return Task.FromResult(result); + } + + public virtual Task?> ScalarExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertExecuting(command, eventData); + + return Task.FromResult(result); + } + + public virtual Task?> NonQueryExecutingAsync( + DbCommand command, + CommandEventData eventData, + InterceptionResult? result, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertExecuting(command, eventData); + + return Task.FromResult(result); + } + + public virtual DbDataReader ReaderExecuted( + DbCommand command, + CommandExecutedEventData eventData, + DbDataReader result) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertExecuted(command, eventData); + + return result; + } + + public virtual object ScalarExecuted( + DbCommand command, + CommandExecutedEventData eventData, + object result) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertExecuted(command, eventData); + + return result; + } + + public virtual int NonQueryExecuted( + DbCommand command, + CommandExecutedEventData eventData, + int result) + { + Assert.False(eventData.IsAsync); + SyncCalled = true; + AssertExecuted(command, eventData); + + return result; + } + + public virtual Task ReaderExecutedAsync( + DbCommand command, + CommandExecutedEventData eventData, + DbDataReader result, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertExecuted(command, eventData); + + return Task.FromResult(result); + } + + public virtual Task ScalarExecutedAsync( + DbCommand command, + CommandExecutedEventData eventData, + object result, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertExecuted(command, eventData); + + return Task.FromResult(result); + } + + public virtual Task NonQueryExecutedAsync( + DbCommand command, + CommandExecutedEventData eventData, + int result, + CancellationToken cancellationToken = default) + { + Assert.True(eventData.IsAsync); + AsyncCalled = true; + AssertExecuted(command, eventData); + + return Task.FromResult(result); + } + + protected virtual void AssertExecuting(DbCommand command, CommandEventData eventData) + { + Assert.NotEqual(default, eventData.CommandId); + Assert.NotEqual(default, eventData.ConnectionId); + Assert.Equal(_commandMethod, eventData.ExecuteMethod); + + CommandText = command.CommandText; + CommandId = eventData.CommandId; + ConnectionId = eventData.ConnectionId; + ExecutingCalled = true; + } + + protected virtual void AssertExecuted(DbCommand command, CommandExecutedEventData eventData) + { + Assert.Equal(CommandText, command.CommandText); + Assert.Equal(CommandId, eventData.CommandId); + Assert.Equal(ConnectionId, eventData.ConnectionId); + Assert.Equal(_commandMethod, eventData.ExecuteMethod); + + ExecutedCalled = 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.Tests/Infrastructure/RelationalInterceptorsDependenciesDependenciesTest.cs b/test/EFCore.Relational.Tests/Infrastructure/RelationalInterceptorsDependenciesDependenciesTest.cs new file mode 100644 index 00000000000..70d91d3ee58 --- /dev/null +++ b/test/EFCore.Relational.Tests/Infrastructure/RelationalInterceptorsDependenciesDependenciesTest.cs @@ -0,0 +1,18 @@ +// 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/RelationalEventIdTest.cs b/test/EFCore.Relational.Tests/RelationalEventIdTest.cs index 44f2d9ab0d7..c42d84028c8 100644 --- a/test/EFCore.Relational.Tests/RelationalEventIdTest.cs +++ b/test/EFCore.Relational.Tests/RelationalEventIdTest.cs @@ -80,7 +80,32 @@ public void Every_eventId_has_a_logger_method_and_logs_when_level_enabled() typeof(RelationalEventId), typeof(RelationalLoggerExtensions), typeof(TestRelationalLoggingDefinitions), - fakeFactories); + fakeFactories, + new Dictionary> + { + { + nameof(RelationalEventId.CommandExecuting), new List + { + nameof(RelationalLoggerExtensions.CommandReaderExecuting), + nameof(RelationalLoggerExtensions.CommandScalarExecuting), + nameof(RelationalLoggerExtensions.CommandNonQueryExecuting), + nameof(RelationalLoggerExtensions.CommandReaderExecutingAsync), + nameof(RelationalLoggerExtensions.CommandScalarExecutingAsync), + nameof(RelationalLoggerExtensions.CommandNonQueryExecutingAsync), + } + }, + { + nameof(RelationalEventId.CommandExecuted), new List + { + nameof(RelationalLoggerExtensions.CommandReaderExecutedAsync), + nameof(RelationalLoggerExtensions.CommandScalarExecutedAsync), + nameof(RelationalLoggerExtensions.CommandNonQueryExecutedAsync), + nameof(RelationalLoggerExtensions.CommandReaderExecuted), + nameof(RelationalLoggerExtensions.CommandScalarExecuted), + nameof(RelationalLoggerExtensions.CommandNonQueryExecuted), + } + } + }); } private class FakeMigration : Migration diff --git a/test/EFCore.Relational.Tests/TestUtilities/FakeDiagnosticsLogger.cs b/test/EFCore.Relational.Tests/TestUtilities/FakeDiagnosticsLogger.cs index 69b2a836e8a..2bad1258bb1 100644 --- a/test/EFCore.Relational.Tests/TestUtilities/FakeDiagnosticsLogger.cs +++ b/test/EFCore.Relational.Tests/TestUtilities/FakeDiagnosticsLogger.cs @@ -36,5 +36,7 @@ public void Log( public IDisposable BeginScope(TState state) => null; public virtual LoggingDefinitions Definitions { get; } = new TestRelationalLoggingDefinitions(); + + public IInterceptors Interceptors { get; } } } diff --git a/test/EFCore.Specification.Tests/TestUtilities/TestLogger`.cs b/test/EFCore.Specification.Tests/TestUtilities/TestLogger`.cs index e9324d85851..b5499345566 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/TestLogger`.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/TestLogger`.cs @@ -9,5 +9,6 @@ public class TestLogger : TestLogger, IDi where TCategory : LoggerCategory, new() where TDefinitions : LoggingDefinitions, new() { + public IInterceptors Interceptors { get; } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/InterceptionSqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/InterceptionSqlServerTest.cs new file mode 100644 index 00000000000..ecd7074257a --- /dev/null +++ b/test/EFCore.SqlServer.FunctionalTests/InterceptionSqlServerTest.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.SqlServer.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public class InterceptionSqlServerTest + : InterceptionTestBase, + IClassFixture + { + private const string DatabaseName = "Interception"; + + public InterceptionSqlServerTest(InterceptionSqlServerFixture fixture) + : base(fixture) + { + } + + public override async Task Intercept_query_passively(bool async, bool inject) + { + AssertSql( + @"SELECT [s].[Id], [s].[Type] FROM [Singularity] AS [s]", + await base.Intercept_query_passively(async, inject)); + + return null; + } + + public override async Task Intercept_query_to_mutate_command(bool async, bool inject) + { + AssertSql( + @"SELECT [s].[Id], [s].[Type] FROM [Brane] AS [s]", + await base.Intercept_query_to_mutate_command(async, inject)); + + return null; + } + + public override async Task Intercept_query_to_replace_execution(bool async, bool inject) + { + AssertSql( + @"SELECT [s].[Id], [s].[Type] FROM [Singularity] AS [s]", + await base.Intercept_query_to_replace_execution(async, inject)); + + return null; + } + + public class InterceptionSqlServerFixture : InterceptionFixtureBase + { + protected override string StoreName => DatabaseName; + + 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; + } + } +} diff --git a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerTestStore.cs b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerTestStore.cs index 48dde886d6e..99a1a181168 100644 --- a/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerTestStore.cs +++ b/test/EFCore.SqlServer.FunctionalTests/TestUtilities/SqlServerTestStore.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore.Infrastructure; #pragma warning disable IDE0022 // Use block body for methods // ReSharper disable SuggestBaseTypeForParameter @@ -94,8 +95,13 @@ protected override void Initialize(Func createContext, Action configureSqlServer) + => builder.UseSqlServer(Connection, b => configureSqlServer?.Invoke(b)); + public override DbContextOptionsBuilder AddProviderOptions(DbContextOptionsBuilder builder) - => builder.UseSqlServer(Connection, b => b.ApplyConfiguration().CommandTimeout(CommandTimeout)); + => AddProviderOptions(builder, configureSqlServer: null); private bool CreateDatabase(Action clean) { diff --git a/test/EFCore.Sqlite.FunctionalTests/InterceptionSqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/InterceptionSqliteTest.cs new file mode 100644 index 00000000000..c5bd55e3091 --- /dev/null +++ b/test/EFCore.Sqlite.FunctionalTests/InterceptionSqliteTest.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Sqlite.Infrastructure.Internal; +using Microsoft.EntityFrameworkCore.TestUtilities; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.EntityFrameworkCore +{ + public class InterceptionSqliteTest + : InterceptionTestBase, + IClassFixture + { + private const string DatabaseName = "Interception"; + + public InterceptionSqliteTest(InterceptionSqliteFixture fixture) + : base(fixture) + { + } + + public override async Task Intercept_query_passively(bool async, bool inject) + { + AssertSql( + @"SELECT ""s"".""Id"", ""s"".""Type"" FROM ""Singularity"" AS ""s""", + await base.Intercept_query_passively(async, inject)); + + return null; + } + + public override async Task Intercept_query_to_mutate_command(bool async, bool inject) + { + AssertSql( + @"SELECT ""s"".""Id"", ""s"".""Type"" FROM ""Brane"" AS ""s""", + await base.Intercept_query_to_mutate_command(async, inject)); + + return null; + } + + public override async Task Intercept_query_to_replace_execution(bool async, bool inject) + { + AssertSql( + @"SELECT ""s"".""Id"", ""s"".""Type"" FROM ""Singularity"" AS ""s""", + await base.Intercept_query_to_replace_execution(async, inject)); + + return null; + } + + public class InterceptionSqliteFixture : InterceptionFixtureBase + { + protected override string StoreName => DatabaseName; + + 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; + } + } +} diff --git a/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteDatabaseCleaner.cs b/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteDatabaseCleaner.cs index b027047292b..3745a667204 100644 --- a/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteDatabaseCleaner.cs +++ b/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteDatabaseCleaner.cs @@ -30,6 +30,8 @@ protected override IDatabaseModelFactory CreateDatabaseModelFactory(ILoggerFacto .AddSingleton() .AddSingleton(typeof(IDiagnosticsLogger<>), typeof(DiagnosticsLogger<>)) .AddSingleton() + .AddSingleton() + .AddSingleton() .AddLogging(); new SqliteDesignTimeServices().ConfigureDesignTimeServices(services); diff --git a/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteTestStore.cs b/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteTestStore.cs index c2fffbfb780..4cd4885292a 100644 --- a/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteTestStore.cs +++ b/test/EFCore.Sqlite.FunctionalTests/TestUtilities/SqliteTestStore.cs @@ -46,8 +46,18 @@ private SqliteTestStore(string name, bool seed = true, bool sharedCache = true, Connection = connection; } + public virtual DbContextOptionsBuilder AddProviderOptions( + DbContextOptionsBuilder builder, + Action configureSqlite) + => builder.UseSqlite( + Connection, b => + { + b.CommandTimeout(CommandTimeout); + configureSqlite?.Invoke(b); + }); + public override DbContextOptionsBuilder AddProviderOptions(DbContextOptionsBuilder builder) - => builder.UseSqlite(Connection, b => b.CommandTimeout(CommandTimeout)); + => AddProviderOptions(builder, configureSqlite: null); public SqliteTestStore InitializeSqlite(IServiceProvider serviceProvider, Func createContext, Action seed) => (SqliteTestStore)Initialize(serviceProvider, createContext, seed, null); diff --git a/test/EFCore.Tests/Infrastructure/EventIdTestBase.cs b/test/EFCore.Tests/Infrastructure/EventIdTestBase.cs index 04ea497d82c..2d5e5a5072f 100644 --- a/test/EFCore.Tests/Infrastructure/EventIdTestBase.cs +++ b/test/EFCore.Tests/Infrastructure/EventIdTestBase.cs @@ -19,7 +19,8 @@ public void TestEventLogging( Type eventIdType, Type loggerExtensionsType, Type loggerDefinitionsType, - IDictionary> fakeFactories) + IDictionary> fakeFactories, + Dictionary> eventMappings = null) { var eventIdFields = eventIdType.GetTypeInfo() .DeclaredFields @@ -38,88 +39,104 @@ public void TestEventLogging( foreach (var eventIdField in eventIdFields) { var eventName = eventIdField.Name; - Assert.Contains(eventName, loggerMethods.Keys); + if (eventMappings == null + || !eventMappings.TryGetValue(eventName, out var mappedNames)) + { + mappedNames = new List + { + eventName + }; + } - var loggerMethod = loggerMethods[eventName]; + foreach (var mappedName in mappedNames) + { + Assert.Contains(mappedName, loggerMethods.Keys); - var loggerParameters = loggerMethod.GetParameters(); - var category = loggerParameters[0].ParameterType.GenericTypeArguments[0]; + var loggerMethod = loggerMethods[mappedName]; - if (category.GetTypeInfo().ContainsGenericParameters) - { - category = typeof(DbLoggerCategory.Infrastructure); - loggerMethod = loggerMethod.MakeGenericMethod(category); - } + var loggerParameters = loggerMethod.GetParameters(); + var category = loggerParameters[0].ParameterType.GenericTypeArguments[0]; - var eventId = (EventId)eventIdField.GetValue(null); + if (category.GetTypeInfo().ContainsGenericParameters) + { + category = typeof(DbLoggerCategory.Infrastructure); + loggerMethod = loggerMethod.MakeGenericMethod(category); + } - Assert.InRange(eventId.Id, CoreEventId.CoreBaseId, ushort.MaxValue); + var eventId = (EventId)eventIdField.GetValue(null); - var categoryName = Activator.CreateInstance(category).ToString(); - Assert.Equal(categoryName + "." + eventName, eventId.Name); + Assert.InRange(eventId.Id, CoreEventId.CoreBaseId, ushort.MaxValue); - var testLogger = (TestLoggerBase)Activator.CreateInstance(typeof(TestLogger<,>).MakeGenericType(category, loggerDefinitionsType)); - var testDiagnostics = (TestDiagnosticSource)testLogger.DiagnosticSource; + var categoryName = Activator.CreateInstance(category).ToString(); + Assert.Equal(categoryName + "." + eventName, eventId.Name); - var args = new object[loggerParameters.Length]; - args[0] = testLogger; + var testLogger = + (TestLoggerBase)Activator.CreateInstance(typeof(TestLogger<,>).MakeGenericType(category, loggerDefinitionsType)); + var testDiagnostics = (TestDiagnosticSource)testLogger.DiagnosticSource; - for (var i = 1; i < args.Length; i++) - { - var type = loggerParameters[i].ParameterType; + var args = new object[loggerParameters.Length]; + args[0] = testLogger; - if (fakeFactories.TryGetValue(type, out var factory)) + for (var i = 1; i < args.Length; i++) { - args[i] = factory(); - } - else - { - try + var type = loggerParameters[i].ParameterType; + + if (fakeFactories.TryGetValue(type, out var factory)) { - args[i] = Activator.CreateInstance(type); + args[i] = factory(); } - catch (Exception) + else { - Assert.True(false, "Need to add factory for type " + type.DisplayName()); + try + { + args[i] = Activator.CreateInstance(type); + } + catch (Exception) + { + Assert.True(false, "Need to add factory for type " + type.DisplayName()); + } } } - } - foreach (var enableFor in new[] { "Foo", eventId.Name }) - { - testDiagnostics.EnableFor = enableFor; - - var logged = false; - foreach (LogLevel logLevel in Enum.GetValues(typeof(LogLevel))) + foreach (var enableFor in new[] { - testLogger.EnabledFor = logLevel; - testLogger.LoggedAt = null; - testDiagnostics.LoggedEventName = null; - - loggerMethod.Invoke(null, args); + "Foo", eventId.Name + }) + { + testDiagnostics.EnableFor = enableFor; - if (testLogger.LoggedAt != null) + var logged = false; + foreach (LogLevel logLevel in Enum.GetValues(typeof(LogLevel))) { - Assert.Equal(logLevel, testLogger.LoggedAt); - logged = true; - } + testLogger.EnabledFor = logLevel; + testLogger.LoggedAt = null; + testDiagnostics.LoggedEventName = null; - if (enableFor == eventId.Name - && categoryName != DbLoggerCategory.Scaffolding.Name) - { - Assert.Equal(eventId.Name, testDiagnostics.LoggedEventName); - if (testDiagnostics.LoggedMessage != null) + loggerMethod.Invoke(null, args); + + if (testLogger.LoggedAt != null) { - Assert.Equal(testLogger.Message, testDiagnostics.LoggedMessage); + Assert.Equal(logLevel, testLogger.LoggedAt); + logged = true; + } + + if (enableFor == eventId.Name + && categoryName != DbLoggerCategory.Scaffolding.Name) + { + Assert.Equal(eventId.Name, testDiagnostics.LoggedEventName); + if (testDiagnostics.LoggedMessage != null) + { + Assert.Equal(testLogger.Message, testDiagnostics.LoggedMessage); + } + } + else + { + Assert.Null(testDiagnostics.LoggedEventName); } } - else - { - Assert.Null(testDiagnostics.LoggedEventName); - } - } - Assert.True(logged, "Failed for " + eventId.Name); + Assert.True(logged, "Failed for " + eventId.Name); + } } } } diff --git a/test/EFCore.Tests/Infrastructure/InterceptorsDependenciesTest.cs b/test/EFCore.Tests/Infrastructure/InterceptorsDependenciesTest.cs new file mode 100644 index 00000000000..20bcc5ff669 --- /dev/null +++ b/test/EFCore.Tests/Infrastructure/InterceptorsDependenciesTest.cs @@ -0,0 +1,18 @@ +// 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(); + } + } +}