From 3405d8171963d09f77be2230048de76422ef43d2 Mon Sep 17 00:00:00 2001 From: thepirat000 Date: Wed, 4 Sep 2024 17:35:53 -0600 Subject: [PATCH] Changes for v27.0.1 --- CHANGELOG.md | 5 ++ Directory.Build.props | 2 +- src/Audit.EntityFramework/CommandEvent.cs | 10 ++++ .../Interceptors/AuditCommandInterceptor.cs | 50 ++++++++++++----- src/Audit.EntityFramework/README.md | 4 +- src/Audit.FileSystem/README.md | 2 +- src/Audit.HttpClient/README.md | 2 +- src/Audit.MongoClient/README.md | 2 +- src/Audit.SignalR/AuditHubFilter.cs | 2 +- src/Audit.WCF.Client/AuditBehavior.cs | 3 + src/Audit.WCF.Client/AuditEndpointBehavior.cs | 13 ++++- src/Audit.WCF.Client/AuditMessageInspector.cs | 14 ++++- src/Audit.WCF.Client/README.md | 2 + src/Audit.WCF/AuditOperationInvoker.cs | 16 +++--- src/Audit.WCF/README.md | 6 ++ .../DbCommandInterceptorTests.cs | 55 +++++++++++++++++-- .../WcfClientTests.cs | 40 +++++++++++--- test/Audit.Wcf.UnitTest/WCFTests.cs | 23 +++++++- 18 files changed, 203 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a514de1a..838a1738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ All notable changes to Audit.NET and its extensions will be documented in this f The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). +## [27.0.1] - 2024-09-05: +- Audit.EntityFramework.Core: Introducing the CommandSource property to the audit output for EF Core 6 and above, along with configurable options to determine whether to include reader events based on the command event data +- Audit.WCF: Allow configuring custom `IAuditScopeFactory` and `AuditDataProvider`. +- Audit.WCF.Client: Allow configuring custom `IAuditScopeFactory` and `AuditDataProvider`. + ## [27.0.0] - 2024-09-03: - Audit.NET: Introducing an `Items` collection in the `AuditScope` to store custom data accessible throughout the audit scope's lifecycle. - Audit.NET: Refactoring `AuditScopeOptions` to eliminate the dependency on the static `Audit.Core.Configuration`. diff --git a/Directory.Build.props b/Directory.Build.props index 2b829212..60bccd6a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 27.0.0 + 27.0.1 false diff --git a/src/Audit.EntityFramework/CommandEvent.cs b/src/Audit.EntityFramework/CommandEvent.cs index c365c317..f6cba590 100644 --- a/src/Audit.EntityFramework/CommandEvent.cs +++ b/src/Audit.EntityFramework/CommandEvent.cs @@ -16,14 +16,24 @@ public class CommandEvent : InterceptorEventBase /// The command method (NonQuery, Scalar, Reader) /// public DbCommandMethod Method { get; set; } + /// /// The command type (Text, StoredProcedure) /// public CommandType CommandType { get; set; } + +#if NET6_0_OR_GREATER + /// + /// Indicates the source of the being used to execute the command. + /// + public CommandSource CommandSource { get; set; } +#endif + /// /// The command text /// public string CommandText { get; set; } + /// /// The parameter values /// diff --git a/src/Audit.EntityFramework/Interceptors/AuditCommandInterceptor.cs b/src/Audit.EntityFramework/Interceptors/AuditCommandInterceptor.cs index bc10bfc0..a31c2caa 100644 --- a/src/Audit.EntityFramework/Interceptors/AuditCommandInterceptor.cs +++ b/src/Audit.EntityFramework/Interceptors/AuditCommandInterceptor.cs @@ -1,16 +1,17 @@ #if EF_CORE_5_OR_GREATER -using Audit.Core; -using Audit.Core.Extensions; -using Microsoft.EntityFrameworkCore.Diagnostics; +using System; using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Linq; using System.Threading; using System.Threading.Tasks; + +using Audit.Core; +using Audit.Core.Extensions; + using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Internal; +using Microsoft.EntityFrameworkCore.Diagnostics; namespace Audit.EntityFramework.Interceptors { @@ -26,13 +27,20 @@ namespace Audit.EntityFramework.Interceptors public class AuditCommandInterceptor : DbCommandInterceptor { /// - /// Boolean value to indicate whether to log the command parameter values. By default (when null) it will depend on EnableSensitiveDataLogging setting on the DbContext. + /// Boolean value to indicate whether to log the command parameter values. By default, (when null) it will depend on EnableSensitiveDataLogging setting on the DbContext. /// public bool? LogParameterValues { get; set; } + /// /// Boolean value to indicate whether to exclude the events handled by ReaderExecuting. Default is false to include the ReaderExecuting events. /// public bool ExcludeReaderEvents { get; set; } + + /// + /// Predicate to include the ReaderExecuting events based on the event data. By default, all the ReaderExecuting events are included. + /// This predicate is ignored if ExcludeReaderEvents is set to true. + /// + public Func IncludeReaderEventsPredicate { get; set; } /// /// Boolean value to indicate whether to exclude the events handled by NonQueryExecuting. Default is false to include the NonQueryExecuting events. @@ -61,10 +69,19 @@ public class AuditCommandInterceptor : DbCommandInterceptor private readonly DbContextHelper _dbContextHelper = new DbContextHelper(); private IAuditScope _currentScope; + /// + /// Returns the current audit scope, if any + /// + /// + protected IAuditScope GetAuditScope() + { + return _currentScope; + } + #region "Reader" public override InterceptionResult ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult result) { - if (Core.Configuration.AuditDisabled || ExcludeReaderEvents) + if (Core.Configuration.AuditDisabled || ExcludeReaderEvents || IncludeReaderEventsPredicate?.Invoke(eventData) == false) { return result; } @@ -78,7 +95,7 @@ public override InterceptionResult ReaderExecuting(DbCommand comma public override DbDataReader ReaderExecuted(DbCommand command, CommandExecutedEventData eventData, DbDataReader result) { - if (ExcludeReaderEvents) + if (_currentScope == null) { return result; } @@ -93,7 +110,7 @@ public override DbDataReader ReaderExecuted(DbCommand command, CommandExecutedEv public override async ValueTask> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) { - if (Core.Configuration.AuditDisabled || ExcludeReaderEvents) + if (Core.Configuration.AuditDisabled || ExcludeReaderEvents || IncludeReaderEventsPredicate?.Invoke(eventData) == false) { return await base.ReaderExecutingAsync(command, eventData, result, cancellationToken); } @@ -107,7 +124,7 @@ public override async ValueTask> ReaderExecutin public override async ValueTask ReaderExecutedAsync(DbCommand command, CommandExecutedEventData eventData, DbDataReader result, CancellationToken cancellationToken = default) { - if (ExcludeReaderEvents) + if (_currentScope == null) { return await base.ReaderExecutedAsync(command, eventData, result, cancellationToken); } @@ -139,7 +156,7 @@ public override InterceptionResult NonQueryExecuting(DbCommand command, Com public override int NonQueryExecuted(DbCommand command, CommandExecutedEventData eventData, int result) { - if (ExcludeNonQueryEvents) + if (_currentScope == null) { return result; } @@ -162,7 +179,7 @@ public override async ValueTask> NonQueryExecutingAsync( } public override async ValueTask NonQueryExecutedAsync(DbCommand command, CommandExecutedEventData eventData, int result, CancellationToken cancellationToken = default) { - if (ExcludeNonQueryEvents) + if (_currentScope == null) { return await base.NonQueryExecutedAsync(command, eventData, result, cancellationToken); } @@ -190,7 +207,7 @@ public override InterceptionResult ScalarExecuting(DbCommand command, Co public override object ScalarExecuted(DbCommand command, CommandExecutedEventData eventData, object result) { - if (ExcludeScalarEvents) + if (_currentScope == null) { return result; } @@ -198,6 +215,7 @@ public override object ScalarExecuted(DbCommand command, CommandExecutedEventDat EndScope(); return result; } + public override async ValueTask> ScalarExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) { if (Core.Configuration.AuditDisabled || ExcludeScalarEvents) @@ -211,9 +229,10 @@ public override async ValueTask> ScalarExecutingAsync _currentScope = await CreateAuditScopeAsync(auditEvent, cancellationToken); return await base.ScalarExecutingAsync(command, eventData, result, cancellationToken); } + public override async ValueTask ScalarExecutedAsync(DbCommand command, CommandExecutedEventData eventData, object result, CancellationToken cancellationToken = default) { - if (ExcludeScalarEvents) + if (_currentScope == null) { return await base.ScalarExecutedAsync(command, eventData, result, cancellationToken); } @@ -248,6 +267,9 @@ protected virtual CommandEvent CreateAuditEvent(DbCommand command, CommandEventD { CommandText = command.CommandText, CommandType = command.CommandType, +#if NET6_0_OR_GREATER + CommandSource = eventData.CommandSource, +#endif ConnectionId = _dbContextHelper.GetClientConnectionId(command.Connection), DbConnectionId = eventData.ConnectionId.ToString(), Database = command.Connection?.Database, diff --git a/src/Audit.EntityFramework/README.md b/src/Audit.EntityFramework/README.md index 401d1843..d3e60b22 100644 --- a/src/Audit.EntityFramework/README.md +++ b/src/Audit.EntityFramework/README.md @@ -271,6 +271,7 @@ optionsBuilder.AddInterceptors(new AuditCommandInterceptor() - **LogParameterValues**: Boolean value to indicate whether to log the command parameter values. By default (when null) it will depend on EnableSensitiveDataLogging setting on the DbContext. - **ExcludeReaderEvents**: Boolean value to indicate whether to exclude the events handled by ReaderExecuting. Default is false to include the ReaderExecuting events. +- **IncludeReaderEventsPredicate**: Predicate to include the ReaderExecuting events based on the event data. By default, all the ReaderExecuting events are included. This predicate is ignored if ExcludeReaderEvents is set to true. - **ExcludeNonQueryEvents**: Boolean value to indicate whether to exclude the events handled by NonQueryExecuting. Default is false to include the NonQueryExecuting events. - **ExcludeScalarEvents**: Boolean value to indicate whether to exclude the events handled by ScalarExecuting. Default is false to include the ScalarExecuting events. - **AuditEventType**: To indicate the event type to use on the audit event. (Default is the execute method name). Can contain the following placeholders: @@ -520,7 +521,8 @@ The following table describes the output fields for the low-level command interc | **ConnectionId** | Guid | A unique identifier for the database connection. | | **ContextId** | string | A unique identifier for the context instance and pool lease. | | **Method** | string | The command method executed (NonQuery, Scalar, Reader) | -| **CommandType** | string | The command type (Text, StoredProcedure) | +| **CommandType** | [CommandType](https://learn.microsoft.com/en-us/dotnet/api/system.data.commandtype) | The command type (Text, StoredProcedure, etc) | +| **CommandSource** | [CommandSource](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.diagnostics.commandsource) | The command source type (SaveChanges, LinqQuery, etc) | | **CommandText** | string | The command text | | **Parameters** | Dictionary | The parameter values, if any, when `EnableSensitiveDataLogging` is enabled | | **IsAsync** | boolean | Indicates whether the call was asynchronous | diff --git a/src/Audit.FileSystem/README.md b/src/Audit.FileSystem/README.md index 60e9ab3a..ab2539bc 100644 --- a/src/Audit.FileSystem/README.md +++ b/src/Audit.FileSystem/README.md @@ -74,7 +74,7 @@ By default content is not included. - **InternalBufferSize**: Gets or sets the size (in bytes) of the [internal buffer](https://docs.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher.internalbuffersize?view=netstandard-2.0#System_IO_FileSystemWatcher_InternalBufferSize). - **AuditDataProvider**: To indicate the Audit Data Provider to use. Default is NULL to use the [globally configured data provider](https://github.com/thepirat000/Audit.NET#data-provider). - **CreationPolicy**: To indicate the event creation policy to use. Default is NULL to use the [globally configured creation policy](https://github.com/thepirat000/Audit.NET#creation-policy). -- **AuditScopeFactory**: Allows to set a specific audit scope factory. By default the general [`AuditScopeFactory`](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.NET/AuditScopeFactory.cs) is used. +- **AuditScopeFactory**: Allows to set a specific audit scope factory. By default the globally configured [`AuditScopeFactory`](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.NET/AuditScopeFactory.cs) is used. ## Output diff --git a/src/Audit.HttpClient/README.md b/src/Audit.HttpClient/README.md index c7dd059c..3be3532a 100644 --- a/src/Audit.HttpClient/README.md +++ b/src/Audit.HttpClient/README.md @@ -93,7 +93,7 @@ The `AuditHttpClientHandler` class allows to configure the following settings: - **IncludeOptions**: Specifies which [HTTP Request Options](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httprequestmessage.options?view=net-8.0) should be included in the audit output. Useful to add contextual information to the HTTP Audit Event. By default, the options are not included. - **CreationPolicy**: Allows to set a specific event creation policy. By default the globally configured creation policy is used. See [Audit.NET Event Creation Policy](https://github.com/thepirat000/Audit.NET#event-creation-policy) section for more information. - **AuditDataProvider**: Allows to set a specific audit data provider. By default the globally configured data provider is used. See [Audit.NET Data Providers](https://github.com/thepirat000/Audit.NET/blob/master/README.md#data-providers) section for more information. -- **AuditScopeFactory**: Allows to set a specific audit scope factory. By default the general [`AuditScopeFactory`](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.NET/AuditScopeFactory.cs) is used. +- **AuditScopeFactory**: Allows to set a specific audit scope factory. By default the globally configured [`AuditScopeFactory`](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.NET/AuditScopeFactory.cs) is used. ## Output Details diff --git a/src/Audit.MongoClient/README.md b/src/Audit.MongoClient/README.md index 0fa7d741..d56aba68 100644 --- a/src/Audit.MongoClient/README.md +++ b/src/Audit.MongoClient/README.md @@ -89,7 +89,7 @@ The `MongoAuditEventSubscriber` class allows to configure the following settings - **CommandFilter**: Set a filter function to determine which command events to log depending on the command start information. By default all commands are logged. - **CreationPolicy**: Allows to set a specific event creation policy. By default the globally configured creation policy is used. See [Audit.NET Event Creation Policy](https://github.com/thepirat000/Audit.NET#event-creation-policy) section for more information. - **AuditDataProvider**: Allows to set a specific audit data provider. By default the globally configured data provider is used. See [Audit.NET Data Providers](https://github.com/thepirat000/Audit.NET/blob/master/README.md#data-providers) section for more information. -- **AuditScopeFactory**: Allows to set a specific audit scope factory. By default the general [`AuditScopeFactory`](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.NET/AuditScopeFactory.cs) is used. +- **AuditScopeFactory**: Allows to set a specific audit scope factory. By default the globally configured [`AuditScopeFactory`](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.NET/AuditScopeFactory.cs) is used. You can customize these settings using the fluent API provided. Additionally, some settings can be set as functions of the executed command, allowing you to adapt the behavior based on the specific command, such as including the reply only in specific cases. diff --git a/src/Audit.SignalR/AuditHubFilter.cs b/src/Audit.SignalR/AuditHubFilter.cs index 5ea3f112..288b5613 100644 --- a/src/Audit.SignalR/AuditHubFilter.cs +++ b/src/Audit.SignalR/AuditHubFilter.cs @@ -18,7 +18,7 @@ public class AuditHubFilter : IHubFilter public Func ConnectEventsFilter { get; set; } public Func DisconnectEventsFilter { get; set; } - public AuditDataProvider AuditDataProvider { get; set; } + public AuditDataProvider? AuditDataProvider { get; set; } public EventCreationPolicy? CreationPolicy { get; set; } public string AuditEventType { get; set; } public bool AuditDisabled { get; set; } diff --git a/src/Audit.WCF.Client/AuditBehavior.cs b/src/Audit.WCF.Client/AuditBehavior.cs index ee67ca4e..04fb26ee 100644 --- a/src/Audit.WCF.Client/AuditBehavior.cs +++ b/src/Audit.WCF.Client/AuditBehavior.cs @@ -5,6 +5,9 @@ namespace Audit.Wcf.Client { + /// + /// WCF client audit behavior configuration + /// public class AuditBehavior : BehaviorExtensionElement { /// diff --git a/src/Audit.WCF.Client/AuditEndpointBehavior.cs b/src/Audit.WCF.Client/AuditEndpointBehavior.cs index 6fc812a8..86a74278 100644 --- a/src/Audit.WCF.Client/AuditEndpointBehavior.cs +++ b/src/Audit.WCF.Client/AuditEndpointBehavior.cs @@ -1,5 +1,6 @@ using System.ServiceModel.Description; using System.ServiceModel.Dispatcher; +using Audit.Core; namespace Audit.Wcf.Client { @@ -20,6 +21,16 @@ public class AuditEndpointBehavior : IEndpointBehavior /// public bool IncludeResponseHeaders { get; set; } + /// + /// The factory to create the audit scopes. Default is NULL to use the globally configured AuditScopeFactory. + /// + public IAuditScopeFactory AuditScopeFactory { get; set; } + + /// + /// The data provider to use. Default is NULL to use the globally configured AuditDataProvider. + /// + public AuditDataProvider AuditDataProvider { get; set; } + public AuditEndpointBehavior() { } @@ -36,7 +47,7 @@ public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.C } public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { - clientRuntime.ClientMessageInspectors.Add(new AuditMessageInspector(EventType, IncludeRequestHeaders, IncludeResponseHeaders)); + clientRuntime.ClientMessageInspectors.Add(new AuditMessageInspector(EventType, IncludeRequestHeaders, IncludeResponseHeaders, AuditScopeFactory, AuditDataProvider)); } public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { diff --git a/src/Audit.WCF.Client/AuditMessageInspector.cs b/src/Audit.WCF.Client/AuditMessageInspector.cs index 81492e3d..18cc07a3 100644 --- a/src/Audit.WCF.Client/AuditMessageInspector.cs +++ b/src/Audit.WCF.Client/AuditMessageInspector.cs @@ -16,16 +16,21 @@ public class AuditMessageInspector : IClientMessageInspector private readonly string _eventType = "{action}"; private readonly bool _includeRequestHeaders; private readonly bool _includeResponseHeaders; + private readonly IAuditScopeFactory _auditScopeFactory; + private readonly AuditDataProvider _auditDataProvider; public AuditMessageInspector() { } - public AuditMessageInspector(string eventType, bool includeRequestHeaders, bool includeResponseHeaders) + public AuditMessageInspector(string eventType, bool includeRequestHeaders, bool includeResponseHeaders, IAuditScopeFactory auditScopeFactory, + AuditDataProvider auditDataProvider) { _eventType = eventType ?? "{action}"; _includeRequestHeaders = includeRequestHeaders; _includeResponseHeaders = includeResponseHeaders; + _auditScopeFactory = auditScopeFactory; + _auditDataProvider = auditDataProvider; } public object BeforeSendRequest(ref Message request, IClientChannel channel) @@ -39,12 +44,15 @@ public object BeforeSendRequest(ref Message request, IClientChannel channel) WcfClientEvent = auditWcfEvent }; + var auditScopeFactory = _auditScopeFactory ?? Configuration.AuditScopeFactory; + // Create the audit scope - var auditScope = Configuration.AuditScopeFactory.Create(new AuditScopeOptions() + var auditScope = auditScopeFactory.Create(new AuditScopeOptions() { EventType = eventType, AuditEvent = auditEventWcf, - SkipExtraFrames = 8 + SkipExtraFrames = 8, + DataProvider = _auditDataProvider }); return auditScope; } diff --git a/src/Audit.WCF.Client/README.md b/src/Audit.WCF.Client/README.md index 75bdda41..5696b34f 100644 --- a/src/Audit.WCF.Client/README.md +++ b/src/Audit.WCF.Client/README.md @@ -93,6 +93,8 @@ The Audit Behavior can be configured with the following properties: - \{action}: Replaced with the action URL - **IncludeRequestHeaders**: Boolean value that indicates whether the output should include the request headers. Default is false. - **IncludeResponseHeaders**: Boolean value that indicates whether the output should include the response headers. Default is false. +- **AuditDataProvider**: Allows to set a specific audit data provider. By default the globally configured data provider is used. See [Audit.NET Data Providers](https://github.com/thepirat000/Audit.NET/blob/master/README.md#data-providers) section for more information. +- **AuditScopeFactory**: Allows to set a specific audit scope factory. By default the globally configured [`AuditScopeFactory`](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.NET/AuditScopeFactory.cs) is used. ### Output mechanism diff --git a/src/Audit.WCF/AuditOperationInvoker.cs b/src/Audit.WCF/AuditOperationInvoker.cs index bdbdf9aa..2c78af37 100644 --- a/src/Audit.WCF/AuditOperationInvoker.cs +++ b/src/Audit.WCF/AuditOperationInvoker.cs @@ -62,12 +62,13 @@ public object Invoke(object instance, object[] inputs, out object[] outputs) WcfEvent = auditWcfEvent }; // Create the audit scope - using (var auditScope = Configuration.AuditScopeFactory.Create(new AuditScopeOptions() + var auditScopeFactory = GetProperty(instance, "AuditScopeFactory") ?? Configuration.AuditScopeFactory; + using (var auditScope = auditScopeFactory.Create(new AuditScopeOptions() { EventType = eventType, CreationPolicy = _creationPolicy, AuditEvent = auditEventWcf, - DataProvider = GetAuditDataProvider(instance), + DataProvider = GetProperty(instance, "AuditDataProvider"), CallingMethod = _operationDescription.SyncMethod ?? _operationDescription.TaskMethod })) { @@ -109,12 +110,13 @@ public IAsyncResult InvokeBegin(object instance, object[] inputs, AsyncCallback WcfEvent = auditWcfEvent }; // Create the audit scope - var auditScope = Configuration.AuditScopeFactory.Create(new AuditScopeOptions() + var auditScopeFactory = GetProperty(instance, "AuditScopeFactory") ?? Configuration.AuditScopeFactory; + var auditScope = auditScopeFactory.Create(new AuditScopeOptions() { EventType = eventType, CreationPolicy = _creationPolicy, AuditEvent = auditEventWcf, - DataProvider = GetAuditDataProvider(instance), + DataProvider = GetProperty(instance, "AuditDataProvider"), CallingMethod = _operationDescription.SyncMethod ?? _operationDescription.TaskMethod }); // Store a reference to this audit scope @@ -201,12 +203,12 @@ private WcfEvent CreateWcfAuditEvent(object instance, object[] inputs) /// /// Get the dataprovider from property AuditDataProvider /// - private AuditDataProvider GetAuditDataProvider(object instance) + private T GetProperty(object instance, string propertyName) where T : class { - var prop = instance.GetType().GetProperty("AuditDataProvider", typeof(AuditDataProvider)); + var prop = instance.GetType().GetProperty(propertyName, typeof(T)); if (prop != null) { - return prop.GetGetMethod().Invoke(instance, null) as AuditDataProvider; + return prop.GetGetMethod().Invoke(instance, null) as T; } return null; } diff --git a/src/Audit.WCF/README.md b/src/Audit.WCF/README.md index ca4790c6..1e0ff95e 100644 --- a/src/Audit.WCF/README.md +++ b/src/Audit.WCF/README.md @@ -83,6 +83,8 @@ The `AuditBehavior` attribute or extension can be configured with the following - **EventTypeName**: A string that identifies the event type. Can contain the following placeholders: - \{contract}: Replaced with the contract name (service interface name) - \{operation}: Replaces with the operation name (service method name) +- **AuditDataProvider**: Allows to set a specific audit data provider. By default the globally configured data provider is used. See [Audit.NET Data Providers](https://github.com/thepirat000/Audit.NET/blob/master/README.md#data-providers) section for more information. +- **AuditScopeFactory**: Allows to set a specific audit scope factory. By default the globally configured [`AuditScopeFactory`](https://github.com/thepirat000/Audit.NET/blob/master/src/Audit.NET/AuditScopeFactory.cs) is used. To globally configure the output persistence mechanism, use the `Audit.Core.Configuration` class. For more details please see [Event Output Configuration](https://github.com/thepirat000/Audit.NET/blob/master/README.md#event-output). @@ -114,6 +116,10 @@ public class OrderService : IOrderService The library will automatically detect the property and use the given data provider for that service instance. +You can do the same with the `AuditScopeFactory` property to provide a custom `IAuditScopeFactory` instance. + +```c# + ## Output `Audit.Wcf` output includes: diff --git a/test/Audit.EntityFramework.Core.UnitTest/DbCommandInterceptorTests.cs b/test/Audit.EntityFramework.Core.UnitTest/DbCommandInterceptorTests.cs index f9793df1..76b78ebc 100644 --- a/test/Audit.EntityFramework.Core.UnitTest/DbCommandInterceptorTests.cs +++ b/test/Audit.EntityFramework.Core.UnitTest/DbCommandInterceptorTests.cs @@ -1,9 +1,4 @@ #if EF_CORE_5_OR_GREATER -using Audit.Core; -using Audit.EntityFramework.Interceptors; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; -using NUnit.Framework; using System; using System.Collections; using System.Collections.Generic; @@ -12,8 +7,15 @@ using System.Data; using System.Linq; using System.Threading.Tasks; + +using Audit.Core; using Audit.Core.Providers; -using Microsoft.EntityFrameworkCore.Infrastructure.Internal; +using Audit.EntityFramework.Interceptors; + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; + +using NUnit.Framework; namespace Audit.EntityFramework.Core.UnitTest { @@ -317,6 +319,47 @@ public async Task Test_DbCommandInterceptor_IncludeReaderResultAsync() Assert.That(resultList.Values.First()[0]["Name"], Is.EqualTo("Test")); } +#if EF_CORE_6_OR_GREATER + [TestCase(CommandSource.LinqQuery)] + [TestCase(CommandSource.SaveChanges)] + public void Test_DbCommandInterceptor_IncludeReaderPredicate(CommandSource sourceToInclude) + { + var commandEvents = new List(); + Audit.Core.Configuration.Setup() + .UseDynamicProvider(_ => _ + .OnInsert(ev => commandEvents.Add(ev.GetCommandEntityFrameworkEvent()))) + .WithCreationPolicy(EventCreationPolicy.InsertOnEnd); + + int newId = new Random().Next(); + + var interceptor = new AuditCommandInterceptor() + { + LogParameterValues = true, + IncludeReaderEventsPredicate = c => c.CommandSource == sourceToInclude + }; + + using (var ctx = new DbCommandInterceptContext(new DbContextOptionsBuilder().AddInterceptors(interceptor).Options)) + { + // Intercepted SaveChanges + var dept = new DbCommandInterceptContext.Department() { Id = newId, Name = "Test", Comments = "Comment" }; + ctx.Departments.Add(dept); + ctx.SaveChanges(); + + // intercepted LinqQuery + dept = ctx.Departments.FirstOrDefault(d => d.Id == newId); + + Assert.That(dept, Is.Not.Null); + } + + Assert.That(commandEvents.Count, Is.EqualTo(1)); + Assert.That(commandEvents[0].Parameters, Is.Not.Null); + Assert.That(commandEvents[0].Parameters.Any(), Is.True); + Assert.That(commandEvents[0].Parameters.First().Value, Is.EqualTo(newId)); + Assert.That(commandEvents[0].Result, Is.Null); + Assert.That(commandEvents[0].CommandSource, Is.EqualTo(sourceToInclude)); + } +#endif + #if EF_CORE_7_OR_GREATER [Test] public async Task Test_DbCommandInterceptor_IncludeReaderResult_MultipleResultSets_EfCore7_Async() diff --git a/test/Audit.Wcf.Client.UnitTest/WcfClientTests.cs b/test/Audit.Wcf.Client.UnitTest/WcfClientTests.cs index 7a844377..fc24b0ce 100644 --- a/test/Audit.Wcf.Client.UnitTest/WcfClientTests.cs +++ b/test/Audit.Wcf.Client.UnitTest/WcfClientTests.cs @@ -6,6 +6,7 @@ using System.ServiceModel; using System.ServiceModel.Channels; using System.Threading; +using Audit.Core.Providers; #if NETCOREAPP3_1 using Microsoft.Extensions.Logging; using Microsoft.AspNetCore.Hosting; @@ -37,6 +38,12 @@ public void TearDown() _cancellationToken.Dispose(); } + [SetUp] + public void SetupTest() + { + Audit.Core.Configuration.Reset(); + } + #if NETCOREAPP3_1 public static IWebHostBuilder CreateWebHostBuilder() => Microsoft.AspNetCore.WebHost.CreateDefaultBuilder() @@ -54,13 +61,16 @@ public void Test_WcfClient_Success() var inserted = new List(); var replaced = new List(); var idsReplaced = new List(); - Audit.Core.Configuration.Setup() - .UseDynamicProvider(_ => _ + + Audit.Core.Configuration.Setup().UseNullProvider().WithCreationPolicy(EventCreationPolicy.InsertOnStartReplaceOnEnd); + + var dynamicProvider = new DynamicDataProvider(_ => _ .OnInsert(ev => inserted.Add(AuditEvent.FromJson(ev.ToJson()))) - .OnReplace((id, ev) => { replaced.Add(AuditEvent.FromJson(ev.ToJson())); idsReplaced.Add(id); })) - .WithCreationPolicy(EventCreationPolicy.InsertOnStartReplaceOnEnd); + .OnReplace((id, ev) => { replaced.Add(AuditEvent.FromJson(ev.ToJson())); idsReplaced.Add(id); })); - var channel = GetServiceProxy(out ICatalogService svc); + var auditScopeFactory = new TestAuditScopeFactory(); + + var channel = GetServiceProxy(out ICatalogService svc, auditScopeFactory, dynamicProvider); using (var scope = new OperationContextScope(channel)) { @@ -81,6 +91,7 @@ public void Test_WcfClient_Success() } } + Assert.That(auditScopeFactory.OnScopeCreatedCount, Is.EqualTo(1)); Assert.That(inserted.Count, Is.EqualTo(1)); Assert.That(replaced.Count, Is.EqualTo(1)); Assert.That(idsReplaced.Count, Is.EqualTo(1)); @@ -134,7 +145,7 @@ public void Test_WcfClient_Fault() .OnReplace((id, ev) => { replaced.Add(AuditEvent.FromJson(ev.ToJson())); idsReplaced.Add(id); })) .WithCreationPolicy(EventCreationPolicy.InsertOnStartReplaceOnEnd); - var channel = GetServiceProxy(out ICatalogService svc); + var channel = GetServiceProxy(out ICatalogService svc, null, null); using (var scope = new OperationContextScope(channel)) { @@ -196,7 +207,7 @@ public void Test_WcfClient_Fault() Assert.That(actionReplaced.IsFault, Is.True); } - public static IContextChannel GetServiceProxy(out ICatalogService svc) + public static IContextChannel GetServiceProxy(out ICatalogService svc, IAuditScopeFactory scopeFactory, AuditDataProvider dataProvider) { #if NET462_OR_GREATER var channelFactory = new ChannelFactory(new BasicHttpBinding(), new EndpointAddress("http://localhost:8733/Design_Time_Addresses/Audit.Wcf.UnitTest/CatalogService/")); @@ -207,11 +218,24 @@ public static IContextChannel GetServiceProxy(out ICatalogService svc) { EventType = "Catalog:{action}", IncludeRequestHeaders = true, - IncludeResponseHeaders = true + IncludeResponseHeaders = true, + AuditScopeFactory = scopeFactory, + AuditDataProvider = dataProvider + }); var x = channelFactory.CreateChannel(); svc = x; return x as IContextChannel; } } + + public class TestAuditScopeFactory : AuditScopeFactory + { + public int OnScopeCreatedCount { get; set; } + + public override void OnScopeCreated(AuditScope auditScope) + { + OnScopeCreatedCount++; + } + } } diff --git a/test/Audit.Wcf.UnitTest/WCFTests.cs b/test/Audit.Wcf.UnitTest/WCFTests.cs index 53cae9a1..3f4ff5f6 100644 --- a/test/Audit.Wcf.UnitTest/WCFTests.cs +++ b/test/Audit.Wcf.UnitTest/WCFTests.cs @@ -157,7 +157,9 @@ public void WCFTest_CreationPolicy_Manual() Assert.That(inserted.Count, Is.EqualTo(0)); Assert.That(replaced.Count, Is.EqualTo(0)); } - + + [TestCase(2, 10)] + [TestCase(6, 10)] public void WCFTest_Concurrency_AuditScope(int threads, int callsPerThread) { var provider = new Mock(); @@ -178,8 +180,10 @@ public void WCFTest_Concurrency_AuditScope(int threads, int callsPerThread) return Guid.NewGuid(); }); + var auditScopeFactory = new TestAuditScopeFactory(); + var basePipeAddress = new Uri(string.Format(@"http://localhost:{0}/test/", 10000 + new Random().Next(1, 9999))); - using (var host = new ServiceHost(new OrderService_AsyncConcurrent_Test(provider.Object), basePipeAddress)) + using (var host = new ServiceHost(new OrderService_AsyncConcurrent_Test(provider.Object, auditScopeFactory), basePipeAddress)) { var serviceEndpoint = host.AddServiceEndpoint(typeof(IOrderService), CreateBinding(), string.Empty); host.Open(); @@ -191,6 +195,7 @@ public void WCFTest_Concurrency_AuditScope(int threads, int callsPerThread) Console.WriteLine("Times: {0}.", threads * callsPerThread); Assert.That(bag.Count, Is.EqualTo(bag.Distinct().Count())); Assert.That(bag.Count, Is.EqualTo(threads * callsPerThread)); + Assert.That(auditScopeFactory.OnScopeCreatedCount, Is.EqualTo(threads * callsPerThread)); } @@ -276,14 +281,17 @@ public class Order IncludeExceptionDetailInFaults = true)] public class OrderService_AsyncConcurrent_Test : OrderService, IOrderService { + private IAuditScopeFactory _auditScopeFactory; private AuditDataProvider _auditDataProvider; - public OrderService_AsyncConcurrent_Test(AuditDataProvider dp) + public OrderService_AsyncConcurrent_Test(AuditDataProvider dp, IAuditScopeFactory auditScopeFactory) { _auditDataProvider = dp; + _auditScopeFactory = auditScopeFactory; } public AuditDataProvider AuditDataProvider => _auditDataProvider; + public IAuditScopeFactory AuditScopeFactory => _auditScopeFactory; } @@ -338,6 +346,15 @@ public async Task TestAsync(int sleep) await Task.Delay(sleep); return sleep.ToString(); } + } + + public class TestAuditScopeFactory : AuditScopeFactory + { + public int OnScopeCreatedCount { get; set; } + public override void OnScopeCreated(AuditScope auditScope) + { + OnScopeCreatedCount++; + } } } \ No newline at end of file