From f5e663c89922488a7898d7beaad2bdc6e2f6eaf9 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Tue, 28 May 2019 07:02:19 -0700 Subject: [PATCH] IDbCommandInterceptor! Fixes #15066 The general idea is the same as `IDbCommandInterceptor` in EF6. Specifically: * Command interceptors can be used to view, change, or suppress execution of the `DbCommand` and to modify the result before it is returned to EF. `DbCommandInterceptor` is provided as an abstract base class. * Example of registration: ```C# builder.UseSqlServer( myConnectionString, b => b.CommandInterceptor(myInterceptor); ``` * Multiple interceptors can be composed using the `CompositeDbCommandInterceptor`. * 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. --- .../DesignTimeServiceCollectionExtensions.cs | 2 + .../CompositeDbCommandInterceptor.cs | 318 +++ .../Diagnostics/DbCommandInterceptor.cs | 331 ++++ .../Diagnostics/IDbCommandInterceptor.cs | 338 ++++ .../Diagnostics/IRelationalInterceptors.cs | 34 + .../Diagnostics/RelationalInterceptors.cs | 110 ++ .../RelationalInterceptorsDependencies.cs | 70 + .../Diagnostics/RelationalLoggerExtensions.cs | 691 ++++++- ...ntityFrameworkRelationalServicesBuilder.cs | 6 + .../RelationalDbContextOptionsBuilder.cs | 18 +- .../RelationalOptionsExtension.cs | 23 + .../Storage/RelationalCommand.cs | 197 +- src/EFCore/Diagnostics/IDiagnosticsLogger`.cs | 11 +- src/EFCore/Diagnostics/IInterceptors.cs | 30 + src/EFCore/Diagnostics/InterceptionResult.cs | 39 + src/EFCore/Diagnostics/Interceptors.cs | 44 + .../Diagnostics/InterceptorsDependencies.cs | 73 + .../EntityFrameworkServicesBuilder.cs | 5 +- src/EFCore/Internal/DiagnosticsLogger.cs | 19 +- .../InterceptionTestBase.cs | 1726 +++++++++++++++++ ...nterceptorsDependenciesDependenciesTest.cs | 18 + .../RelationalEventIdTest.cs | 27 +- .../TestUtilities/FakeDiagnosticsLogger.cs | 2 + .../TestUtilities/TestLogger`.cs | 1 + .../InterceptionSqlServerTest.cs | 76 + .../TestUtilities/SqlServerTestStore.cs | 8 +- .../InterceptionSqliteTest.cs | 76 + .../TestUtilities/SqliteDatabaseCleaner.cs | 2 + .../TestUtilities/SqliteTestStore.cs | 12 +- .../Infrastructure/EventIdTestBase.cs | 133 +- .../InterceptorsDependenciesTest.cs | 18 + 31 files changed, 4276 insertions(+), 182 deletions(-) create mode 100644 src/EFCore.Relational/Diagnostics/CompositeDbCommandInterceptor.cs create mode 100644 src/EFCore.Relational/Diagnostics/DbCommandInterceptor.cs create mode 100644 src/EFCore.Relational/Diagnostics/IDbCommandInterceptor.cs create mode 100644 src/EFCore.Relational/Diagnostics/IRelationalInterceptors.cs create mode 100644 src/EFCore.Relational/Diagnostics/RelationalInterceptors.cs create mode 100644 src/EFCore.Relational/Diagnostics/RelationalInterceptorsDependencies.cs create mode 100644 src/EFCore/Diagnostics/IInterceptors.cs create mode 100644 src/EFCore/Diagnostics/InterceptionResult.cs create mode 100644 src/EFCore/Diagnostics/Interceptors.cs create mode 100644 src/EFCore/Diagnostics/InterceptorsDependencies.cs create mode 100644 test/EFCore.Relational.Specification.Tests/InterceptionTestBase.cs create mode 100644 test/EFCore.Relational.Tests/Infrastructure/RelationalInterceptorsDependenciesDependenciesTest.cs create mode 100644 test/EFCore.SqlServer.FunctionalTests/InterceptionSqlServerTest.cs create mode 100644 test/EFCore.Sqlite.FunctionalTests/InterceptionSqliteTest.cs create mode 100644 test/EFCore.Tests/Infrastructure/InterceptorsDependenciesTest.cs 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(); + } + } +}