From 499a37015cb342d87fb8f5722bfcdc34e062863e Mon Sep 17 00:00:00 2001 From: thepirat000 Date: Tue, 3 Sep 2024 11:40:26 -0600 Subject: [PATCH 1/4] Changes for the version 27 --- CHANGELOG.md | 12 + Directory.Build.props | 2 +- README.md | 112 +++++--- src/Audit.EntityFramework/DbContextHelper.cs | 50 +++- .../Interceptors/AuditCommandInterceptor.cs | 14 +- .../AuditTransactionInterceptor.cs | 13 +- src/Audit.EntityFramework/README.md | 50 +++- src/Audit.Mvc/AuditAttribute.Core.cs | 9 +- src/Audit.Mvc/AuditAttribute.cs | 2 +- src/Audit.Mvc/AuditPageFilter.cs | 19 +- .../Providers/NLogDataProvider.cs | 2 +- .../Providers/SerilogDataProvider.cs | 2 +- .../Providers/Log4netDataProvider.cs | 2 +- src/Audit.NET/Audit.NET.csproj | 2 +- src/Audit.NET/AuditScope.Helpers.cs | 4 +- src/Audit.NET/AuditScope.cs | 137 +++++----- src/Audit.NET/AuditScopeFactory.cs | 142 +++++++--- src/Audit.NET/AuditScopeOptions.cs | 82 ++---- .../AuditScopeOptionsConfigurator.cs | 23 ++ src/Audit.NET/Configuration.cs | 253 ++++++++++++++++-- .../ConfigurationApi/Configurator.cs | 9 + .../ConfigurationApi/IConfigurator.cs | 7 + src/Audit.NET/DefaultSystemClock.cs | 6 +- src/Audit.NET/Extensions/TypeExtensions.cs | 8 +- src/Audit.NET/IAuditScope.cs | 77 ++++++ .../IAuditScopeOptionsConfigurator.cs | 16 ++ src/Audit.NET/ISystemClock.cs | 8 +- .../Providers/EventLogDataProvider.cs | 2 +- src/Audit.NET/Providers/FileDataProvider.cs | 6 +- src/Audit.SignalR/AuditHubFilter.cs | 4 +- src/Audit.SignalR/SignalrExtensions.cs | 19 ++ src/Audit.WCF.Client/AuditMessageInspector.cs | 3 +- src/Audit.WCF/AuditOperationInvoker.cs | 4 +- src/Audit.WebApi/AuditApiAdapter.Core.cs | 5 +- src/Audit.WebApi/AuditApiAdapter.cs | 26 +- src/Audit.WebApi/AuditMiddleware.cs | 5 +- .../DbCommandInterceptorTests.cs | 1 + .../MongoAuditEventSubscriberTests.cs | 2 +- test/Audit.UnitTest/Audit.UnitTest.csproj | 2 +- test/Audit.UnitTest/AuditScopeFactoryTests.cs | 133 +++++++++ test/Audit.UnitTest/AuditScopeTests.cs | 62 ++++- test/Audit.UnitTest/ConfigurationTests.cs | 197 +++++++++++--- test/Audit.UnitTest/FileDataProviderTests.cs | 4 +- test/Audit.UnitTest/MyClock.cs | 14 +- test/Audit.UnitTest/UdpProviderUnitTests.cs | 2 +- test/Audit.UnitTest/UnitTest.Async.cs | 10 +- test/Audit.UnitTest/UnitTest.cs | 10 +- test/Audit.WebApi.UnitTest/MiddlewareTests.cs | 25 +- test/Audit.WebApi.UnitTest/TestHelper.cs | 7 +- 49 files changed, 1267 insertions(+), 339 deletions(-) create mode 100644 test/Audit.UnitTest/AuditScopeFactoryTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 032095d4..a514de1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,18 @@ 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.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`. +- Audit.NET: Adding virtual methods `OnConfiguring`/`OnScopeCreated` method to the `AuditScopeFactory` to enable custom configuration of the audit scope. +- Audit.NET: Refactoring the `ISystemClock` interface to use a method instead of a property to get the current time. +- Audit.NET: Enabling Custom Actions to optionally return a boolean value, signaling whether to proceed with or halt the processing of subsequent actions of the same type. +- Audit.NET: Adding `ExcludeEnvironmentInfo` configuration to the `Audit.Core.Configuration` and `AuditScopeOptions` to allow excluding the environment information from the audit event. +- Audit.EntityFramework.Core: Enabling configuration of the `IAuditScopeFactory` and `AuditDataProvider` through dependency injection. +- Audit.WebApi.Core: Enabling configuration of the `IAuditScopeFactory` and `AuditDataProvider` through dependency injection. +- Audit.Mvc.Core: Enabling configuration of the `IAuditScopeFactory` and `AuditDataProvider` through dependency injection. +- Audit.SignalR: Enabling configuration of the `IAuditScopeFactory` and `AuditDataProvider` through dependency injection. + ## [26.0.1] - 2024-08-22: - Audit.WebApi.Core: Adding new SkipResponseBodyContent() configuration to allow deciding whether to skip or include the response body content **after** the action is executed (#690) diff --git a/Directory.Build.props b/Directory.Build.props index bbbc7f21..2b829212 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 26.0.1 + 27.0.0 false diff --git a/README.md b/README.md index 4a454d69..e54b1393 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,74 @@ The `AuditEvent` is typically serialized into a format suitable for storage or t The audit events are stored using a **Data Provider**. You can use one of the [available data providers](https://github.com/thepirat000/Audit.NET?tab=readme-ov-file#storage-providers) or [implement your own](https://github.com/thepirat000/Audit.NET?tab=readme-ov-file#data-providers). -# SUPPORT FOR OLDER .NET FRAMEWORKS +### Audit Scope Factory + +The preferred method for creating audit scopes is by using an `IAuditScopeFactory` instance. +This approach ensures a centralized and consistent configuration for all audit scopes. + +```c# +var factory = new AuditScopeFactory(); +using var scope = factory.Create(new AuditScopeOptions(...)); +... +``` + +If you're using a DI container, you can register the `IAuditScopeFactory` as a service and inject it into your classes. +The default implementation of `IAuditScopeFactory` is provided by the `AuditScopeFactory` class. + +```c# +services.AddSingleton(); +``` + +Then you can inject the IAuditScopeFactory into your classes to create audit scopes: + +```c# +public class MyService +{ + private readonly IAuditScopeFactory _auditScopeFactory; + + public MyService(IAuditScopeFactory auditScopeFactory) + { + _auditScopeFactory = auditScopeFactory; + } + + public void MyMethod() + { + using var scope = _auditScopeFactory.Create(new AuditScopeOptions(...)); + ... + } +} +``` + +You can also implement your own `IAuditScopeFactory` to customize the creation of audit scopes. +The recommended approach is to inherit from the `AuditScopeFactory` class. +By overriding the `OnConfiguring` and `OnScopeCreated` methods, you can configure the audit scope options before creation and customize the audit scope after creation, respectively. + +For example: + +```c# +public class MyAuditScopeFactory : AuditScopeFactory +{ + private readonly IMyService _myService; + public MyAuditScopeFactory(IMyService myService) + { + _myService = myService; + } + + public override void OnConfiguring(AuditScopeOptions options) + { + // Set the data provider to use + options.DataProvider = new SqlDataProvider(...); + } + + public override void OnScopeCreated(AuditScope auditScope) + { + // Add a custom field to the audit scope + auditScope.SetCustomField("CustomId", _myService.GetId()); + } +} +``` + +# Support for older .NET frameworks Beginning with version 23.0.0, this library and its extensions have discontinued support for older .NET Framework and Entity Framework (versions that lost Microsoft support before 2023). @@ -96,46 +163,22 @@ This library and its extensions will maintain support for the following **minimu - .NET Standard 2.0 (netstandard2.0) - .NET 6 (net6.0) -The following frameworks were **deprecated and removed** from the list of target frameworks: - -- net45, net451, net452, net461 -- netstandard1.3, netstandard1.4, netstandard1.5, netstandard1.6 -- netcoreapp2.1, netcoreapp3.0 -- net5.0 - -This discontinuation led to the following modifications: - -- All library versions will now use `System.Text.Json` as the default (Newtonsoft.Json will be deprecated but can still be used through the JsonAdapter). -- Support for EF Core versions 3 and earlier has been discontinued in the `Audit.EntityFramework.Core` libraries. The minimum supported version is now EF Core 5 (`Audit.EntityFramework` will continue to support .NET Entity Framework 6). -- The libraries `Audit.EntityFramework.Core.v3` and `Audit.EntityFramework.Identity.Core.v3` has been deprecated. -- `Audit.NET.JsonSystemAdapter` has been deprecated. - - ## Usage The **Audit Scope** is the central object of this framework. It encapsulates an audit event, controlling its life cycle. The **Audit Event** is an extensible information container of an audited operation. - There are several ways to create an Audit Scope: -- Calling the `Create()` / `CreateAsync()` method of an `AuditScopeFactory` instance, for example: +- Calling the `Create()` / `CreateAsync()` methods of an `AuditScopeFactory` instance. +This is the recommended approach. For example: ```c# - var factory = new AuditScopeFactory(); - var scope = factory.Create(new AuditScopeOptions(...)); + var scope = auditScopeFactory.Create(new AuditScopeOptions(...)); ``` - Using the overloads of the static methods `Create()` / `CreateAsync()` on `AuditScope`, for example: - ```c# - var scope = AuditScope.Create("Order:Update", () => order, new { MyProperty = "value" }); - ``` - - The first parameter of the `AuditScope.Create` method is an _event type name_ intended to identify and group the events. The second is the delegate to obtain the object to track (target object). This object is passed as a `Func` to allow the library to inspect the value at the beginning and the disposal of the scope. It is not mandatory to supply a target object. - - You can use the overload that accepts an `AuditScopeOptions` instance to configure any of the available options for the scope: - ```c# var scope = AuditScope.Create(new AuditScopeOptions() { @@ -167,6 +210,7 @@ IsCreateAndSave | `bool` | Value indicating whether this scope should be immedia AuditEvent | `AuditEvent` | Custom initial audit event to use. By default it will create a new instance of basic `AuditEvent` SkipExtraFrames | `int` | Value used to indicate how many frames in the stack should be skipped to determine the calling method. Default is 0 CallingMethod | `MethodBase` | Specific calling method to store on the event. Default is to use the calling stack to determine the calling method. +Items | `Dictionary` | A dictionary of items that can be used to store additional information on the scope, accessible from the `AuditScope` instance. Suppose you have the following code to _cancel an order_ that you want to audit: @@ -732,7 +776,8 @@ Audit.Core.Configuration.CreationPolicy = EventCreationPolicy.Manual; > If you don't specify a Creation Policy, the default `Insert on End` will be used. ### Custom Actions -You can configure Custom Actions that are executed for all the Audit Scopes in your application. This allows to globally change the behavior and data, intercepting the scopes after they are created or before they are saved. +You can configure Custom Actions to be executed across all Audit Scopes in your application. +This allows you to globally modify behavior and data, intercepting scopes after they are created, before they are saved, or during disposal Call the static `AddCustomAction()` method on `Audit.Core.Configuration` class to attach a custom action. @@ -740,13 +785,20 @@ For example, to globally discard the events under a certain condition: ```c# Audit.Core.Configuration.AddCustomAction(ActionType.OnScopeCreated, scope => { - if (DateTime.Now.Hour == 17) // Tea time + if (DateTime.Now.Hour >= 17) { scope.Discard(); + return false; } + return true; }); ``` +> **Note** +> +> The custom actions can return a boolean value to indicate if subsequent actions of the same type should be executed. + + Or to add custom fields/comments globally to all scopes: ```c# Audit.Core.Configuration.AddCustomAction(ActionType.OnEventSaving, scope => diff --git a/src/Audit.EntityFramework/DbContextHelper.cs b/src/Audit.EntityFramework/DbContextHelper.cs index 1cdfd5f0..362c746f 100644 --- a/src/Audit.EntityFramework/DbContextHelper.cs +++ b/src/Audit.EntityFramework/DbContextHelper.cs @@ -1,6 +1,8 @@ #if EF_CORE using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; #else using System.Data.Entity; using System.Data.Entity.Infrastructure; @@ -310,12 +312,15 @@ public IAuditScope CreateAuditScope(IAuditDbContext context, EntityFrameworkEven { auditEfEvent.CustomFields = new Dictionary(context.ExtraFields); } - var factory = context.AuditScopeFactory ?? Core.Configuration.AuditScopeFactory; + + var factory = GetAuditScopeFactory(context.DbContext); + var dataProvider = GetDataProvider(context.DbContext); + var options = new AuditScopeOptions() { EventType = eventType, CreationPolicy = EventCreationPolicy.Manual, - DataProvider = context.AuditDataProvider, + DataProvider = dataProvider, AuditEvent = auditEfEvent, #if NETSTANDARD2_1_OR_GREATER || NET6_0_OR_GREATER SkipExtraFrames = 5 @@ -345,12 +350,15 @@ public async Task CreateAuditScopeAsync(IAuditDbContext context, En { auditEfEvent.CustomFields = new Dictionary(context.ExtraFields); } - var factory = context.AuditScopeFactory ?? Core.Configuration.AuditScopeFactory; + + var factory = GetAuditScopeFactory(context.DbContext); + var dataProvider = GetDataProvider(context.DbContext); + var options = new AuditScopeOptions() { EventType = eventType, CreationPolicy = EventCreationPolicy.Manual, - DataProvider = context.AuditDataProvider, + DataProvider = dataProvider, AuditEvent = auditEfEvent, SkipExtraFrames = 3 }; @@ -359,6 +367,40 @@ public async Task CreateAuditScopeAsync(IAuditDbContext context, En return scope; } + internal IAuditScopeFactory GetAuditScopeFactory(DbContext dbContext) + { + var auditDbContext = dbContext as IAuditDbContext; +#if EF_CORE + return auditDbContext?.AuditScopeFactory ?? TryGetService(dbContext) ?? Core.Configuration.AuditScopeFactory; +#else + return auditDbContext?.AuditScopeFactory ?? Core.Configuration.AuditScopeFactory; +#endif + } + + internal AuditDataProvider GetDataProvider(DbContext dbContext) + { + var auditDbContext = dbContext as IAuditDbContext; +#if EF_CORE + return auditDbContext?.AuditDataProvider ?? TryGetService(dbContext); +#else + return auditDbContext?.AuditDataProvider; +#endif + } + +#if EF_CORE + private T TryGetService(DbContext dbContext) where T : class + { + var infrastructure = dbContext?.GetInfrastructure(); + + var service = + infrastructure?.GetService(typeof(T)) ?? + infrastructure?.GetService()?.Extensions.OfType() + .FirstOrDefault()?.ApplicationServiceProvider?.GetService(typeof(T)); + + return service as T; + } +#endif + /// /// Gets the modified entries to process. /// diff --git a/src/Audit.EntityFramework/Interceptors/AuditCommandInterceptor.cs b/src/Audit.EntityFramework/Interceptors/AuditCommandInterceptor.cs index bfc7d692..bc10bfc0 100644 --- a/src/Audit.EntityFramework/Interceptors/AuditCommandInterceptor.cs +++ b/src/Audit.EntityFramework/Interceptors/AuditCommandInterceptor.cs @@ -9,6 +9,8 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Internal; namespace Audit.EntityFramework.Interceptors { @@ -363,13 +365,15 @@ private IAuditScope CreateAuditScope(AuditEventCommandEntityFramework cmdEvent) cmdEvent.CustomFields = new Dictionary(context.ExtraFields); } - var factory = context?.AuditScopeFactory ?? Core.Configuration.AuditScopeFactory; + var factory = _dbContextHelper.GetAuditScopeFactory(cmdEvent.CommandEvent.DbContext); + var dataProvider = _dbContextHelper.GetDataProvider(cmdEvent.CommandEvent.DbContext); + var options = new AuditScopeOptions() { EventType = eventType, AuditEvent = cmdEvent, SkipExtraFrames = 3, - DataProvider = context?.AuditDataProvider + DataProvider = dataProvider }; var scope = factory.Create(options); @@ -396,14 +400,16 @@ private async Task CreateAuditScopeAsync(AuditEventCommandEntityFra { cmdEvent.CustomFields = new Dictionary(context.ExtraFields); } + + var factory = _dbContextHelper.GetAuditScopeFactory(cmdEvent.CommandEvent.DbContext); + var dataProvider = _dbContextHelper.GetDataProvider(cmdEvent.CommandEvent.DbContext); - var factory = context?.AuditScopeFactory ?? Core.Configuration.AuditScopeFactory; var options = new AuditScopeOptions() { EventType = eventType, AuditEvent = cmdEvent, SkipExtraFrames = 3, - DataProvider = context?.AuditDataProvider + DataProvider = dataProvider }; var scope = await factory.CreateAsync(options, cancellationToken); diff --git a/src/Audit.EntityFramework/Interceptors/AuditTransactionInterceptor.cs b/src/Audit.EntityFramework/Interceptors/AuditTransactionInterceptor.cs index 31d48481..5bfbbfa3 100644 --- a/src/Audit.EntityFramework/Interceptors/AuditTransactionInterceptor.cs +++ b/src/Audit.EntityFramework/Interceptors/AuditTransactionInterceptor.cs @@ -6,6 +6,7 @@ using System.Data.Common; using System.Threading; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Infrastructure; namespace Audit.EntityFramework.Interceptors { @@ -310,13 +311,15 @@ private IAuditScope CreateAuditScope(AuditEventTransactionEntityFramework tranEv tranEvent.CustomFields = new Dictionary(context.ExtraFields); } - var factory = context?.AuditScopeFactory ?? Core.Configuration.AuditScopeFactory; + var factory = _dbContextHelper.GetAuditScopeFactory(tranEvent.TransactionEvent.DbContext); + var dataProvider = _dbContextHelper.GetDataProvider(tranEvent.TransactionEvent.DbContext); + var options = new AuditScopeOptions() { EventType = eventType, AuditEvent = tranEvent, SkipExtraFrames = 3, - DataProvider = context?.AuditDataProvider + DataProvider = dataProvider }; var scope = factory.Create(options); @@ -339,13 +342,15 @@ private async Task CreateAuditScopeAsync(AuditEventTransactionEntit tranEvent.CustomFields = new Dictionary(context.ExtraFields); } - var factory = context?.AuditScopeFactory ?? Core.Configuration.AuditScopeFactory; + var factory = _dbContextHelper.GetAuditScopeFactory(tranEvent.TransactionEvent.DbContext); + var dataProvider = _dbContextHelper.GetDataProvider(tranEvent.TransactionEvent.DbContext); + var options = new AuditScopeOptions() { EventType = eventType, AuditEvent = tranEvent, SkipExtraFrames = 3, - DataProvider = context?.AuditDataProvider + DataProvider = dataProvider }; var scope = await factory.CreateAsync(options, cancellationToken); diff --git a/src/Audit.EntityFramework/README.md b/src/Audit.EntityFramework/README.md index ad6515b2..401d1843 100644 --- a/src/Audit.EntityFramework/README.md +++ b/src/Audit.EntityFramework/README.md @@ -163,7 +163,7 @@ builder.Services.AddDbContext(c => c ### Low-Level Command Interception -A low-level command interceptor is also provided for EF Core ≥ 3.0. +A low-level command interceptor is also provided for Entity Framework Core. In order to audit low-level operations like *reads*, *stored procedure calls* and *non-query commands*, you can attach the provided `AuditCommandInterceptor` to your `DbContext` configuration. @@ -211,9 +211,51 @@ builder.Services.AddDbContext(c => c ### Output -The EF audit events are stored using a _Data Provider_. -You can use one of the [available data providers](https://github.com/thepirat000/Audit.NET#data-providers-included) or implement your own. -This can be set per `DbContext` instance or globally. If you plan to store the audit logs with EF, you can use the [Entity Framework Data Provider](#entity-framework-data-provider). +EF audit events are stored via a _Data Provider_. You can either use one of the [available data providers](https://github.com/thepirat000/Audit.NET#data-providers-included) or implement your own. + +The Audit Data Provider can be configured in several ways: + +- Per `DbContext` instance by explicitly setting the `AuditDataProvider` property. + For example: + ```c# + public class MyContext : AuditDbContext + { + public MyContext() + { + AuditDataProvider = new SqlDataProvider(config => config...); + } + } + ``` + +- By registering an `AuditDataProvider` instance in the dependency injection container. + + For example: + ```c# + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddSingleton(new SqlDataProvider(config => config...)); + } + } + ``` + +- Globally, by setting the `AuditDataProvider` instance through the `Audit.Core.Configuration.DataProvider` static property or the `Audit.Core.Configuration.Use()` methods. + + For example: + ```c# + public class Program + { + public static void Main(string[] args) + { + Audit.Core.Configuration.Setup().UseSqlServer(config => config...); + } + } + ``` + +If you intend to store audit logs with EF, consider using the [Entity Framework Data Provider](#entity-framework-data-provider). ### Settings (low-Level interceptor) The low-level command interceptor can be configured by setting the `AuditCommandInterceptor` properties, for example: diff --git a/src/Audit.Mvc/AuditAttribute.Core.cs b/src/Audit.Mvc/AuditAttribute.Core.cs index b388f095..2b48860c 100644 --- a/src/Audit.Mvc/AuditAttribute.Core.cs +++ b/src/Audit.Mvc/AuditAttribute.Core.cs @@ -15,6 +15,7 @@ using System.Text; using Microsoft.AspNetCore.Mvc.Abstractions; using System.Threading; +using Microsoft.Extensions.DependencyInjection; namespace Audit.Mvc { @@ -100,13 +101,17 @@ private async Task BeforeExecutingAsync(ActionExecutingContext filterContext) { Action = auditAction }; + var scopeFactory = httpContext.RequestServices?.GetService() ?? Core.Configuration.AuditScopeFactory; + var dataProvider = httpContext.RequestServices?.GetService(); + var auditScopeOptions = new AuditScopeOptions() { EventType = eventType, AuditEvent = auditEventAction, - CallingMethod = actionDescriptor?.MethodInfo + CallingMethod = actionDescriptor?.MethodInfo, + DataProvider = dataProvider }; - var auditScope = await AuditScope.CreateAsync(auditScopeOptions, requestCancellationToken); + var auditScope = await scopeFactory.CreateAsync(auditScopeOptions, requestCancellationToken); httpContext.Items[AuditActionKey] = auditAction; httpContext.Items[AuditScopeKey] = auditScope; } diff --git a/src/Audit.Mvc/AuditAttribute.cs b/src/Audit.Mvc/AuditAttribute.cs index 16ab24d9..a30e0173 100644 --- a/src/Audit.Mvc/AuditAttribute.cs +++ b/src/Audit.Mvc/AuditAttribute.cs @@ -93,7 +93,7 @@ public override void OnActionExecuting(ActionExecutingContext filterContext) AuditEvent = auditEventAction, CallingMethod = (filterContext.ActionDescriptor as ReflectedActionDescriptor)?.MethodInfo }; - var auditScope = AuditScope.Create(options); + var auditScope = Configuration.AuditScopeFactory.Create(options); filterContext.HttpContext.Items[AuditActionKey] = auditAction; filterContext.HttpContext.Items[AuditScopeKey] = auditScope; base.OnActionExecuting(filterContext); diff --git a/src/Audit.Mvc/AuditPageFilter.cs b/src/Audit.Mvc/AuditPageFilter.cs index c42d739d..7d855654 100644 --- a/src/Audit.Mvc/AuditPageFilter.cs +++ b/src/Audit.Mvc/AuditPageFilter.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Primitives; using System; using System.Collections.Generic; @@ -95,7 +96,7 @@ public virtual async Task BeforeExecutingAsync(PageHandlerExecutingContext conte { UserName = httpContext.User?.Identity?.Name, IpAddress = httpContext.Connection?.RemoteIpAddress?.ToString(), - RequestUrl = string.Format("{0}://{1}{2}{3}", request.Scheme, request.Host, request.Path, request.QueryString), + RequestUrl = $"{request.Scheme}://{request.Host}{request.Path}{request.QueryString}", HttpMethod = request.Method, FormVariables = request.HasFormContentType ? AuditHelper.ToDictionary(request.Form) : null, Headers = IncludeHeaders ? AuditHelper.ToDictionary(request.Headers) : null, @@ -124,13 +125,21 @@ public virtual async Task BeforeExecutingAsync(PageHandlerExecutingContext conte { Action = auditAction }; + + var scopeFactory = httpContext?.RequestServices?.GetService() ?? Core.Configuration.AuditScopeFactory; + var dataProvider = httpContext?.RequestServices?.GetService(); + var auditScopeOptions = new AuditScopeOptions() { - EventType = eventType, - AuditEvent = auditEventAction, - CallingMethod = context.HandlerMethod?.MethodInfo + EventType = eventType, + AuditEvent = auditEventAction, + CallingMethod = context.HandlerMethod?.MethodInfo, + DataProvider = dataProvider }; - var auditScope = await AuditScope.CreateAsync(auditScopeOptions, requestCancellationToken); + + var auditScope = await scopeFactory.CreateAsync(auditScopeOptions, requestCancellationToken); + + httpContext.Items[AuditActionKey] = auditAction; httpContext.Items[AuditScopeKey] = auditScope; } diff --git a/src/Audit.NET.NLog/Providers/NLogDataProvider.cs b/src/Audit.NET.NLog/Providers/NLogDataProvider.cs index a1a79895..6bbda656 100644 --- a/src/Audit.NET.NLog/Providers/NLogDataProvider.cs +++ b/src/Audit.NET.NLog/Providers/NLogDataProvider.cs @@ -51,7 +51,7 @@ private ILogger GetLogger(AuditEvent auditEvent) private LogLevel GetLogLevel(AuditEvent auditEvent) { - return LogLevel.GetValue(auditEvent) ?? (auditEvent.Environment.Exception != null ? NLog.LogLevel.Error : NLog.LogLevel.Info); + return LogLevel.GetValue(auditEvent) ?? (auditEvent.Environment?.Exception != null ? NLog.LogLevel.Error : NLog.LogLevel.Info); } private object GetLogObject(AuditEvent auditEvent, object eventId) diff --git a/src/Audit.NET.Serilog/Providers/SerilogDataProvider.cs b/src/Audit.NET.Serilog/Providers/SerilogDataProvider.cs index d7230bbc..1751e250 100644 --- a/src/Audit.NET.Serilog/Providers/SerilogDataProvider.cs +++ b/src/Audit.NET.Serilog/Providers/SerilogDataProvider.cs @@ -88,7 +88,7 @@ private ILogger GetLogger(AuditEvent auditEvent) private LogLevel GetLogLevel(AuditEvent auditEvent) { return this.LogLevel.GetValue(auditEvent) ?? - (auditEvent.Environment.Exception != null ? Serilog.LogLevel.Error : Serilog.LogLevel.Info); + (auditEvent.Environment?.Exception != null ? Serilog.LogLevel.Error : Serilog.LogLevel.Info); } private object GetLogObject(AuditEvent auditEvent, object eventId) diff --git a/src/Audit.NET.log4net/Providers/Log4netDataProvider.cs b/src/Audit.NET.log4net/Providers/Log4netDataProvider.cs index 7cf02246..65b0ba73 100644 --- a/src/Audit.NET.log4net/Providers/Log4netDataProvider.cs +++ b/src/Audit.NET.log4net/Providers/Log4netDataProvider.cs @@ -51,7 +51,7 @@ private ILog GetLogger(AuditEvent auditEvent) private LogLevel GetLogLevel(AuditEvent auditEvent) { - return LogLevel.GetValue(auditEvent) ?? (auditEvent.Environment.Exception != null ? log4net.LogLevel.Error : log4net.LogLevel.Info); + return LogLevel.GetValue(auditEvent) ?? (auditEvent.Environment?.Exception != null ? log4net.LogLevel.Error : log4net.LogLevel.Info); } private object GetLogObject(AuditEvent auditEvent, object eventId) diff --git a/src/Audit.NET/Audit.NET.csproj b/src/Audit.NET/Audit.NET.csproj index c57d2786..13fe213a 100644 --- a/src/Audit.NET/Audit.NET.csproj +++ b/src/Audit.NET/Audit.NET.csproj @@ -2,7 +2,7 @@ An extensible framework to audit executing operations in .NET and .NET Core. - 10 + latest Copyright 2016 Audit.NET Federico Colombo diff --git a/src/Audit.NET/AuditScope.Helpers.cs b/src/Audit.NET/AuditScope.Helpers.cs index e03fdac7..c7bdb663 100644 --- a/src/Audit.NET/AuditScope.Helpers.cs +++ b/src/Audit.NET/AuditScope.Helpers.cs @@ -49,7 +49,7 @@ public static async Task CreateAsync(ActionAn anonymous object that contains additional fields to be merged into the audit event. public static AuditScope Create(string eventType, Func target, object extraFields = null) { - var options = new AuditScopeOptions(eventType: eventType, targetGetter: target, extraFields: extraFields); + var options = new AuditScopeOptions() { EventType = eventType, TargetGetter = target, ExtraFields = extraFields }; return new AuditScope(options).Start(); } /// @@ -61,7 +61,7 @@ public static AuditScope Create(string eventType, Func target, object ex /// The Cancellation Token. public static async Task CreateAsync(string eventType, Func target, object extraFields = null, CancellationToken cancellationToken = default) { - var options = new AuditScopeOptions(eventType: eventType, targetGetter: target, extraFields: extraFields); + var options = new AuditScopeOptions() { EventType = eventType, TargetGetter = target, ExtraFields = extraFields }; return await new AuditScope(options).StartAsync(cancellationToken); } diff --git a/src/Audit.NET/AuditScope.cs b/src/Audit.NET/AuditScope.cs index c939f2b3..f0d22b28 100644 --- a/src/Audit.NET/AuditScope.cs +++ b/src/Audit.NET/AuditScope.cs @@ -24,18 +24,19 @@ internal AuditScope(AuditScopeOptions options) _options = options; _creationPolicy = options.CreationPolicy ?? Configuration.CreationPolicy; _dataProvider = options.DataProvider ?? Configuration.DataProvider; + _systemClock = options.SystemClock ?? Configuration.SystemClock; _targetGetter = options.TargetGetter; - + _items = options.Items ?? new Dictionary(); _event = options.AuditEvent ?? new AuditEvent(); _event.SetScope(this); - _event.StartDate = Configuration.SystemClock.UtcNow; + _event.StartDate = _systemClock.GetCurrentDateTime(); _event.Environment = GetEnvironmentInfo(options); #if NET6_0_OR_GREATER - if (options.StartActivityTrace) + if (options.StartActivityTrace ?? Configuration.StartActivityTrace) { var activitySource = new ActivitySource(nameof(AuditScope), typeof(AuditScope).Assembly.GetName().Version!.ToString()); _activity = activitySource.StartActivity(_event.GetType().Name, ActivityKind.Internal, null, tags: new Dictionary @@ -43,7 +44,7 @@ internal AuditScope(AuditScopeOptions options) { nameof(AuditEvent), _event } }); } - if (options.IncludeActivityTrace) + if (options.IncludeActivityTrace ?? Configuration.IncludeActivityTrace) { _event.Activity = GetActivityTrace(); } @@ -70,43 +71,36 @@ internal AuditScope(AuditScopeOptions options) }; } } -#endregion + + #endregion #region Public Properties - /// - /// The current save mode. Useful on custom actions to determine the saving trigger. - /// + + /// public SaveMode SaveMode => _saveMode; - /// - /// Indicates the change type - /// + /// public string EventType { get => _event.EventType; set => _event.EventType = value; } - /// - /// Gets the event related to this scope. - /// + /// public AuditEvent Event => _event; - /// - /// Gets the data provider for this AuditScope instance. - /// + /// public AuditDataProvider DataProvider => _dataProvider; - /// - /// Gets the current event ID, or NULL if not yet created. - /// + /// public object EventId => _eventId; - /// - /// Gets the creation policy for this scope. - /// + /// public EventCreationPolicy EventCreationPolicy => _creationPolicy; + /// + public IDictionary Items => _items; + #endregion #region Private fields @@ -118,17 +112,17 @@ public string EventType private bool _disposed; private bool _ended; private readonly AuditDataProvider _dataProvider; + private readonly ISystemClock _systemClock; private Func _targetGetter; + private readonly IDictionary _items; #if NET6_0_OR_GREATER private readonly Activity _activity; #endif -#endregion + #endregion #region Public Methods - /// - /// Replaces the target object getter whose old/new value will be stored on the AuditEvent.Target property - /// - /// A function that returns the target + + /// public void SetTargetGetter(Func targetGetter) { _targetGetter = targetGetter; @@ -146,17 +140,13 @@ public void SetTargetGetter(Func targetGetter) _event.Target = null; } } - /// - /// Add a textual comment to the event - /// + /// public void Comment(string text) { Comment(text, Array.Empty()); } - /// - /// Add a textual comment to the event - /// + /// public void Comment(string format, params object[] args) { if (_event.Comments == null) @@ -166,18 +156,26 @@ public void Comment(string format, params object[] args) _event.Comments.Add(string.Format(format, args)); } - /// - /// Adds a custom field to the event - /// - /// The type of the value. - /// Name of the field. - /// The value object. - /// if set to true the value will be serialized immediately. + /// public void SetCustomField(string fieldName, TC value, bool serialize = false) { _event.CustomFields[fieldName] = serialize ? _dataProvider.CloneValue(value, _event) : value; } + /// + public T GetItem(string key) + { + if (_items.TryGetValue(key, out var value)) + { + if (value is T obj) + { + return obj; + } + } + + return default; + } + /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// @@ -193,7 +191,7 @@ public void Dispose() _activity?.Dispose(); _activity?.Source?.Dispose(); #endif - Configuration.InvokeScopeCustomActions(ActionType.OnScopeDisposed, this); + Configuration.InvokeCustomActions(ActionType.OnScopeDisposed, this); } /// @@ -212,12 +210,10 @@ public async ValueTask DisposeAsync() _activity?.Dispose(); _activity?.Source?.Dispose(); #endif - await Configuration.InvokeScopeCustomActionsAsync(ActionType.OnScopeDisposed, this, CancellationToken.None); + await Configuration.InvokeCustomActionsAsync(ActionType.OnScopeDisposed, this, CancellationToken.None); } - /// - /// Discards this audit scope, so the event will not be written. - /// + /// public void Discard() { // Mark as saved to ignore the saving @@ -225,7 +221,7 @@ public void Discard() } /// - /// Saves the event. + /// Ends the event. /// private void End() { @@ -247,7 +243,7 @@ private void End() } /// - /// Saves the event. + /// Ends the event. /// private async Task EndAsync() { @@ -268,10 +264,7 @@ private async Task EndAsync() _ended = true; } - /// - /// Manually Saves (insert/replace) the Event. - /// Use this method to save (insert/replace) the event when CreationPolicy is set to Manual. - /// + /// public void Save() { if (IsEndedOrDisabled()) @@ -282,11 +275,7 @@ public void Save() SaveEvent(); } - /// - /// Manually Saves (insert/replace) the Event asynchronously. - /// Use this method to save (insert/replace) the event when CreationPolicy is set to Manual. - /// - /// The Cancellation Token. + /// public async Task SaveAsync(CancellationToken cancellationToken = default) { if (IsEndedOrDisabled()) @@ -297,10 +286,7 @@ public async Task SaveAsync(CancellationToken cancellationToken = default) await SaveEventAsync(false, cancellationToken); } - /// - /// Gets the event related to this scope of a known AuditEvent derived type. Returns null if the event is not of the specified type. - /// - /// The AuditEvent derived type + /// public T EventAs() where T : AuditEvent { return _event as T; @@ -375,6 +361,10 @@ private AuditActivityTrace GetActivityTrace() [MethodImpl(MethodImplOptions.NoInlining)] private AuditEventEnvironment GetEnvironmentInfo(AuditScopeOptions options) { + if (options.ExcludeEnvironmentInfo ?? Configuration.ExcludeEnvironmentInfo) + { + return null; + } var environment = new AuditEventEnvironment() { Culture = System.Globalization.CultureInfo.CurrentCulture.ToString(), @@ -387,7 +377,7 @@ private AuditEventEnvironment GetEnvironmentInfo(AuditScopeOptions options) { callingMethod = new StackFrame(3 + options.SkipExtraFrames).GetMethod(); } - if (options.IncludeStackTrace) + if (options.IncludeStackTrace ?? Configuration.IncludeStackTrace) { environment.StackTrace = new StackTrace(options.SkipExtraFrames, true).ToString(); } @@ -408,7 +398,7 @@ internal AuditScope Start() { _saveMode = SaveMode.InsertOnStart; // Execute custom on scope created actions - Configuration.InvokeScopeCustomActions(ActionType.OnScopeCreated, this); + Configuration.InvokeCustomActions(ActionType.OnScopeCreated, this); // Process the event insertion (if applies) if (_options.IsCreateAndSave) @@ -442,7 +432,7 @@ internal async Task StartAsync(CancellationToken cancellationToken = { _saveMode = SaveMode.InsertOnStart; // Execute custom on scope created actions - await Configuration.InvokeScopeCustomActionsAsync(ActionType.OnScopeCreated, this, cancellationToken); + await Configuration.InvokeCustomActionsAsync(ActionType.OnScopeCreated, this, cancellationToken); // Process the event insertion (if applies) if (_options.IsCreateAndSave) @@ -466,6 +456,7 @@ internal async Task StartAsync(CancellationToken cancellationToken = } return this; } + private bool IsEndedOrDisabled() { if (!_ended && Configuration.AuditDisabled) @@ -474,12 +465,16 @@ private bool IsEndedOrDisabled() } return _ended; } + // Update event info prior to save private void EndEvent() { - var exception = GetCurrentException(); - _event.Environment.Exception = exception != null ? $"{exception.GetType().Name}: {exception.Message}" : null; - _event.EndDate = Configuration.SystemClock.UtcNow; + if (_event.Environment != null) + { + var exception = GetCurrentException(); + _event.Environment.Exception = exception != null ? $"{exception.GetType().Name}: {exception.Message}" : null; + } + _event.EndDate = _systemClock.GetCurrentDateTime(); _event.Duration = Convert.ToInt32((_event.EndDate.Value - _event.StartDate).TotalMilliseconds); if (_targetGetter != null) { @@ -523,7 +518,7 @@ private void SaveEvent(bool forceInsert = false) return; } // Execute custom on event saving actions - Configuration.InvokeScopeCustomActions(ActionType.OnEventSaving, this); + Configuration.InvokeCustomActions(ActionType.OnEventSaving, this); if (IsEndedOrDisabled()) { return; @@ -537,7 +532,7 @@ private void SaveEvent(bool forceInsert = false) _eventId = _dataProvider.InsertEvent(_event); } // Execute custom after saving actions - Configuration.InvokeScopeCustomActions(ActionType.OnEventSaved, this); + Configuration.InvokeCustomActions(ActionType.OnEventSaved, this); } private async Task SaveEventAsync(bool forceInsert = false, CancellationToken cancellationToken = default) @@ -547,7 +542,7 @@ private async Task SaveEventAsync(bool forceInsert = false, CancellationToken ca return; } // Execute custom on event saving actions - await Configuration.InvokeScopeCustomActionsAsync(ActionType.OnEventSaving, this, cancellationToken); + await Configuration.InvokeCustomActionsAsync(ActionType.OnEventSaving, this, cancellationToken); if (IsEndedOrDisabled()) { return; @@ -561,7 +556,7 @@ private async Task SaveEventAsync(bool forceInsert = false, CancellationToken ca _eventId = await _dataProvider.InsertEventAsync(_event, cancellationToken); } // Execute custom after saving actions - await Configuration.InvokeScopeCustomActionsAsync(ActionType.OnEventSaved, this, cancellationToken); + await Configuration.InvokeCustomActionsAsync(ActionType.OnEventSaved, this, cancellationToken); } #endregion } diff --git a/src/Audit.NET/AuditScopeFactory.cs b/src/Audit.NET/AuditScopeFactory.cs index 53dc415e..84a1b2d5 100644 --- a/src/Audit.NET/AuditScopeFactory.cs +++ b/src/Audit.NET/AuditScopeFactory.cs @@ -1,5 +1,4 @@ -using Audit.Core.Providers; -using System; +using System; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -7,24 +6,49 @@ namespace Audit.Core { /// - /// A factory of scopes. + /// The default factory of audit scopes. /// public class AuditScopeFactory : IAuditScopeFactory { - #region IAuditScopeFactory implementation + /// + /// Override this method to customize the configuration of audit scopes prior to their creation. + /// + /// The audit scope options. + public virtual void OnConfiguring(AuditScopeOptions options) + { + } + + /// + /// Override this method to implement custom logic for the audit scope after its creation. + /// This is executed before the global OnScopeCreated custom actions + /// + /// The audit scope created. + public virtual void OnScopeCreated(AuditScope auditScope) + { + } + + #region IAuditScopeFactory + /// [MethodImpl(MethodImplOptions.NoInlining)] public IAuditScope Create(AuditScopeOptions options) { - return new AuditScope(options).Start(); + OnConfiguring(options); + var auditScope = new AuditScope(options); + OnScopeCreated(auditScope); + return auditScope.Start(); } /// [MethodImpl(MethodImplOptions.NoInlining)] public async Task CreateAsync(AuditScopeOptions options, CancellationToken cancellationToken = default) { - return await new AuditScope(options).StartAsync(cancellationToken); + OnConfiguring(options); + var auditScope = new AuditScope(options); + OnScopeCreated(auditScope); + return await auditScope.StartAsync(cancellationToken); } + #endregion /// @@ -35,7 +59,10 @@ public async Task CreateAsync(AuditScopeOptions options, Cancellati public IAuditScope Create(Action config) { var options = new AuditScopeOptions(config); - return new AuditScope(options).Start(); + OnConfiguring(options); + var auditScope = new AuditScope(options); + OnScopeCreated(auditScope); + return auditScope.Start(); } /// @@ -47,35 +74,10 @@ public IAuditScope Create(Action config) public async Task CreateAsync(Action config, CancellationToken cancellationToken = default) { var options = new AuditScopeOptions(config); - return await new AuditScope(options).StartAsync(cancellationToken); - } - - /// - /// Creates an audit scope with the given extra fields, and saves it right away using the globally configured data provider. Shortcut for CreateAndSave(). - /// - /// Type of the event. - /// An anonymous object that can contain additional fields to be merged into the audit event. - [MethodImpl(MethodImplOptions.NoInlining)] - public void Log(string eventType, object extraFields) - { - using (var scope = new AuditScope(new AuditScopeOptions(eventType, null, extraFields, null, null, true))) - { - scope.Start(); - } - } - /// - /// Creates an audit scope with the given extra fields, and saves it right away using the globally configured data provider. Shortcut for CreateAndSaveAsync(). - /// - /// Type of the event. - /// An anonymous object that can contain additional fields to be merged into the audit event. - /// The Cancellation Token. - [MethodImpl(MethodImplOptions.NoInlining)] - public async Task LogAsync(string eventType, object extraFields, CancellationToken cancellationToken = default) - { - using (var scope = new AuditScope(new AuditScopeOptions(eventType, null, extraFields, null, null, true))) - { - await scope.StartAsync(cancellationToken); - } + OnConfiguring(options); + var auditScope = new AuditScope(options); + OnScopeCreated(auditScope); + return await auditScope.StartAsync(cancellationToken); } /// @@ -86,8 +88,13 @@ public async Task LogAsync(string eventType, object extraFields, CancellationTok [MethodImpl(MethodImplOptions.NoInlining)] public IAuditScope Create(string eventType, Func target) { - return new AuditScope(new AuditScopeOptions(eventType, target, null, null, null)).Start(); + var options = new AuditScopeOptions() { EventType = eventType, TargetGetter = target }; + OnConfiguring(options); + var auditScope = new AuditScope(options); + OnScopeCreated(auditScope); + return auditScope.Start(); } + /// /// Creates an audit scope for a target object and an event type. /// @@ -97,7 +104,11 @@ public IAuditScope Create(string eventType, Func target) [MethodImpl(MethodImplOptions.NoInlining)] public async Task CreateAsync(string eventType, Func target, CancellationToken cancellationToken = default) { - return await new AuditScope(new AuditScopeOptions(eventType, target, null, null, null)).StartAsync(cancellationToken); + var options = new AuditScopeOptions() { EventType = eventType, TargetGetter = target }; + OnConfiguring(options); + var auditScope = new AuditScope(options); + OnScopeCreated(auditScope); + return await auditScope.StartAsync(cancellationToken); } /// @@ -110,8 +121,13 @@ public async Task CreateAsync(string eventType, Func target [MethodImpl(MethodImplOptions.NoInlining)] public IAuditScope Create(string eventType, Func target, EventCreationPolicy creationPolicy, AuditDataProvider dataProvider) { - return new AuditScope(new AuditScopeOptions(eventType, target, null, dataProvider, creationPolicy)).Start(); + var options = new AuditScopeOptions() { EventType = eventType, TargetGetter = target, DataProvider = dataProvider, CreationPolicy = creationPolicy }; + OnConfiguring(options); + var auditScope = new AuditScope(options); + OnScopeCreated(auditScope); + return auditScope.Start(); } + /// /// Creates an audit scope for a target object and an event type, providing the creation policy and optionally the Data Provider. /// @@ -123,7 +139,11 @@ public IAuditScope Create(string eventType, Func target, EventCreationPo [MethodImpl(MethodImplOptions.NoInlining)] public async Task CreateAsync(string eventType, Func target, EventCreationPolicy? creationPolicy, AuditDataProvider dataProvider, CancellationToken cancellationToken = default) { - return await new AuditScope(new AuditScopeOptions(eventType, target, null, dataProvider, creationPolicy)).StartAsync(cancellationToken); + var options = new AuditScopeOptions() { EventType = eventType, TargetGetter = target, DataProvider = dataProvider, CreationPolicy = creationPolicy }; + OnConfiguring(options); + var auditScope = new AuditScope(options); + OnScopeCreated(auditScope); + return await auditScope.StartAsync(cancellationToken); } /// @@ -137,7 +157,11 @@ public async Task CreateAsync(string eventType, Func target [MethodImpl(MethodImplOptions.NoInlining)] public IAuditScope Create(string eventType, Func target, object extraFields, EventCreationPolicy? creationPolicy, AuditDataProvider dataProvider) { - return new AuditScope(new AuditScopeOptions(eventType, target, extraFields, dataProvider, creationPolicy)).Start(); + var options = new AuditScopeOptions() { EventType = eventType, TargetGetter = target, ExtraFields = extraFields, DataProvider = dataProvider, CreationPolicy = creationPolicy }; + OnConfiguring(options); + var auditScope = new AuditScope(options); + OnScopeCreated(auditScope); + return auditScope.Start(); } /// /// Creates an audit scope from a reference value, and an event type. @@ -151,8 +175,42 @@ public IAuditScope Create(string eventType, Func target, object extraFie [MethodImpl(MethodImplOptions.NoInlining)] public async Task CreateAsync(string eventType, Func target, object extraFields, EventCreationPolicy? creationPolicy, AuditDataProvider dataProvider, CancellationToken cancellationToken = default) { - return await new AuditScope(new AuditScopeOptions(eventType, target, extraFields, dataProvider, creationPolicy)).StartAsync(cancellationToken); + var options = new AuditScopeOptions() { EventType = eventType, TargetGetter = target, ExtraFields = extraFields, DataProvider = dataProvider, CreationPolicy = creationPolicy }; + OnConfiguring(options); + var auditScope = new AuditScope(options); + OnScopeCreated(auditScope); + return await auditScope.StartAsync(cancellationToken); } + /// + /// Creates an audit scope with the given extra fields, and saves it right away using the globally configured data provider. Shortcut for CreateAndSave(). + /// + /// Type of the event. + /// An anonymous object that can contain additional fields to be merged into the audit event. + [MethodImpl(MethodImplOptions.NoInlining)] + public void Log(string eventType, object extraFields) + { + var options = new AuditScopeOptions() { EventType = eventType, ExtraFields = extraFields, IsCreateAndSave = true }; + OnConfiguring(options); + using var auditScope = new AuditScope(options); + OnScopeCreated(auditScope); + auditScope.Start(); + } + + /// + /// Creates an audit scope with the given extra fields, and saves it right away using the globally configured data provider. Shortcut for CreateAndSaveAsync(). + /// + /// Type of the event. + /// An anonymous object that can contain additional fields to be merged into the audit event. + /// The Cancellation Token. + [MethodImpl(MethodImplOptions.NoInlining)] + public async Task LogAsync(string eventType, object extraFields, CancellationToken cancellationToken = default) + { + var options = new AuditScopeOptions() { EventType = eventType, ExtraFields = extraFields, IsCreateAndSave = true }; + OnConfiguring(options); + await using var auditScope = new AuditScope(options); + OnScopeCreated(auditScope); + await auditScope.StartAsync(cancellationToken); + } } } diff --git a/src/Audit.NET/AuditScopeOptions.cs b/src/Audit.NET/AuditScopeOptions.cs index 7884e6a8..17de0660 100644 --- a/src/Audit.NET/AuditScopeOptions.cs +++ b/src/Audit.NET/AuditScopeOptions.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Reflection; + using Audit.Core.Providers.Wrappers; namespace Audit.Core @@ -38,7 +40,7 @@ public Func DataProviderFactory public AuditDataProvider DataProvider { get; set; } /// - /// Gets or sets the event creation policy to use. + /// Gets or sets the event creation policy to use. When NULL, it will use the static Audit.Core.Configuration.CreationPolicy. /// public EventCreationPolicy? CreationPolicy { get; set; } @@ -48,7 +50,7 @@ public Func DataProviderFactory public bool IsCreateAndSave { get; set; } /// - /// Gets or sets the initial audit event to use, or NULL to create a new instance of AuditEvent + /// Gets or sets the initial audit event to use. When NULL, it will create a new instance of AuditEvent /// public AuditEvent AuditEvent { get; set; } @@ -58,78 +60,50 @@ public Func DataProviderFactory public int SkipExtraFrames { get; set; } /// - /// Gets or sets a specific calling method to store on the event. NULL to use the calling stack to determine the calling method. + /// Gets or sets a specific calling method to store on the event. When NULL, it will use the calling stack to determine the calling method. /// public MethodBase CallingMethod { get; set; } /// - /// Gets or sets a value indicating whether the audit event's environment should include the full stack trace. + /// Gets or sets a value indicating whether the audit event's environment should include the full stack trace. When NULL, it will use the static Audit.Core.Configuration.IncludeStackTrace. + /// + public bool? IncludeStackTrace { get; set; } + + /// + /// Gets or sets the custom items to be included in the audit scope. /// - public bool IncludeStackTrace { get; set; } + public Dictionary Items { get; set; } = new(); + + /// + /// Gets or sets the system clock to use. When NULL, it uses the static Audit.Core.Configuration.SystemClock. + /// + public ISystemClock SystemClock { get; set; } + #if NET6_0_OR_GREATER /// - /// Gets or sets a value indicating whether the audit event should include the Distributed Tracing Activity data. + /// Gets or sets a value indicating whether the audit event should include the Distributed Tracing Activity data. When NULL, it will use the static Audit.Core.Configuration.IncludeActivityTrace. /// - public bool IncludeActivityTrace { get; set; } + public bool? IncludeActivityTrace { get; set; } /// - /// Gets or sets a value indicating whether the audit scope should create and start a new Distributed Tracing Activity. + /// Gets or sets a value indicating whether the audit scope should create and start a new Distributed Tracing Activity. When NULL, it will use the static Audit.Core.Configuration.StartActivityTrace. /// - public bool StartActivityTrace { get; set; } + public bool? StartActivityTrace { get; set; } #endif + /// - /// Creates an instance of options for an audit scope creation. - /// - /// A string representing the type of the event. - /// The target object getter. - /// An anonymous object that contains additional fields to be merged into the audit event. - /// The event creation policy to use. NULL to use the configured default creation policy. - /// The data provider to use. NULL to use the configured default data provider. - /// To indicate if the scope should be immediately saved after creation. - /// The initialized audit event to use, or NULL to create a new instance of AuditEvent. - /// Used to indicate how many frames in the stack should be skipped to determine the calling method. - /// Used to indicate whether the audit event's environment should include the full stack trace. - /// Used to indicate whether the audit event should include the activity trace. - /// Used to indicate whether the audit scope should create and start a new activity. - public AuditScopeOptions( - string eventType = null, - Func targetGetter = null, - object extraFields = null, - AuditDataProvider dataProvider = null, - EventCreationPolicy? creationPolicy = null, - bool isCreateAndSave = false, - AuditEvent auditEvent = null, - int skipExtraFrames = 0, - bool? includeStackTrace = null, - bool? includeActivityTrace = null, - bool? startActivityTrace = null) - { - EventType = eventType; - TargetGetter = targetGetter; - ExtraFields = extraFields; - CreationPolicy = creationPolicy ?? Configuration.CreationPolicy; - DataProvider = dataProvider ?? Configuration.DataProvider; - IsCreateAndSave = isCreateAndSave; - AuditEvent = auditEvent; - SkipExtraFrames = skipExtraFrames; - CallingMethod = null; - IncludeStackTrace = includeStackTrace ?? Configuration.IncludeStackTrace; -#if NET6_0_OR_GREATER - IncludeActivityTrace = includeActivityTrace ?? Configuration.IncludeActivityTrace; - StartActivityTrace = startActivityTrace ?? Configuration.StartActivityTrace; -#endif - } + /// Gets or sets a value indicating whether the environment information should be excluded from the audit event. When NULL, it will use the static Audit.Core.Configuration.ExcludeEnvironmentInfo. + /// + public bool? ExcludeEnvironmentInfo { get; set; } /// /// Initializes a new instance of the class. /// public AuditScopeOptions() - : this(null, null, null, null, null) { } public AuditScopeOptions(Action config) - : this(null, null, null, null, null) { if (config != null) { @@ -146,12 +120,14 @@ public AuditScopeOptions(Action config) SkipExtraFrames = scopeConfig._options.SkipExtraFrames; CallingMethod = scopeConfig._options.CallingMethod; IncludeStackTrace = scopeConfig._options.IncludeStackTrace; + Items = scopeConfig._options.Items; + SystemClock = scopeConfig._options.SystemClock; + ExcludeEnvironmentInfo = scopeConfig._options.ExcludeEnvironmentInfo; #if NET6_0_OR_GREATER IncludeActivityTrace = scopeConfig._options.IncludeActivityTrace; StartActivityTrace = scopeConfig._options.StartActivityTrace; #endif } - } } } diff --git a/src/Audit.NET/AuditScopeOptionsConfigurator.cs b/src/Audit.NET/AuditScopeOptionsConfigurator.cs index 745ef980..c371a7e7 100644 --- a/src/Audit.NET/AuditScopeOptionsConfigurator.cs +++ b/src/Audit.NET/AuditScopeOptionsConfigurator.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Reflection; using Audit.Core.Providers.Wrappers; @@ -79,6 +80,28 @@ public IAuditScopeOptionsConfigurator IncludeStackTrace(bool includeStackTrace = _options.IncludeStackTrace = includeStackTrace; return this; } + + public IAuditScopeOptionsConfigurator ExcludeEnvironmentInfo(bool excludeEnvironmentInfo = true) + { + _options.ExcludeEnvironmentInfo = excludeEnvironmentInfo; + return this; + } + + public IAuditScopeOptionsConfigurator SystemClock(ISystemClock systemClock) + { + _options.SystemClock = systemClock; + return this; + } + + public IAuditScopeOptionsConfigurator WithItem(string key, object value) + { + if (_options.Items == null) + { + _options.Items = new Dictionary(); + } + _options.Items[key] = value; + return this; + } } } diff --git a/src/Audit.NET/Configuration.cs b/src/Audit.NET/Configuration.cs index edc50586..83566c13 100644 --- a/src/Audit.NET/Configuration.cs +++ b/src/Audit.NET/Configuration.cs @@ -66,6 +66,10 @@ public static Func DataProviderFactory /// public static bool StartActivityTrace { get; set; } #endif + /// + /// Gets or Sets a value that indicates if the environment information should be excluded from the audit output. Default is false to include the environment object. + /// + public static bool ExcludeEnvironmentInfo { get; set; } /// /// Gets or Sets the Default audit scope factory. @@ -87,7 +91,7 @@ public static IAuditScopeFactory AuditScopeFactory public static JsonSerializerOptions JsonSettings { get; set; } // Custom actions - internal static Dictionary>> AuditScopeActions { get; private set; } + internal static Dictionary>>> AuditScopeActions { get; private set; } internal static readonly object Locker = new object(); @@ -121,6 +125,7 @@ public static void Reset() _auditScopeFactory = new AuditScopeFactory(); IncludeTypeNamespaces = false; IncludeStackTrace = false; + ExcludeEnvironmentInfo = false; #if NET6_0_OR_GREATER IncludeActivityTrace = false; StartActivityTrace = false; @@ -148,7 +153,24 @@ public static void AddCustomAction(ActionType when, Action action) AuditScopeActions[when].Add((scope, ct) => { action.Invoke(scope); - return Task.CompletedTask; + return Task.FromResult(true); + }); + } + } + + /// + /// Attaches an action to be performed globally on any AuditScope. The action returns a boolean value indicating if the subsequent actions should be executed. + /// + /// To indicate when the action should be performed. + /// The action to perform. Return true to continue with the next actions, false to stop executing the subsequent actions. + public static void AddCustomAction(ActionType when, Func action) + { + lock (Locker) + { + AuditScopeActions[when].Add((scope, ct) => + { + var result = action.Invoke(scope); + return Task.FromResult(result); }); } } @@ -165,6 +187,24 @@ public static void AddCustomAction(ActionType when, Func async AuditScopeActions[when].Add(async (scope, _) => { await asyncAction.Invoke(scope); + return true; + }); + } + } + + /// + /// Attaches an asynchronous action to be performed globally on any AuditScope. The action returns a boolean value indicating if the subsequent actions should be executed. + /// + /// To indicate when the action should be performed. + /// The asynchronous action to perform. Return true to continue with the next actions, false to stop executing the subsequent actions. + public static void AddCustomAction(ActionType when, Func> asyncAction) + { + lock (Locker) + { + AuditScopeActions[when].Add(async (scope, _) => + { + var result = await asyncAction.Invoke(scope); + return result; }); } } @@ -181,6 +221,24 @@ public static void AddCustomAction(ActionType when, Func { await asyncAction.Invoke(scope, ct); + return true; + }); + } + } + + /// + /// Attaches an asynchronous action to be performed globally on any AuditScope. The action returns a boolean value indicating if the subsequent actions should be executed. + /// + /// To indicate when the action should be performed. + /// The asynchronous action to perform. Return true to continue with the next actions, false to stop executing the subsequent actions. + public static void AddCustomAction(ActionType when, Func> asyncAction) + { + lock (Locker) + { + AuditScopeActions[when].Add(async (scope, ct) => + { + var result = await asyncAction.Invoke(scope, ct); + return result; }); } } @@ -196,7 +254,23 @@ public static void AddOnSavingAction(Action action) AuditScopeActions[ActionType.OnEventSaving].Add((scope, ct) => { action.Invoke(scope); - return Task.CompletedTask; + return Task.FromResult(true); + }); + } + } + + /// + /// Attaches a global action to be performed on the audit scope before the audit event is saved. The action returns a boolean value indicating if the subsequent actions should be executed. + /// + /// The action to perform. Return true to continue with the next actions, false to stop executing the subsequent actions. + public static void AddOnSavingAction(Func action) + { + lock (Locker) + { + AuditScopeActions[ActionType.OnEventSaving].Add((scope, ct) => + { + var result = action.Invoke(scope); + return Task.FromResult(result); }); } } @@ -212,6 +286,23 @@ public static void AddOnSavingAction(Func asyncAction) AuditScopeActions[ActionType.OnEventSaving].Add(async (scope, _) => { await asyncAction.Invoke(scope); + return true; + }); + } + } + + /// + /// Attaches a global asynchronous action to be performed on the audit scope before the audit event is saved. The action returns a boolean value indicating if the subsequent actions should be executed. + /// + /// The asynchronous action to perform. Return true to continue with the next actions, false to stop executing the subsequent actions. + public static void AddOnSavingAction(Func> asyncAction) + { + lock (Locker) + { + AuditScopeActions[ActionType.OnEventSaving].Add(async (scope, _) => + { + var result = await asyncAction.Invoke(scope); + return result; }); } } @@ -227,6 +318,23 @@ public static void AddOnSavingAction(Func a AuditScopeActions[ActionType.OnEventSaving].Add(async (scope, ct) => { await asyncAction.Invoke(scope, ct); + return true; + }); + } + } + + /// + /// Attaches a global asynchronous action to be performed on the audit scope before the audit event is saved. The action returns a boolean value indicating if the subsequent actions should be executed. + /// + /// The asynchronous action to perform. Return true to continue with the next actions, false to stop executing the subsequent actions. + public static void AddOnSavingAction(Func> asyncAction) + { + lock (Locker) + { + AuditScopeActions[ActionType.OnEventSaving].Add(async (scope, ct) => + { + var result = await asyncAction.Invoke(scope, ct); + return result; }); } } @@ -242,8 +350,23 @@ public static void AddOnCreatedAction(Action action) AuditScopeActions[ActionType.OnScopeCreated].Add((scope, ct) => { action.Invoke(scope); - return Task.CompletedTask; + return Task.FromResult(true); + }); + } + } + /// + /// Attaches a global action to be performed on the audit scope right after it is created and before any saving. The action returns a boolean value indicating if the subsequent actions should be executed. + /// + /// The action to perform. Return true to continue with the next actions, false to stop executing the subsequent actions. + public static void AddOnCreatedAction(Func action) + { + lock (Locker) + { + AuditScopeActions[ActionType.OnScopeCreated].Add((scope, ct) => + { + var result = action.Invoke(scope); + return Task.FromResult(result); }); } } @@ -259,6 +382,23 @@ public static void AddOnCreatedAction(Func asyncAction) AuditScopeActions[ActionType.OnScopeCreated].Add(async (scope, _) => { await asyncAction.Invoke(scope); + return true; + }); + } + } + + /// + /// Attaches a global asynchronous action to be performed on the audit scope right after it is created and before any saving. The action returns a boolean value indicating if the subsequent actions should be executed. + /// + /// The asynchronous action to perform. Return true to continue with the next actions, false to stop executing the subsequent actions. + public static void AddOnCreatedAction(Func> asyncAction) + { + lock (Locker) + { + AuditScopeActions[ActionType.OnScopeCreated].Add(async (scope, _) => + { + var result = await asyncAction.Invoke(scope); + return result; }); } } @@ -274,6 +414,24 @@ public static void AddOnCreatedAction(Func AuditScopeActions[ActionType.OnScopeCreated].Add(async (scope, ct) => { await asyncAction.Invoke(scope, ct); + return true; + }); + } + } + + + /// + /// Attaches a global asynchronous action to be performed on the audit scope right after it is created and before any saving. The action returns a boolean value indicating if the subsequent actions should be executed. + /// + /// The asynchronous action to perform. Return true to continue with the next actions, false to stop executing the subsequent actions. + public static void AddOnCreatedAction(Func> asyncAction) + { + lock (Locker) + { + AuditScopeActions[ActionType.OnScopeCreated].Add(async (scope, ct) => + { + var result = await asyncAction.Invoke(scope, ct); + return result; }); } } @@ -289,8 +447,23 @@ public static void AddOnDisposedAction(Action action) AuditScopeActions[ActionType.OnScopeDisposed].Add((scope, ct) => { action.Invoke(scope); - return Task.CompletedTask; + return Task.FromResult(true); + }); + } + } + /// + /// Attaches a global action to be performed on the audit scope right after it is disposed. The action returns a boolean value indicating if the subsequent actions should be executed. + /// + /// The action to perform. Return true to continue with the next actions, false to stop executing the subsequent actions. + public static void AddOnDisposedAction(Func action) + { + lock (Locker) + { + AuditScopeActions[ActionType.OnScopeDisposed].Add((scope, ct) => + { + var result = action.Invoke(scope); + return Task.FromResult(result); }); } } @@ -306,6 +479,23 @@ public static void AddOnDisposedAction(Func asyncAction) AuditScopeActions[ActionType.OnScopeDisposed].Add(async (scope, _) => { await asyncAction.Invoke(scope); + return true; + }); + } + } + + /// + /// Attaches a global action to be performed on the audit scope right after it is disposed. The action returns a boolean value indicating if the subsequent actions should be executed. + /// + /// The asynchronous action to perform. Return true to continue with the next actions, false to stop executing the subsequent actions. + public static void AddOnDisposedAction(Func> asyncAction) + { + lock (Locker) + { + AuditScopeActions[ActionType.OnScopeDisposed].Add(async (scope, _) => + { + var result = await asyncAction.Invoke(scope); + return result; }); } } @@ -321,10 +511,27 @@ public static void AddOnDisposedAction(Func AuditScopeActions[ActionType.OnScopeDisposed].Add(async (scope, ct) => { await asyncAction.Invoke(scope, ct); + return true; }); } } - + + /// + /// Attaches a global action to be performed on the audit scope right after it is disposed. The action returns a boolean value indicating if the subsequent actions should be executed. + /// + /// The asynchronous action to perform. Return true to continue with the next actions, false to stop executing the subsequent actions. + public static void AddOnDisposedAction(Func> asyncAction) + { + lock (Locker) + { + AuditScopeActions[ActionType.OnScopeDisposed].Add(async (scope, ct) => + { + var result = await asyncAction.Invoke(scope, ct); + return result; + }); + } + } + /// /// Resets the audit scope handlers. Removes all the attached actions for the Audit Scopes. /// @@ -332,12 +539,12 @@ public static void ResetCustomActions() { lock (Locker) { - AuditScopeActions = new Dictionary>>() + AuditScopeActions = new() { - {ActionType.OnScopeCreated, new List>()}, - {ActionType.OnEventSaving, new List>()}, - {ActionType.OnEventSaved, new List>()}, - {ActionType.OnScopeDisposed, new List>()} + { ActionType.OnScopeCreated, new() }, + { ActionType.OnEventSaving, new() }, + { ActionType.OnEventSaved, new() }, + { ActionType.OnScopeDisposed, new() } }; } } @@ -354,34 +561,42 @@ public static void ResetCustomActions(ActionType actionType) } /// - /// Synchronously invokes the scope custom actions. + /// Synchronously invokes the global custom actions. /// - internal static void InvokeScopeCustomActions(ActionType type, AuditScope auditScope) + internal static void InvokeCustomActions(ActionType type, AuditScope auditScope) { - List> actions; + List>> actions; lock (Locker) { actions = AuditScopeActions[type].ToList(); } foreach (var action in actions) { - action.Invoke(auditScope, default).GetAwaiter().GetResult(); + var @continue = action.Invoke(auditScope, default).GetAwaiter().GetResult(); + if (!@continue) + { + break; + } } } /// - /// Asynchronously invokes the scope custom actions. + /// Asynchronously invokes the global custom actions. /// - internal static async Task InvokeScopeCustomActionsAsync(ActionType type, AuditScope auditScope, CancellationToken cancellationToken) + internal static async Task InvokeCustomActionsAsync(ActionType type, AuditScope auditScope, CancellationToken cancellationToken) { - List> actions; + List>> actions; lock (Locker) { actions = AuditScopeActions[type].ToList(); } foreach (var action in actions) { - await action.Invoke(auditScope, cancellationToken); + var @continue = await action.Invoke(auditScope, cancellationToken); + if (!@continue) + { + break; + } } } diff --git a/src/Audit.NET/ConfigurationApi/Configurator.cs b/src/Audit.NET/ConfigurationApi/Configurator.cs index 70b703f7..8fe1919d 100644 --- a/src/Audit.NET/ConfigurationApi/Configurator.cs +++ b/src/Audit.NET/ConfigurationApi/Configurator.cs @@ -127,6 +127,15 @@ public ICreationPolicyConfigurator UseInMemoryProvider() return new CreationPolicyConfigurator(); } + public ICreationPolicyConfigurator UseInMemoryProvider(out InMemoryDataProvider dataProvider) + { + dataProvider = new InMemoryDataProvider(); + + Configuration.DataProvider = dataProvider; + + return new CreationPolicyConfigurator(); + } + public ICreationPolicyConfigurator UseInMemoryBlockingCollectionProvider(Action config) { Configuration.DataProvider = new BlockingCollectionDataProvider(config); diff --git a/src/Audit.NET/ConfigurationApi/IConfigurator.cs b/src/Audit.NET/ConfigurationApi/IConfigurator.cs index e0044cea..26eff03b 100644 --- a/src/Audit.NET/ConfigurationApi/IConfigurator.cs +++ b/src/Audit.NET/ConfigurationApi/IConfigurator.cs @@ -1,4 +1,5 @@ using System; +using Audit.Core.Providers; namespace Audit.Core.ConfigurationApi { @@ -112,6 +113,12 @@ public interface IConfigurator /// ICreationPolicyConfigurator UseInMemoryProvider(); + /// + /// Store the events in memory in a thread-safe list. Useful for testing purposes. Returns the created InMemoryDataProvider instance as an out parameter. + /// + /// The created InMemoryDataProvider instance + ICreationPolicyConfigurator UseInMemoryProvider(out InMemoryDataProvider dataProvider); + /// /// Store the events in memory in a thread-safe BlockingCollection. Useful for scenarios where the events need to be consumed by another thread. /// diff --git a/src/Audit.NET/DefaultSystemClock.cs b/src/Audit.NET/DefaultSystemClock.cs index e84c3164..c58182b1 100644 --- a/src/Audit.NET/DefaultSystemClock.cs +++ b/src/Audit.NET/DefaultSystemClock.cs @@ -7,6 +7,10 @@ namespace Audit.Core /// public class DefaultSystemClock : ISystemClock { - public virtual DateTime UtcNow => DateTime.UtcNow; + /// + public DateTime GetCurrentDateTime() + { + return DateTime.UtcNow; + } } } diff --git a/src/Audit.NET/Extensions/TypeExtensions.cs b/src/Audit.NET/Extensions/TypeExtensions.cs index 32e54d2d..a07cabf8 100644 --- a/src/Audit.NET/Extensions/TypeExtensions.cs +++ b/src/Audit.NET/Extensions/TypeExtensions.cs @@ -32,14 +32,10 @@ public static string GetFullTypeName(this Type type) return name; } var sb = new StringBuilder(); - sb.Append(name.Substring(0, name.IndexOf("`"))); + sb.Append(name.Substring(0, name.IndexOf("`", StringComparison.Ordinal))); sb.Append(genericTypes.Aggregate("<", - delegate (string aggregate, Type t) - { - return aggregate + (aggregate == "<" ? "" : ",") + GetFullTypeName(t); - } - )); + (aggregate, t) => aggregate + (aggregate == "<" ? "" : ",") + GetFullTypeName(t))); sb.Append(">"); return AnonymousTypeRegex.Replace(sb.ToString(), AnonymousReplacementString); } diff --git a/src/Audit.NET/IAuditScope.cs b/src/Audit.NET/IAuditScope.cs index 0f535f24..66a1c0cb 100644 --- a/src/Audit.NET/IAuditScope.cs +++ b/src/Audit.NET/IAuditScope.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -6,19 +7,95 @@ namespace Audit.Core { public interface IAuditScope : IDisposable, IAsyncDisposable { + /// + /// Gets the data provider for this AuditScope instance. + /// AuditDataProvider DataProvider { get; } + + /// + /// Gets the event related to this scope. + /// AuditEvent Event { get; } + + /// + /// Gets the creation policy for this scope. + /// EventCreationPolicy EventCreationPolicy { get; } + + /// + /// Gets a key/value collection that can be used to share data within this Audit Scope. + /// + IDictionary Items { get; } + + /// + /// Gets the current event ID, or NULL if not yet created. + /// object EventId { get; } + + /// + /// Indicates the change type + /// string EventType { get; set; } + + /// + /// The current save mode. Useful on custom actions to determine the saving trigger. + /// SaveMode SaveMode { get; } + + /// + /// Add a textual comment to the event + /// void Comment(string text); + + /// + /// Add a textual comment to the event + /// void Comment(string format, params object[] args); + + /// + /// Discards this audit scope, so the event will not be written. + /// void Discard(); + + /// + /// Manually Saves (insert/replace) the Event. + /// Use this method to save (insert/replace) the event when CreationPolicy is set to Manual. + /// void Save(); + + /// + /// Manually Saves (insert/replace) the Event asynchronously. + /// Use this method to save (insert/replace) the event when CreationPolicy is set to Manual. + /// + /// The Cancellation Token. Task SaveAsync(CancellationToken cancellationToken = default); + + /// + /// Adds a custom field to the event + /// + /// The type of the value. + /// Name of the field. + /// The value object. + /// if set to true the value will be serialized immediately. void SetCustomField(string fieldName, TC value, bool serialize = false); + + /// + /// Replaces the target object getter whose old/new value will be stored on the AuditEvent.Target property + /// + /// A function that returns the target void SetTargetGetter(Func targetGetter); + + /// + /// Gets the event related to this scope of a known AuditEvent derived type. Returns null if the event is not of the specified type. + /// + /// The AuditEvent derived type T EventAs() where T : AuditEvent; + + /// + /// Returns the value of an item from the Items collection, or NULL if the key does not exist or is of a different type. + /// + /// The type of the value. + /// The key. + T GetItem(string key); } } \ No newline at end of file diff --git a/src/Audit.NET/IAuditScopeOptionsConfigurator.cs b/src/Audit.NET/IAuditScopeOptionsConfigurator.cs index 557c55ae..6496fca4 100644 --- a/src/Audit.NET/IAuditScopeOptionsConfigurator.cs +++ b/src/Audit.NET/IAuditScopeOptionsConfigurator.cs @@ -53,6 +53,22 @@ public interface IAuditScopeOptionsConfigurator /// Sets the value used to indicate whether the audit event's environment should include the full stack trace /// IAuditScopeOptionsConfigurator IncludeStackTrace(bool includeStackTrace = true); + /// + /// Sets the value used to indicate whether the audit event's should exclude the environment information + /// + IAuditScopeOptionsConfigurator ExcludeEnvironmentInfo(bool excludeEnvironmentInfo = true); + /// + /// Sets the system clock to use within this scope. + /// + /// The system clock to use + IAuditScopeOptionsConfigurator SystemClock(ISystemClock systemClock); + /// + /// Adds an item to the custom items collection within the audit scope. + /// These items are excluded from the audit event output but may be utilized by data providers or custom actions + /// + /// The item key + /// The item value + IAuditScopeOptionsConfigurator WithItem(string key, object value); } } diff --git a/src/Audit.NET/ISystemClock.cs b/src/Audit.NET/ISystemClock.cs index 3ed37d63..62fa1669 100644 --- a/src/Audit.NET/ISystemClock.cs +++ b/src/Audit.NET/ISystemClock.cs @@ -8,11 +8,9 @@ namespace Audit.Core public interface ISystemClock { /// - /// Retrieves the current system time in UTC. + /// Retrieves the current system time to be stored in the audit event. /// - DateTime UtcNow - { - get; - } + /// + DateTime GetCurrentDateTime(); } } diff --git a/src/Audit.NET/Providers/EventLogDataProvider.cs b/src/Audit.NET/Providers/EventLogDataProvider.cs index f217bd58..c4206367 100644 --- a/src/Audit.NET/Providers/EventLogDataProvider.cs +++ b/src/Audit.NET/Providers/EventLogDataProvider.cs @@ -67,7 +67,7 @@ public override object InsertEvent(AuditEvent auditEvent) using (var eventLog = new EventLog(logName, machine, source)) { - eventLog.WriteEntry(message, auditEvent.Environment.Exception == null ? EventLogEntryType.Information : EventLogEntryType.Error); + eventLog.WriteEntry(message, auditEvent.Environment?.Exception == null ? EventLogEntryType.Information : EventLogEntryType.Error); } return null; diff --git a/src/Audit.NET/Providers/FileDataProvider.cs b/src/Audit.NET/Providers/FileDataProvider.cs index 09e1b8aa..b8e1963f 100644 --- a/src/Audit.NET/Providers/FileDataProvider.cs +++ b/src/Audit.NET/Providers/FileDataProvider.cs @@ -115,7 +115,7 @@ private string GetFilePath(AuditEvent auditEvent) } else { - fileName += $"{Configuration.SystemClock.UtcNow:yyyyMMddHHmmss}_{HiResDateTime.UtcNowTicks}.json"; + fileName += $"{Configuration.SystemClock.GetCurrentDateTime():yyyyMMddHHmmss}_{HiResDateTime.UtcNowTicks}.json"; } var directory = DirectoryPath.GetValue(auditEvent) ?? ""; @@ -131,7 +131,7 @@ private string GetFilePath(AuditEvent auditEvent) // Original from: http://stackoverflow.com/a/14369695/122195 public class HiResDateTime { - private static long lastTimeStamp = Configuration.SystemClock.UtcNow.Ticks; + private static long lastTimeStamp = Configuration.SystemClock.GetCurrentDateTime().Ticks; public static long UtcNowTicks { get @@ -140,7 +140,7 @@ public static long UtcNowTicks do { original = lastTimeStamp; - long now = Configuration.SystemClock.UtcNow.Ticks; + long now = Configuration.SystemClock.GetCurrentDateTime().Ticks; newValue = Math.Max(now, original + 1); } while (Interlocked.CompareExchange (ref lastTimeStamp, newValue, original) != original); diff --git a/src/Audit.SignalR/AuditHubFilter.cs b/src/Audit.SignalR/AuditHubFilter.cs index b23a8231..5ea3f112 100644 --- a/src/Audit.SignalR/AuditHubFilter.cs +++ b/src/Audit.SignalR/AuditHubFilter.cs @@ -170,8 +170,8 @@ private async Task CreateAuditScopeAsync(SignalrEventBase signalrEv // Try to get IAuditScopeFactory / DataProvider as registered services var httpContext = signalrEvent.Hub.Context.GetHttpContext(); - var scopeFactory = httpContext?.RequestServices.GetService() ?? Core.Configuration.AuditScopeFactory; - var dataProvider = AuditDataProvider ?? httpContext?.RequestServices.GetService(); + var scopeFactory = httpContext?.RequestServices?.GetService() ?? Core.Configuration.AuditScopeFactory; + var dataProvider = AuditDataProvider ?? httpContext?.RequestServices?.GetService(); var scope = await scopeFactory.CreateAsync(new AuditScopeOptions() { diff --git a/src/Audit.SignalR/SignalrExtensions.cs b/src/Audit.SignalR/SignalrExtensions.cs index 9c53d9cb..5aedf96d 100644 --- a/src/Audit.SignalR/SignalrExtensions.cs +++ b/src/Audit.SignalR/SignalrExtensions.cs @@ -31,6 +31,25 @@ public static T GetSignalrEvent(this AuditEvent auditEvent) return (auditEvent as AuditEventSignalr)?.Event as T; } + /// + /// Gets the SignalR Event portion from the Audit Scope. + /// + /// The audit scope. + public static SignalrEventBase GetSignalrEvent(this AuditScope auditScope) + { + return (auditScope.Event as AuditEventSignalr)?.Event; + } + + /// + /// Gets the SignalR Event portion from the Audit Scope. + /// + /// The audit scope. + public static T GetSignalrEvent(this AuditScope auditScope) + where T : SignalrEventBase + { + return (auditScope.Event as AuditEventSignalr)?.Event as T; + } + #if ASP_NET /// /// Adds a custom field to the current Incoming event diff --git a/src/Audit.WCF.Client/AuditMessageInspector.cs b/src/Audit.WCF.Client/AuditMessageInspector.cs index 8505a43a..81492e3d 100644 --- a/src/Audit.WCF.Client/AuditMessageInspector.cs +++ b/src/Audit.WCF.Client/AuditMessageInspector.cs @@ -38,8 +38,9 @@ public object BeforeSendRequest(ref Message request, IClientChannel channel) { WcfClientEvent = auditWcfEvent }; + // Create the audit scope - var auditScope = AuditScope.Create(new AuditScopeOptions() + var auditScope = Configuration.AuditScopeFactory.Create(new AuditScopeOptions() { EventType = eventType, AuditEvent = auditEventWcf, diff --git a/src/Audit.WCF/AuditOperationInvoker.cs b/src/Audit.WCF/AuditOperationInvoker.cs index c3c577b1..bdbdf9aa 100644 --- a/src/Audit.WCF/AuditOperationInvoker.cs +++ b/src/Audit.WCF/AuditOperationInvoker.cs @@ -62,7 +62,7 @@ public object Invoke(object instance, object[] inputs, out object[] outputs) WcfEvent = auditWcfEvent }; // Create the audit scope - using (var auditScope = AuditScope.Create(new AuditScopeOptions() + using (var auditScope = Configuration.AuditScopeFactory.Create(new AuditScopeOptions() { EventType = eventType, CreationPolicy = _creationPolicy, @@ -109,7 +109,7 @@ public IAsyncResult InvokeBegin(object instance, object[] inputs, AsyncCallback WcfEvent = auditWcfEvent }; // Create the audit scope - var auditScope = AuditScope.Create(new AuditScopeOptions() + var auditScope = Configuration.AuditScopeFactory.Create(new AuditScopeOptions() { EventType = eventType, CreationPolicy = _creationPolicy, diff --git a/src/Audit.WebApi/AuditApiAdapter.Core.cs b/src/Audit.WebApi/AuditApiAdapter.Core.cs index bae9f172..0e36d84c 100644 --- a/src/Audit.WebApi/AuditApiAdapter.Core.cs +++ b/src/Audit.WebApi/AuditApiAdapter.Core.cs @@ -77,8 +77,11 @@ internal async Task BeforeExecutingAsync(ActionExecutingContext actionContext, { Action = auditAction }; + + var scopeFactory = httpContext.RequestServices?.GetService() ?? Core.Configuration.AuditScopeFactory; var dataProvider = httpContext.RequestServices?.GetService(); - var auditScope = await Core.Configuration.AuditScopeFactory.CreateAsync(new AuditScopeOptions() + + var auditScope = await scopeFactory.CreateAsync(new AuditScopeOptions() { EventType = eventType, AuditEvent = auditEventAction, diff --git a/src/Audit.WebApi/AuditApiAdapter.cs b/src/Audit.WebApi/AuditApiAdapter.cs index abc56dbe..701e9694 100644 --- a/src/Audit.WebApi/AuditApiAdapter.cs +++ b/src/Audit.WebApi/AuditApiAdapter.cs @@ -1,20 +1,19 @@ #if ASP_NET -using System; -using Audit.Core; -using System.Threading.Tasks; -using Audit.Core.Extensions; using System.Collections.Generic; +using System.Collections.Specialized; using System.IO; -using System.Text; +using System.Linq; using System.Net.Http; -using System.Web.Http.Controllers; -using System.Web.Http.Filters; using System.Net.Http.Headers; -using System.Collections.Specialized; -using System.Reflection; -using System.Linq; using System.Runtime.CompilerServices; +using System.Text; using System.Threading; +using System.Threading.Tasks; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; + +using Audit.Core; +using Audit.Core.Extensions; using Audit.Core.Providers; namespace Audit.WebApi @@ -68,8 +67,11 @@ public async Task BeforeExecutingAsync(HttpActionContext actionContext, IContext { Action = auditAction }; + var httpContext = contextWrapper.GetHttpContext(); + + var scopeFactory = (httpContext?.GetService(typeof(IAuditScopeFactory)) as IAuditScopeFactory) ?? Core.Configuration.AuditScopeFactory; + var dataProvider = httpContext?.GetService(typeof(AuditDataProvider)) as AuditDataProvider; - var dataProvider = contextWrapper.GetHttpContext()?.GetService(typeof(AuditDataProvider)) as AuditDataProvider; var options = new AuditScopeOptions() { EventType = eventType, @@ -78,7 +80,7 @@ public async Task BeforeExecutingAsync(HttpActionContext actionContext, IContext // the inner ActionDescriptor is of type ReflectedHttpActionDescriptor even when using api versioning: CallingMethod = (actionContext.ActionDescriptor?.ActionBinding?.ActionDescriptor as ReflectedHttpActionDescriptor)?.MethodInfo }; - var auditScope = await Configuration.AuditScopeFactory.CreateAsync(options, cancellationToken); + var auditScope = await scopeFactory.CreateAsync(options, cancellationToken); contextWrapper.Set(AuditApiHelper.AuditApiActionKey, auditAction); contextWrapper.Set(AuditApiHelper.AuditApiScopeKey, auditScope); } diff --git a/src/Audit.WebApi/AuditMiddleware.cs b/src/Audit.WebApi/AuditMiddleware.cs index 6bc2a45e..46470f07 100644 --- a/src/Audit.WebApi/AuditMiddleware.cs +++ b/src/Audit.WebApi/AuditMiddleware.cs @@ -122,8 +122,11 @@ private async Task BeforeInvoke(HttpContext context, bool includeHeaders, bool i { Action = auditAction }; + + var scopeFactory = context.RequestServices?.GetService() ?? Core.Configuration.AuditScopeFactory; var dataProvider = context.RequestServices?.GetService(); - var auditScope = await Core.Configuration.AuditScopeFactory.CreateAsync(new AuditScopeOptions() + + var auditScope = await scopeFactory.CreateAsync(new AuditScopeOptions() { EventType = eventType, AuditEvent = auditEventAction, diff --git a/test/Audit.EntityFramework.Core.UnitTest/DbCommandInterceptorTests.cs b/test/Audit.EntityFramework.Core.UnitTest/DbCommandInterceptorTests.cs index 0574e3e5..f9793df1 100644 --- a/test/Audit.EntityFramework.Core.UnitTest/DbCommandInterceptorTests.cs +++ b/test/Audit.EntityFramework.Core.UnitTest/DbCommandInterceptorTests.cs @@ -13,6 +13,7 @@ using System.Linq; using System.Threading.Tasks; using Audit.Core.Providers; +using Microsoft.EntityFrameworkCore.Infrastructure.Internal; namespace Audit.EntityFramework.Core.UnitTest { diff --git a/test/Audit.MongoClient.UnitTest/MongoAuditEventSubscriberTests.cs b/test/Audit.MongoClient.UnitTest/MongoAuditEventSubscriberTests.cs index 817af457..ab0c7d69 100644 --- a/test/Audit.MongoClient.UnitTest/MongoAuditEventSubscriberTests.cs +++ b/test/Audit.MongoClient.UnitTest/MongoAuditEventSubscriberTests.cs @@ -205,7 +205,7 @@ public void Test_Custom_AuditScopeFactory() var mockFactory = new Mock(MockBehavior.Strict); mockFactory.Setup(x => x.Create(It.IsAny())) - .Returns(AuditScope.Create(new AuditScopeOptions(eventType))); + .Returns(AuditScope.Create(new AuditScopeOptions() { EventType = eventType })); var sut = new MongoAuditEventSubscriber() { diff --git a/test/Audit.UnitTest/Audit.UnitTest.csproj b/test/Audit.UnitTest/Audit.UnitTest.csproj index 58d226c0..8f605502 100644 --- a/test/Audit.UnitTest/Audit.UnitTest.csproj +++ b/test/Audit.UnitTest/Audit.UnitTest.csproj @@ -3,7 +3,7 @@ 3.0.0 net462;net472;netcoreapp3.1;net6.0;net7.0;net8.0 - 8.0 + latest $(DefineConstants);STRONG_NAME $(NoWarn);1591;NETSDK1138 Audit.UnitTest diff --git a/test/Audit.UnitTest/AuditScopeFactoryTests.cs b/test/Audit.UnitTest/AuditScopeFactoryTests.cs new file mode 100644 index 00000000..9c610330 --- /dev/null +++ b/test/Audit.UnitTest/AuditScopeFactoryTests.cs @@ -0,0 +1,133 @@ +using System.Threading.Tasks; + +using Audit.Core; +using Audit.Core.Providers; + +using NUnit.Framework; + +namespace Audit.UnitTest +{ + [TestFixture] + public class AuditScopeFactoryTests + { + [SetUp] + public void SetUp() + { + Configuration.Reset(); + Configuration.DataProvider = new InMemoryDataProvider(); + Configuration.CreationPolicy = EventCreationPolicy.Manual; + } + + [Test] + public void Test_Create_OnScopeCreated_OnConfiguring_Calls() + { + // Arrange + var auditScopeFactory = new MockAuditScopeFactory(); + + // Act + var scope = auditScopeFactory.Create(new AuditScopeOptions()); + scope.Save(); + scope.Dispose(); + + scope = auditScopeFactory.Create(cfg => cfg.EventType("")); + scope.Save(); + scope.Dispose(); + + scope = auditScopeFactory.Create(eventType: "eventType", target: null); + scope.Save(); + scope.Dispose(); + + scope = auditScopeFactory.Create(eventType: "eventType", target: null, EventCreationPolicy.Manual, new InMemoryDataProvider()); + scope.Save(); + scope.Dispose(); + + scope = auditScopeFactory.Create(eventType: "eventType", target: null, extraFields: new {}, EventCreationPolicy.Manual, new InMemoryDataProvider()); + scope.Save(); + scope.Dispose(); + + // Assert + Assert.That(scope, Is.Not.Null); + Assert.That(auditScopeFactory.OnConfiguringCalls, Is.EqualTo(5)); + Assert.That(auditScopeFactory.OnScopeCreatedCalls, Is.EqualTo(5)); + } + + [Test] + public async Task Test_CreateAsync_OnScopeCreated_OnConfiguring_Calls() + { + // Arrange + var auditScopeFactory = new MockAuditScopeFactory(); + + // Act + var scope = await auditScopeFactory.CreateAsync(new AuditScopeOptions()); + await scope.SaveAsync(); + await scope.DisposeAsync(); + + scope = await auditScopeFactory.CreateAsync(cfg => cfg.EventType("")); + await scope.SaveAsync(); + await scope.DisposeAsync(); + + scope = await auditScopeFactory.CreateAsync(eventType: "eventType", target: null); + await scope.SaveAsync(); + await scope.DisposeAsync(); + + scope = await auditScopeFactory.CreateAsync(eventType: "eventType", target: null, EventCreationPolicy.Manual, new InMemoryDataProvider()); + await scope.SaveAsync(); + await scope.DisposeAsync(); + + scope = await auditScopeFactory.CreateAsync(eventType: "eventType", target: null, extraFields: new { }, EventCreationPolicy.Manual, new InMemoryDataProvider()); + await scope.SaveAsync(); + await scope.DisposeAsync(); + + // Assert + Assert.That(scope, Is.Not.Null); + Assert.That(auditScopeFactory.OnConfiguringCalls, Is.EqualTo(5)); + Assert.That(auditScopeFactory.OnScopeCreatedCalls, Is.EqualTo(5)); + } + + [Test] + public void Test_Log_OnScopeCreated_OnConfiguring_Calls() + { + // Arrange + var auditScopeFactory = new MockAuditScopeFactory(); + + // Act + auditScopeFactory.Log("eventType", extraFields: new { }); + auditScopeFactory.Log("eventType", extraFields: new { }); + + // Assert + Assert.That(auditScopeFactory.OnConfiguringCalls, Is.EqualTo(2)); + Assert.That(auditScopeFactory.OnScopeCreatedCalls, Is.EqualTo(2)); + } + + [Test] + public async Task Test_LogAsync_OnScopeCreated_OnConfiguring_Calls() + { + // Arrange + var auditScopeFactory = new MockAuditScopeFactory(); + + // Act + await auditScopeFactory.LogAsync("eventType", extraFields: new { }); + await auditScopeFactory.LogAsync("eventType", extraFields: new { }); + + // Assert + Assert.That(auditScopeFactory.OnConfiguringCalls, Is.EqualTo(2)); + Assert.That(auditScopeFactory.OnScopeCreatedCalls, Is.EqualTo(2)); + } + } + + public class MockAuditScopeFactory : AuditScopeFactory + { + public int OnScopeCreatedCalls; + public int OnConfiguringCalls; + + public override void OnScopeCreated(AuditScope auditScope) + { + OnScopeCreatedCalls++; + } + + public override void OnConfiguring(AuditScopeOptions options) + { + OnConfiguringCalls++; + } + } +} diff --git a/test/Audit.UnitTest/AuditScopeTests.cs b/test/Audit.UnitTest/AuditScopeTests.cs index 8bd031e8..5774bf3f 100644 --- a/test/Audit.UnitTest/AuditScopeTests.cs +++ b/test/Audit.UnitTest/AuditScopeTests.cs @@ -1,11 +1,11 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; -using System.Text; using System.Threading.Tasks; + using Audit.Core; using Audit.Core.Providers; + using NUnit.Framework; namespace Audit.UnitTest @@ -18,7 +18,63 @@ public void Setup() { Audit.Core.Configuration.Reset(); } - + + [Test] + public void Test_ScopeItems() + { + var testObject = new object(); + + object onCreatedObject = null; + + Audit.Core.Configuration.Setup().UseInMemoryProvider(out var dp); + + Audit.Core.Configuration.AddOnCreatedAction(scope => + { + onCreatedObject = scope.Items["TestObject"]; + }); + + var scope = AuditScope.Create(c => c.EventType("Event").WithItem("TestObject", testObject)); + + var afterCreatedObject = scope.Items["TestObject"]; + + scope.Dispose(); + + var evs = dp.GetAllEvents(); + + Assert.That(evs, Has.Count.EqualTo(1)); + Assert.That(onCreatedObject, Is.Not.Null); + Assert.That(onCreatedObject, Is.EqualTo(testObject)); + Assert.That(afterCreatedObject, Is.Not.Null); + Assert.That(afterCreatedObject, Is.EqualTo(testObject)); + } + + [Test] + public void Test_ScopeItems_GetItem() + { + Audit.Core.Configuration.Setup().UseInMemoryProvider(out var dp); + + var scope = AuditScope.Create(new AuditScopeOptions() + { + Items = new() { { "Key", new ArgumentException("test") }} + }); + + var getItemAsObject = scope.GetItem("Key"); + var getItemAsException = scope.GetItem("Key"); + var getItemAsArgumentException = scope.GetItem("Key"); + + var getItemDifferentType = scope.GetItem("Key"); + var getItemKeyNotFound = scope.GetItem("NotFound"); + + scope.Dispose(); + + Assert.That(getItemAsObject, Is.Not.Null); + Assert.That(getItemAsException, Is.Not.Null); + Assert.That(getItemAsArgumentException, Is.Not.Null); + + Assert.That(getItemDifferentType, Is.Null); + Assert.That(getItemKeyNotFound, Is.Null); + } + [Test] public void Test_AuditEvent_GetAuditScope_WeakReference() { diff --git a/test/Audit.UnitTest/ConfigurationTests.cs b/test/Audit.UnitTest/ConfigurationTests.cs index 120b5226..2d7ff58c 100644 --- a/test/Audit.UnitTest/ConfigurationTests.cs +++ b/test/Audit.UnitTest/ConfigurationTests.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Audit.Core; using Audit.Core.Providers; using NUnit.Framework; @@ -12,14 +13,13 @@ public class ConfigurationTests public void SetUp() { Core.Configuration.Reset(); + Core.Configuration.DataProvider = new InMemoryDataProvider(); + Core.Configuration.CreationPolicy = EventCreationPolicy.Manual; } [Test] public void TestConfiguration_DataProviderAs() { - // Arrange - Core.Configuration.DataProvider = new InMemoryDataProvider(); - // Act var dataProvider = Core.Configuration.DataProviderAs(); @@ -33,8 +33,6 @@ public async Task TestConfiguration_AddCustomActionAsync() { // Arrange int test = 0; - Core.Configuration.DataProvider = new InMemoryDataProvider(); - Core.Configuration.CreationPolicy = EventCreationPolicy.Manual; // Act Core.Configuration.AddCustomAction(ActionType.OnEventSaved, async (scope, ct) => @@ -54,14 +52,9 @@ public void TestConfiguration_AddOnSavingAction() { // Arrange int test = 0; - Core.Configuration.DataProvider = new InMemoryDataProvider(); - Core.Configuration.CreationPolicy = EventCreationPolicy.Manual; - + // Act - Core.Configuration.AddOnSavingAction(scope => - { - test++; - }); + Core.Configuration.AddOnSavingAction(scope => { test++; }); var scope = AuditScope.Create("test", null); scope.Save(); @@ -75,8 +68,6 @@ public async Task TestConfiguration_AddOnSavingActionAsync() { // Arrange int test = 0; - Core.Configuration.DataProvider = new InMemoryDataProvider(); - Core.Configuration.CreationPolicy = EventCreationPolicy.Manual; // Act Core.Configuration.AddOnSavingAction(async (scope, ct) => @@ -97,8 +88,6 @@ public async Task TestConfiguration_AddOnSavingActionAsyncNoCancellationToken() { // Arrange int test = 0; - Core.Configuration.DataProvider = new InMemoryDataProvider(); - Core.Configuration.CreationPolicy = EventCreationPolicy.Manual; // Act Core.Configuration.AddOnSavingAction(async scope => @@ -119,8 +108,6 @@ public async Task TestConfiguration_AddOnCreatedActionAsync() { // Arrange int test = 0; - Core.Configuration.DataProvider = new InMemoryDataProvider(); - Core.Configuration.CreationPolicy = EventCreationPolicy.Manual; // Act Core.Configuration.AddOnCreatedAction(async (scope, ct) => @@ -141,8 +128,6 @@ public async Task TestConfiguration_AddOnCreatedActionAsyncNoCancellationToken() { // Arrange int test = 0; - Core.Configuration.DataProvider = new InMemoryDataProvider(); - Core.Configuration.CreationPolicy = EventCreationPolicy.Manual; // Act Core.Configuration.AddOnCreatedAction(async scope => @@ -165,8 +150,6 @@ public async Task TestConfiguration_AddOnCreatedActionAsyncNoCancellationToken() public void TestConfiguration_ResetCustomActionsByType(ActionType type) { // Arrange - Core.Configuration.DataProvider = new InMemoryDataProvider(); - Core.Configuration.CreationPolicy = EventCreationPolicy.Manual; Core.Configuration.AddCustomAction(ActionType.OnScopeCreated, _ => { }); Core.Configuration.AddCustomAction(ActionType.OnEventSaving, _ => { }); Core.Configuration.AddCustomAction(ActionType.OnEventSaved, _ => { }); @@ -184,14 +167,10 @@ public void TestConfiguration_ResetCustomActionsByType(ActionType type) public void TestConfiguration_AddOnDisposedAction() { int onDisposed = 0; - Core.Configuration.AddOnDisposedAction(s => - { - onDisposed++; - - }); + Core.Configuration.AddOnDisposedAction(s => { onDisposed++; }); Core.Configuration.AddOnDisposedAction(async s => { - await Task.Yield(); + await Task.Yield(); onDisposed++; }); Core.Configuration.AddOnDisposedAction(async (s, ct) => @@ -207,8 +186,166 @@ public void TestConfiguration_AddOnDisposedAction() var scope = AuditScope.Create(new AuditScopeOptions() { DataProvider = new NullDataProvider() }); scope.Dispose(); - + Assert.That(onDisposed, Is.EqualTo(4)); } + + [TestCase(ActionType.OnScopeCreated)] + [TestCase(ActionType.OnEventSaving)] + [TestCase(ActionType.OnScopeDisposed)] + public void TestConfiguration_CustomActionsContinuation(ActionType type) + { + // Arrange + int count = 0; + + // To cover the methods per action type + switch (type) + { + case ActionType.OnScopeCreated: + Core.Configuration.AddOnCreatedAction(scope => + { + count++; + return true; + }); + break; + case ActionType.OnEventSaving: + Core.Configuration.AddOnSavingAction(scope => + { + count++; + return true; + }); + break; + case ActionType.OnScopeDisposed: + Core.Configuration.AddOnDisposedAction(scope => + { + count++; + return true; + }); + break; + default: + Core.Configuration.AddCustomAction(type, scope => + { + count++; + return true; + }); + break; + } + + Core.Configuration.AddCustomAction(type, async (scope, ct) => + { + count++; + return await Task.FromResult(false); + }); + Core.Configuration.AddCustomAction(type, async (scope, ct) => + { + count = -1; + return await Task.FromResult(true); + }); + + // Act + var scope = AuditScope.Create("test", null); + scope.Save(); + scope.Dispose(); + + // Assert + Assert.That(count, Is.EqualTo(2)); + } + + [TestCase(ActionType.OnScopeCreated)] + [TestCase(ActionType.OnEventSaving)] + [TestCase(ActionType.OnEventSaved)] + [TestCase(ActionType.OnScopeDisposed)] + public void TestConfiguration_CustomActionsContinuation_Async(ActionType type) + { + // Arrange + int count = 0; + Core.Configuration.AddCustomAction(type, async (scope, ct) => + { + count = 1; + return await Task.FromResult(true); + }); + + // To cover the methods per action type + switch (type) + { + case ActionType.OnScopeCreated: + Core.Configuration.AddOnCreatedAction(async (scope, ct) => + { + count++; + return await Task.FromResult(false); + }); + break; + case ActionType.OnEventSaving: + Core.Configuration.AddOnSavingAction(async (scope, ct) => + { + count++; + return await Task.FromResult(false); + }); + break; + case ActionType.OnScopeDisposed: + Core.Configuration.AddOnDisposedAction(async (scope, ct) => + { + count++; + return await Task.FromResult(false); + }); + break; + default: + Core.Configuration.AddCustomAction(type, async (scope, ct) => + { + count++; + return await Task.FromResult(false); + }); + break; + } + + Core.Configuration.AddCustomAction(type, async (scope, ct) => + { + count++; + return await Task.FromResult(false); + }); + Core.Configuration.AddCustomAction(type, async (scope, ct) => + { + count = -1; + return await Task.FromResult(true); + }); + + // Act + var scope = AuditScope.Create("test", null); + scope.Save(); + scope.Dispose(); + + // Assert + Assert.That(count, Is.EqualTo(2)); + } + + [TestCase(true)] + [TestCase(false)] + public async Task TestConfiguration_ExcludeEnvironmentInfo(bool exclude) + { + // Arrange + Core.Configuration.ExcludeEnvironmentInfo = exclude; + + // Act + var scope = await AuditScope.CreateAsync("test", null); + await scope.SaveAsync(); + + // Assert + Assert.That(scope.Event.Environment, exclude ? Is.Null : Is.Not.Null); + } + + [TestCase(true)] + [TestCase(false)] + public async Task TestConfiguration_ExcludeEnvironmentInfo_Option(bool exclude) + { + // Arrange + Core.Configuration.ExcludeEnvironmentInfo = !exclude; + + // Act + var scope = await AuditScope.CreateAsync(c => c.ExcludeEnvironmentInfo(exclude)); + await scope.SaveAsync(); + + // Assert + Assert.That(scope.Event.Environment, exclude ? Is.Null : Is.Not.Null); + } } } diff --git a/test/Audit.UnitTest/FileDataProviderTests.cs b/test/Audit.UnitTest/FileDataProviderTests.cs index 1501cb3e..546709ce 100644 --- a/test/Audit.UnitTest/FileDataProviderTests.cs +++ b/test/Audit.UnitTest/FileDataProviderTests.cs @@ -180,7 +180,7 @@ public void Test_FileDataProvider_Error() var guid = "x" + Guid.NewGuid().ToString(); try { - new AuditScopeFactory().Create(new AuditScopeOptions(guid, extraFields: loop, isCreateAndSave: true)); + new AuditScopeFactory().Create(new AuditScopeOptions() { EventType = guid, ExtraFields = loop, IsCreateAndSave = true }); Assert.Fail("Should not get here. JsonSettings not respected?"); } catch (Exception ex) @@ -212,7 +212,7 @@ public async Task Test_FileDataProvider_ErrorAsync() var guid = "x" + Guid.NewGuid().ToString(); try { - await new AuditScopeFactory().CreateAsync(new AuditScopeOptions(guid, extraFields: loop, isCreateAndSave: true)); + await new AuditScopeFactory().CreateAsync(new AuditScopeOptions() { EventType = guid, ExtraFields = loop, IsCreateAndSave = true }); Assert.Fail("Should not get here. JsonSettings not respected?"); } catch (Exception ex) diff --git a/test/Audit.UnitTest/MyClock.cs b/test/Audit.UnitTest/MyClock.cs index 19bcf70e..800b6d22 100644 --- a/test/Audit.UnitTest/MyClock.cs +++ b/test/Audit.UnitTest/MyClock.cs @@ -5,15 +5,13 @@ namespace Audit.UnitTest { public class MyClock : ISystemClock { - public DateTime _start = new DateTime(2020, 1, 1); - public DateTime UtcNow + private DateTime _start = new DateTime(2020, 1, 1); + + public DateTime GetCurrentDateTime() { - get - { - var dt = _start; - _start = _start.AddSeconds(10); - return dt; - } + var dt = _start; + _start = _start.AddSeconds(10); + return dt; } } } diff --git a/test/Audit.UnitTest/UdpProviderUnitTests.cs b/test/Audit.UnitTest/UdpProviderUnitTests.cs index df125fbc..f77b0278 100644 --- a/test/Audit.UnitTest/UdpProviderUnitTests.cs +++ b/test/Audit.UnitTest/UdpProviderUnitTests.cs @@ -64,7 +64,7 @@ public async Task Test_UdpDataProvider_BasicTest_Async(string ip, int port, bool var listener = Task.Factory.StartNew(() => { Listen(re, ip, port, multicast); }, cts.Token); re.WaitOne(); await Task.Delay(1000); - using (var scope = await new AuditScopeFactory().CreateAsync(new AuditScopeOptions("Test_UdpDataProvider_BasicTest", null, null, null, EventCreationPolicy.InsertOnStartReplaceOnEnd))) + using (var scope = await new AuditScopeFactory().CreateAsync(new AuditScopeOptions() { EventType = "Test_UdpDataProvider_BasicTest", CreationPolicy = EventCreationPolicy.InsertOnStartReplaceOnEnd })) { Task.Delay(100).Wait(); await scope.SaveAsync(); diff --git a/test/Audit.UnitTest/UnitTest.Async.cs b/test/Audit.UnitTest/UnitTest.Async.cs index 14e940ec..78d6a396 100644 --- a/test/Audit.UnitTest/UnitTest.Async.cs +++ b/test/Audit.UnitTest/UnitTest.Async.cs @@ -523,7 +523,7 @@ public async Task Test_ScopeActionsStress_Async() //do nothing, just bother var d = ev.Event.Duration * 1234567; }); - await factory.CreateAsync(new AuditScopeOptions("LoginFailed", extraFields: new { username = "adriano", id = i * -1 }, isCreateAndSave: true)); + await factory.CreateAsync(new AuditScopeOptions() { EventType = "LoginFailed", ExtraFields = new { username = "adriano", id = i * -1 }, IsCreateAndSave = true }); })); } await Task.WhenAll(tasks.ToArray()); @@ -567,7 +567,7 @@ public async Task Test_StartAndSave_Async() var eventType = "event type"; - await new AuditScopeFactory().CreateAsync(new AuditScopeOptions(eventType, extraFields: new { Extra1 = new { SubExtra1 = "test1" }, Extra2 = "test2" }, dataProvider: provider.Object, isCreateAndSave: true)); + await new AuditScopeFactory().CreateAsync(new AuditScopeOptions() { EventType = eventType, ExtraFields = new { Extra1 = new { SubExtra1 = "test1" }, Extra2 = "test2" }, DataProvider = provider.Object, IsCreateAndSave = true }); provider.Verify(p => p.InsertEventAsync(It.IsAny(), It.IsAny()), Times.Once); provider.Verify(p => p.ReplaceEvent(It.IsAny(), It.IsAny()), Times.Never); provider.Verify(p => p.ReplaceEventAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); @@ -821,7 +821,7 @@ public async Task Test_EventCreationPolicy_Manual_Async() public async Task Test_ExtraFields_Async() { Core.Configuration.DataProvider = new FileDataProvider(); - var scope = await new AuditScopeFactory().CreateAsync(new AuditScopeOptions("SomeEvent", null, new { @class = "class value", DATA = 123 }, null, EventCreationPolicy.Manual)); + var scope = await new AuditScopeFactory().CreateAsync(new AuditScopeOptions() { EventType = "SomeEvent", ExtraFields = new { @class = "class value", DATA = 123 }, CreationPolicy = EventCreationPolicy.Manual }); scope.Comment("test"); var ev = scope.Event; scope.Discard(); @@ -836,9 +836,9 @@ public async Task Test_TwoScopes_Async() provider.Setup(p => p.InsertEvent(It.IsAny())).Returns(() => Guid.NewGuid()); provider.Setup(p => p.InsertEventAsync(It.IsAny(), It.IsAny())).Returns(() => Task.FromResult((object)Guid.NewGuid())); Core.Configuration.DataProvider = provider.Object; - var scope1 = await new AuditScopeFactory().CreateAsync(new AuditScopeOptions("SomeEvent1", null, new { @class = "class value1", DATA = 111 }, null, EventCreationPolicy.Manual)); + var scope1 = await new AuditScopeFactory().CreateAsync(new AuditScopeOptions() { EventType = "SomeEvent1", ExtraFields = new { @class = "class value1", DATA = 111 }, CreationPolicy = EventCreationPolicy.Manual }); await scope1.SaveAsync(); - var scope2 = await new AuditScopeFactory().CreateAsync(new AuditScopeOptions("SomeEvent2", null, new { @class = "class value2", DATA = 222 }, null, EventCreationPolicy.Manual)); + var scope2 = await new AuditScopeFactory().CreateAsync(new AuditScopeOptions() { EventType = "SomeEvent2", ExtraFields = new { @class = "class value2", DATA = 222 }, CreationPolicy = EventCreationPolicy.Manual }); await scope2.SaveAsync(); Assert.NotNull(scope1.EventId); Assert.NotNull(scope2.EventId); diff --git a/test/Audit.UnitTest/UnitTest.cs b/test/Audit.UnitTest/UnitTest.cs index 947479f7..c574be3d 100644 --- a/test/Audit.UnitTest/UnitTest.cs +++ b/test/Audit.UnitTest/UnitTest.cs @@ -711,7 +711,7 @@ public void Test_ScopeActionsStress() //do nothing, just bother var d = ev.Event.Duration * 1234567; }); - factory.Create(new AuditScopeOptions("LoginFailed", null, new { username = "adriano", id = i * -1 }, null, null, true)); + factory.Create(new AuditScopeOptions() { EventType = "LoginFailed", ExtraFields = new { username = "adriano", id = i * -1 }, IsCreateAndSave = true }); })); } Task.WaitAll(tasks.ToArray()); @@ -811,7 +811,7 @@ public void Test_StartAndSave() var eventType = "event type"; - new AuditScopeFactory().Create(new AuditScopeOptions(eventType, null, new { Extra1 = new { SubExtra1 = "test1" }, Extra2 = "test2" }, provider.Object, null, true)); + new AuditScopeFactory().Create(new AuditScopeOptions() { EventType = eventType, ExtraFields = new { Extra1 = new { SubExtra1 = "test1" }, Extra2 = "test2" }, DataProvider = provider.Object, IsCreateAndSave = true }); provider.Verify(p => p.InsertEvent(It.IsAny()), Times.Once); provider.Verify(p => p.ReplaceEvent(It.IsAny(), It.IsAny()), Times.Never); } @@ -1071,7 +1071,7 @@ public void Test_EventCreationPolicy_Manual() public void Test_ExtraFields() { Core.Configuration.DataProvider = new FileDataProvider(); - var scope = new AuditScopeFactory().Create(new AuditScopeOptions("SomeEvent", null, new { @class = "class value", DATA = 123 }, null, EventCreationPolicy.Manual)); + var scope = new AuditScopeFactory().Create(new AuditScopeOptions() { EventType = "SomeEvent", ExtraFields = new { @class = "class value", DATA = 123 }, CreationPolicy = EventCreationPolicy.Manual }); scope.Comment("test"); var ev = scope.Event; scope.Discard(); @@ -1085,9 +1085,9 @@ public void Test_TwoScopes() var provider = new Mock(); provider.Setup(p => p.InsertEvent(It.IsAny())).Returns(() => Guid.NewGuid()); Core.Configuration.DataProvider = provider.Object; - var scope1 = new AuditScopeFactory().Create(new AuditScopeOptions("SomeEvent1", null, new { @class = "class value1", DATA = 111 }, null, EventCreationPolicy.Manual)); + var scope1 = new AuditScopeFactory().Create(new AuditScopeOptions() { EventType = "SomeEvent1", ExtraFields = new { @class = "class value1", DATA = 111 }, CreationPolicy = EventCreationPolicy.Manual }); scope1.Save(); - var scope2 = new AuditScopeFactory().Create(new AuditScopeOptions("SomeEvent2", null, new { @class = "class value2", DATA = 222 }, null, EventCreationPolicy.Manual)); + var scope2 = new AuditScopeFactory().Create(new AuditScopeOptions() { EventType = "SomeEvent2", ExtraFields = new { @class = "class value2", DATA = 222 }, CreationPolicy = EventCreationPolicy.Manual }); scope2.Save(); Assert.NotNull(scope1.EventId); Assert.NotNull(scope2.EventId); diff --git a/test/Audit.WebApi.UnitTest/MiddlewareTests.cs b/test/Audit.WebApi.UnitTest/MiddlewareTests.cs index 5a2bada3..1bc866a7 100644 --- a/test/Audit.WebApi.UnitTest/MiddlewareTests.cs +++ b/test/Audit.WebApi.UnitTest/MiddlewareTests.cs @@ -1,6 +1,7 @@ #if NETCOREAPP3_1 || NET6_0 using System.Net; using System.Threading.Tasks; +using Audit.Core; using Audit.Core.Providers; using Microsoft.AspNetCore.Mvc; using NUnit.Framework; @@ -53,7 +54,7 @@ public async Task Test_IncludeResponseBody(bool? includeResponseBody, bool? skip cfg.SkipResponseBodyContent(skipResponseBody!.Value); } } - }); + }, new CustomAuditScopeFactory()); using var client = app.CreateClient(); @@ -64,9 +65,31 @@ public async Task Test_IncludeResponseBody(bool? includeResponseBody, bool? skip Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); Assert.That(events, Has.Count.EqualTo(1)); + Assert.That(events[0].CustomFields, Contains.Key("TestField")); + Assert.That(events[0].CustomFields["TestField"].ToString(), Is.EqualTo("FromOnConfiguring")); + Assert.That(events[0].CustomFields, Contains.Key("TestField2")); + Assert.That(events[0].CustomFields["TestField2"].ToString(), Is.EqualTo("FromOnScopeCreated")); Assert.That(events[0].GetWebApiAuditAction().ResponseBody, expectNullResponseBody ? Is.Null : Is.Not.Null); Assert.That(events[0].GetWebApiAuditAction().ResponseBody?.Value, expectNullResponseBodyContent ? Is.Null : Is.Not.Null); } } + + public class CustomAuditScopeFactory : AuditScopeFactory + { + public override void OnConfiguring(AuditScopeOptions options) + { + options.Items.Add("TestItem", "TestValue"); + options.ExtraFields = new { TestField = "FromOnConfiguring" }; + } + + public override void OnScopeCreated(AuditScope auditScope) + { + if (auditScope.Items["TestItem"].ToString() != "TestValue") + { + Assert.Fail("TestItem not found"); + } + auditScope.SetCustomField("TestField2", "FromOnScopeCreated"); + } + } } #endif \ No newline at end of file diff --git a/test/Audit.WebApi.UnitTest/TestHelper.cs b/test/Audit.WebApi.UnitTest/TestHelper.cs index a5ca05f0..2d790a9d 100644 --- a/test/Audit.WebApi.UnitTest/TestHelper.cs +++ b/test/Audit.WebApi.UnitTest/TestHelper.cs @@ -12,12 +12,17 @@ namespace Audit.WebApi.UnitTest { public class TestHelper { - public static TestServer GetTestServer(AuditDataProvider dataProvider, Action middlewareConfig) + public static TestServer GetTestServer(AuditDataProvider dataProvider, Action middlewareConfig, + IAuditScopeFactory scopeFactory = null) { return new TestServer(WebHost.CreateDefaultBuilder() .ConfigureServices(services => { services.AddSingleton(dataProvider); + if (scopeFactory != null) + { + services.AddSingleton(scopeFactory); + } services.AddControllers(); }) .Configure((ctx, app) => From 458aff2a506de00637c1af56712622797706ebee Mon Sep 17 00:00:00 2001 From: thepirat000 Date: Tue, 3 Sep 2024 11:45:07 -0600 Subject: [PATCH 2/4] adding comment --- src/Audit.EntityFramework/DbContextHelper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Audit.EntityFramework/DbContextHelper.cs b/src/Audit.EntityFramework/DbContextHelper.cs index 362c746f..269cc157 100644 --- a/src/Audit.EntityFramework/DbContextHelper.cs +++ b/src/Audit.EntityFramework/DbContextHelper.cs @@ -392,6 +392,7 @@ private T TryGetService(DbContext dbContext) where T : class { var infrastructure = dbContext?.GetInfrastructure(); + // Based on EF Core code from: https://github.com/dotnet/efcore/blob/ecfee78eb1fa2b2eaa0dbf945f1d4f8fa571be74/src/EFCore/Infrastructure/Internal/InfrastructureExtensions.cs#L32 var service = infrastructure?.GetService(typeof(T)) ?? infrastructure?.GetService()?.Extensions.OfType() From f9ff6377d98e4f37a52fbfba63a2042f4012dacb Mon Sep 17 00:00:00 2001 From: thepirat000 Date: Tue, 3 Sep 2024 11:53:53 -0600 Subject: [PATCH 3/4] Adding create methods to IAuditScopeFactory --- src/Audit.NET/AuditScopeFactory.cs | 15 ++++----------- src/Audit.NET/IAuditScopeFactory.cs | 13 +++++++++++++ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Audit.NET/AuditScopeFactory.cs b/src/Audit.NET/AuditScopeFactory.cs index 84a1b2d5..ef38ac97 100644 --- a/src/Audit.NET/AuditScopeFactory.cs +++ b/src/Audit.NET/AuditScopeFactory.cs @@ -49,12 +49,7 @@ public async Task CreateAsync(AuditScopeOptions options, Cancellati return await auditScope.StartAsync(cancellationToken); } - #endregion - - /// - /// Creates an audit scope with the given creation options as a Fluent API. - /// - /// Fluent API to configure the Audit Scope creation options + /// [MethodImpl(MethodImplOptions.NoInlining)] public IAuditScope Create(Action config) { @@ -65,11 +60,7 @@ public IAuditScope Create(Action config) return auditScope.Start(); } - /// - /// Creates an audit scope with the given creation options as a Fluent API. - /// - /// Fluent API to configure the Audit Scope creation options - /// The Cancellation Token. + /// [MethodImpl(MethodImplOptions.NoInlining)] public async Task CreateAsync(Action config, CancellationToken cancellationToken = default) { @@ -80,6 +71,8 @@ public async Task CreateAsync(Action /// Creates an audit scope for a target object and an event type. /// diff --git a/src/Audit.NET/IAuditScopeFactory.cs b/src/Audit.NET/IAuditScopeFactory.cs index 133495fa..cc621efa 100644 --- a/src/Audit.NET/IAuditScopeFactory.cs +++ b/src/Audit.NET/IAuditScopeFactory.cs @@ -17,5 +17,18 @@ public interface IAuditScopeFactory /// The Audit Scope creation options /// The Cancellation Token. Task CreateAsync(AuditScopeOptions options, CancellationToken cancellationToken = default); + + /// + /// Creates an audit scope with the given creation options as a Fluent API. + /// + /// Fluent API to configure the Audit Scope creation options + IAuditScope Create(Action config); + + /// + /// Creates an audit scope with the given creation options as a Fluent API. + /// + /// Fluent API to configure the Audit Scope creation options + /// The Cancellation Token. + Task CreateAsync(Action config, CancellationToken cancellationToken = default); } } \ No newline at end of file From d1150b8f2820c7f186b2c577bec0a950307c94a1 Mon Sep 17 00:00:00 2001 From: thepirat000 Date: Tue, 3 Sep 2024 18:31:07 +0000 Subject: [PATCH 4/4] Commit CODE_COVERAGE.md --- CODE_COVERAGE.md | 92 ++++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/CODE_COVERAGE.md b/CODE_COVERAGE.md index 70891c4d..0d6a9cf9 100644 --- a/CODE_COVERAGE.md +++ b/CODE_COVERAGE.md @@ -1,19 +1,19 @@ # Summary ||| |:---|:---| -| Generated on: | 8/22/2024 - 1:10:30 AM | +| Generated on: | 9/3/2024 - 6:30:57 PM | | Parser: | MultiReport (2x Cobertura) | | Assemblies: | 23 | | Classes: | 261 | | Files: | 199 | -| **Line coverage:** | 76.9% (7740 of 10059) | -| Covered lines: | 7740 | -| Uncovered lines: | 2319 | -| Coverable lines: | 10059 | -| Total lines: | 18468 | -| **Branch coverage:** | 70.8% (2983 of 4210) | -| Covered branches: | 2983 | -| Total branches: | 4210 | +| **Line coverage:** | 76.8% (7872 of 10240) | +| Covered lines: | 7872 | +| Uncovered lines: | 2368 | +| Coverable lines: | 10240 | +| Total lines: | 18833 | +| **Branch coverage:** | 70.6% (3015 of 4268) | +| Covered branches: | 3015 | +| Total branches: | 4268 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | |**Name**|**Covered**|**Uncovered**|**Coverable**|**Total**|**Line coverage**|**Covered**|**Total**|**Branch coverage**| @@ -27,7 +27,7 @@ |Audit.DynamicProxy.AuditProxy|11|0|11|59|100%|4|4|100%| |Audit.DynamicProxy.InterceptEvent|12|0|12|59|100%|0|0|| |Audit.DynamicProxy.InterceptionSettings|7|0|7|48|100%|0|0|| -|**Audit.EntityFramework**|**1765**|**575**|**2340**|**5214**|**75.4%**|**642**|**854**|**75.1%**| +|**Audit.EntityFramework**|**1769**|**577**|**2346**|**5257**|**75.4%**|**644**|**856**|**75.2%**| |Audit.Core.EntityFrameworkConfiguratorExtensions|15|15|30|72|50%|0|2|0%| |Audit.EntityFramework.AssociationEntry|0|3|3|18|0%|0|0|| |Audit.EntityFramework.AssociationEntryRecord|0|5|5|19|0%|0|0|| @@ -55,15 +55,15 @@ |Audit.EntityFramework.ConfigurationApi.IncludeConfigurator|2|4|6|23|33.3%|0|0|| |Audit.EntityFramework.ConfigurationApi.MappingInfo|4|0|4|31|100%|0|0|| |Audit.EntityFramework.ConfigurationApi.ModeConfigurator|6|0|6|21|100%|0|0|| -|Audit.EntityFramework.DbContextHelper|306|114|420|933|72.8%|296|390|75.8%| -|Audit.EntityFramework.DefaultAuditContext|20|2|22|45|90.9%|0|0|| +|Audit.EntityFramework.DbContextHelper|312|114|426|976|73.2%|298|392|76%| +|Audit.EntityFramework.DefaultAuditContext|18|4|22|45|81.8%|0|0|| |Audit.EntityFramework.EntityFrameworkEvent|11|3|14|81|78.5%|0|0|| |Audit.EntityFramework.EntityKeyHelper|114|0|114|295|100%|80|84|95.2%| |Audit.EntityFramework.EntityName|2|0|2|11|100%|0|0|| |Audit.EntityFramework.EventEntry|12|4|16|68|75%|0|0|| |Audit.EntityFramework.EventEntryChange|3|0|3|9|100%|0|0|| |Audit.EntityFramework.Providers.EntityFrameworkDataProvider|115|17|132|379|87.1%|81|102|79.4%| -|**Audit.EntityFramework.Core**|**1837**|**817**|**2654**|**6047**|**69.2%**|**806**|**1044**|**77.2%**| +|**Audit.EntityFramework.Core**|**1851**|**819**|**2670**|**6101**|**69.3%**|**796**|**1040**|**76.5%**| |Audit.Core.EntityFrameworkConfiguratorExtensions|15|15|30|72|50%|0|2|0%| |Audit.EntityFramework.AuditDbContext|33|0|33|249|100%|0|0|| |Audit.EntityFramework.AuditDbContextAttribute|9|10|19|124|47.3%|0|8|0%| @@ -93,15 +93,15 @@ |Audit.EntityFramework.ConfigurationApi.IncludeConfigurator|2|4|6|23|33.3%|0|0|| |Audit.EntityFramework.ConfigurationApi.MappingInfo|4|0|4|31|100%|0|0|| |Audit.EntityFramework.ConfigurationApi.ModeConfigurator|6|0|6|21|100%|0|0|| -|Audit.EntityFramework.DbContextHelper|411|31|442|1045|92.9%|342|404|84.6%| -|Audit.EntityFramework.DefaultAuditContext|19|2|21|45|90.4%|0|0|| +|Audit.EntityFramework.DbContextHelper|423|31|454|1088|93.1%|356|424|83.9%| +|Audit.EntityFramework.DefaultAuditContext|17|4|21|45|80.9%|0|0|| |Audit.EntityFramework.EntityFrameworkEvent|13|1|14|81|92.8%|0|0|| |Audit.EntityFramework.EntityName|2|0|2|11|100%|0|0|| |Audit.EntityFramework.EventEntry|15|1|16|68|93.7%|0|0|| |Audit.EntityFramework.EventEntryChange|3|0|3|9|100%|0|0|| |Audit.EntityFramework.InterceptorEventBase|8|0|8|56|100%|0|0|| -|Audit.EntityFramework.Interceptors.AuditCommandInterceptor|168|38|206|443|81.5%|117|150|78%| -|Audit.EntityFramework.Interceptors.AuditTransactionInterceptor|154|29|183|385|84.1%|69|98|70.4%| +|Audit.EntityFramework.Interceptors.AuditCommandInterceptor|170|38|208|449|81.7%|105|138|76%| +|Audit.EntityFramework.Interceptors.AuditTransactionInterceptor|156|29|185|390|84.3%|57|86|66.2%| |Audit.EntityFramework.Providers.EntityFrameworkDataProvider|118|9|127|379|92.9%|95|104|91.3%| |Audit.EntityFramework.TransactionEvent|4|1|5|32|80%|0|0|| |**Audit.EntityFramework.Identity.Core**|**27**|**32**|**59**|**1163**|**45.7%**|**0**|**0**|****| @@ -145,16 +145,16 @@ |Audit.Mvc.AuditHelper|6|6|12|42|50%|7|18|38.8%| |Audit.Mvc.BodyContent|3|0|3|9|100%|0|0|| |Audit.Mvc.ControllerExtensions|0|2|2|31|0%|0|0|| -|**Audit.Mvc.Core**|**319**|**59**|**378**|**890**|**84.3%**|**190**|**306**|**62%**| +|**Audit.Mvc.Core**|**325**|**59**|**384**|**904**|**84.6%**|**202**|**322**|**62.7%**| |Audit.Mvc.AuditAction|23|3|26|65|88.4%|0|0|| -|Audit.Mvc.AuditAttribute|163|18|181|376|90%|104|148|70.2%| +|Audit.Mvc.AuditAttribute|166|18|184|381|90.2%|110|154|71.4%| |Audit.Mvc.AuditEventExtensions|2|4|6|32|33.3%|1|6|16.6%| |Audit.Mvc.AuditEventMvcAction|1|0|1|16|100%|0|0|| |Audit.Mvc.AuditHelper|14|4|18|57|77.7%|14|22|63.6%| -|Audit.Mvc.AuditPageFilter|112|28|140|295|80%|71|130|54.6%| +|Audit.Mvc.AuditPageFilter|115|28|143|304|80.4%|77|140|55%| |Audit.Mvc.BodyContent|3|0|3|9|100%|0|0|| |Audit.Mvc.ControllerExtensions|1|2|3|40|33.3%|0|0|| -|**Audit.NET**|**1136**|**92**|**1228**|**4517**|**92.5%**|**268**|**342**|**78.3%**| +|**Audit.NET**|**1240**|**135**|**1375**|**4786**|**90.1%**|**284**|**360**|**78.8%**| |Audit.Core.AuditActivityEvent|2|0|2|80|100%|0|0|| |Audit.Core.AuditActivityTag|2|0|2|80|100%|0|0|| |Audit.Core.AuditActivityTrace|10|0|10|80|100%|0|0|| @@ -162,33 +162,33 @@ |Audit.Core.AuditDataProvider|16|0|16|118|100%|4|4|100%| |Audit.Core.AuditEvent|16|0|16|109|100%|0|0|| |Audit.Core.AuditEventEnvironment|11|0|11|74|100%|0|0|| -|Audit.Core.AuditScope|247|8|255|668|96.8%|137|154|88.9%| -|Audit.Core.AuditScopeFactory|25|0|25|158|100%|0|0|| -|Audit.Core.AuditScopeOptions|57|0|57|157|100%|9|12|75%| -|Audit.Core.AuditScopeOptionsConfigurator|23|2|25|84|92%|0|0|| +|Audit.Core.AuditScope|257|8|265|663|96.9%|155|174|89%| +|Audit.Core.AuditScopeFactory|67|0|67|209|100%|0|0|| +|Audit.Core.AuditScopeOptions|38|0|38|133|100%|2|2|100%| +|Audit.Core.AuditScopeOptionsConfigurator|28|5|33|107|84.8%|1|2|50%| |Audit.Core.AuditTarget|3|0|3|26|100%|0|0|| -|Audit.Core.Configuration|150|2|152|389|98.6%|4|6|66.6%| +|Audit.Core.Configuration|217|42|259|604|83.7%|8|10|80%| |Audit.Core.ConfigurationApi.ActionConfigurator|5|0|5|19|100%|0|0|| |Audit.Core.ConfigurationApi.ActionEventSelector|14|2|16|48|87.5%|0|0|| |Audit.Core.ConfigurationApi.BlockingCollectionProviderConfigurator|10|0|10|32|100%|0|0|| |Audit.Core.ConfigurationApi.BlockingCollectionProviderExtraConfigurator|2|0|2|13|100%|0|0|| |Audit.Core.ConfigurationApi.ConditionalDataProviderConfigurator|25|12|37|77|67.5%|2|6|33.3%| -|Audit.Core.ConfigurationApi.Configurator|38|15|53|144|71.6%|0|0|| +|Audit.Core.ConfigurationApi.Configurator|41|15|56|153|73.2%|0|0|| |Audit.Core.ConfigurationApi.CreationPolicyConfigurator|10|0|10|35|100%|0|0|| |Audit.Core.ConfigurationApi.DynamicAsyncDataProviderConfigurator|21|2|23|79|91.3%|0|0|| |Audit.Core.ConfigurationApi.DynamicDataProviderConfigurator|11|2|13|47|84.6%|0|0|| |Audit.Core.ConfigurationApi.EventLogProviderConfigurator|9|8|17|54|52.9%|0|0|| |Audit.Core.ConfigurationApi.FileLogProviderConfigurator|10|0|10|35|100%|0|0|| -|Audit.Core.DefaultSystemClock|1|0|1|12|100%|0|0|| +|Audit.Core.DefaultSystemClock|1|0|1|16|100%|0|0|| |Audit.Core.Extensions.ExceptionExtensions|3|0|3|25|100%|2|2|100%| -|Audit.Core.Extensions.TypeExtensions|17|1|18|47|94.4%|9|10|90%| +|Audit.Core.Extensions.TypeExtensions|13|1|14|43|92.8%|9|10|90%| |Audit.Core.JsonAdapter|11|3|14|53|78.5%|4|8|50%| |Audit.Core.JsonAdapter|11|3|14|53|78.5%|4|8|50%| |Audit.Core.PlatformHelper|5|0|5|16|100%|0|0|| |Audit.Core.Providers.BlockingCollectionDataProvider|37|2|39|169|94.8%|17|22|77.2%| |Audit.Core.Providers.DynamicAsyncDataProvider|41|6|47|169|87.2%|6|6|100%| |Audit.Core.Providers.DynamicDataProvider|28|3|31|106|90.3%|6|6|100%| -|Audit.Core.Providers.EventLogDataProvider|13|14|27|82|48.1%|2|8|25%| +|Audit.Core.Providers.EventLogDataProvider|13|14|27|82|48.1%|2|10|20%| |Audit.Core.Providers.FileDataProvider|56|0|56|153|100%|12|12|100%| |Audit.Core.Providers.FileDataProvider.HiResDateTime|7|0|7|153|100%|2|2|100%| |Audit.Core.Providers.FileDataProvider|56|0|56|153|100%|12|12|100%| @@ -208,14 +208,14 @@ |Audit.Core.JsonNewtonsoftAdapter|17|17|34|91|50%|2|10|20%| |Audit.Core.JsonNewtonsoftAdapter|17|17|34|91|50%|2|10|20%| |Audit.JsonNewtonsoftAdapter.AuditContractResolver|57|1|58|102|98.2%|44|46|95.6%| -|**Audit.NET.log4net**|**39**|**15**|**54**|**195**|**72.2%**|**11**|**18**|**61.1%**| +|**Audit.NET.log4net**|**39**|**15**|**54**|**195**|**72.2%**|**11**|**20**|**55%**| |Audit.Core.Log4netConfiguratorExtensions|2|2|4|32|50%|0|0|| |Audit.log4net.Configuration.Log4netConfigurator|6|6|12|47|50%|0|0|| -|Audit.log4net.Providers.Log4netDataProvider|31|7|38|116|81.5%|11|18|61.1%| -|**Audit.NET.NLog**|**39**|**15**|**54**|**193**|**72.2%**|**11**|**18**|**61.1%**| +|Audit.log4net.Providers.Log4netDataProvider|31|7|38|116|81.5%|11|20|55%| +|**Audit.NET.NLog**|**39**|**15**|**54**|**193**|**72.2%**|**11**|**20**|**55%**| |Audit.Core.NLogConfiguratorExtensions|2|2|4|30|50%|0|0|| |Audit.NLog.Configuration.NLogConfigurator|6|6|12|47|50%|0|0|| -|Audit.NLog.Providers.NLogDataProvider|31|7|38|116|81.5%|11|18|61.1%| +|Audit.NLog.Providers.NLogDataProvider|31|7|38|116|81.5%|11|20|55%| |**Audit.NET.Polly**|**63**|**24**|**87**|**317**|**72.4%**|**4**|**6**|**66.6%**| |Audit.Core.PollyConfiguratorExtensions|2|0|2|19|100%|0|0|| |Audit.Polly.Configuration.PollyProviderConfigurator|5|0|5|19|100%|0|0|| @@ -224,10 +224,10 @@ |Audit.Polly.HedgingActionGeneratorArgumentsExtensions|7|10|17|65|41.1%|1|2|50%| |Audit.Polly.Providers.PollyDataProvider|30|12|42|120|71.4%|2|2|100%| |Audit.Polly.ResilienceContextExtensions|1|0|1|16|100%|0|0|| -|**Audit.NET.Serilog**|**36**|**20**|**56**|**223**|**64.2%**|**8**|**18**|**44.4%**| +|**Audit.NET.Serilog**|**36**|**20**|**56**|**223**|**64.2%**|**9**|**20**|**45%**| |Audit.Core.SerilogConfiguratorExtensions|2|2|4|37|50%|0|0|| |Audit.Serilog.Configuration.SerilogConfigurator|6|6|12|53|50%|0|0|| -|Audit.Serilog.Providers.SerilogDataProvider|28|12|40|133|70%|8|18|44.4%| +|Audit.Serilog.Providers.SerilogDataProvider|28|12|40|133|70%|9|20|45%| |**Audit.NET.SqlServer**|**286**|**62**|**348**|**886**|**82.1%**|**134**|**154**|**87%**| |Audit.Core.SqlServerConfiguratorExtensions|2|0|2|23|100%|0|0|| |Audit.SqlServer.AuditContext|10|0|10|34|100%|2|2|100%| @@ -240,9 +240,9 @@ |Audit.Core.UdpProviderConfiguratorExtensions|13|0|13|48|100%|0|0|| |Audit.Udp.Configuration.UdpProviderConfigurator|8|8|16|64|50%|4|4|100%| |Audit.Udp.Providers.UdpDataProvider|60|2|62|196|96.7%|14|16|87.5%| -|**Audit.SignalR**|**341**|**100**|**441**|**1106**|**77.3%**|**143**|**294**|**48.6%**| +|**Audit.SignalR**|**341**|**102**|**443**|**1125**|**76.9%**|**144**|**302**|**47.6%**| |Audit.SignalR.AuditEventSignalr|1|0|1|15|100%|0|0|| -|Audit.SignalR.AuditHubFilter|99|9|108|196|91.6%|70|100|70%| +|Audit.SignalR.AuditHubFilter|99|9|108|196|91.6%|71|104|68.2%| |Audit.SignalR.AuditPipelineModule|159|48|207|348|76.8%|68|174|39%| |Audit.SignalR.Configuration.AuditHubConfigurator|11|4|15|58|73.3%|0|0|| |Audit.SignalR.Configuration.AuditHubFilterConfigurator|6|18|24|90|25%|0|0|| @@ -253,7 +253,7 @@ |Audit.SignalR.SignalrEventIncoming|13|1|14|49|92.8%|0|0|| |Audit.SignalR.SignalrEventOutgoing|7|0|7|25|100%|0|0|| |Audit.SignalR.SignalrEventReconnect|7|0|7|25|100%|0|0|| -|Audit.SignalR.SignalrExtensions|8|18|26|150|30.7%|5|20|25%| +|Audit.SignalR.SignalrExtensions|8|20|28|169|28.5%|5|24|20.8%| |**Audit.WCF**|**148**|**77**|**225**|**624**|**65.7%**|**21**|**62**|**33.8%**| |Audit.WCF.AuditBehavior|2|6|8|72|25%|0|0|| |Audit.WCF.AuditBehaviorAttribute|9|11|20|90|45%|0|10|0%| @@ -266,17 +266,17 @@ |Audit.WCF.AuditWcfEventFault|0|6|6|15|0%|0|0|| |Audit.WCF.WcfEvent|15|0|15|26|100%|0|0|| |Audit.WCF.WcfOperationContext|0|11|11|58|0%|0|2|0%| -|**Audit.Wcf.Client**|**75**|**23**|**98**|**318**|**76.5%**|**28**|**48**|**58.3%**| +|**Audit.Wcf.Client**|**75**|**23**|**98**|**319**|**76.5%**|**28**|**48**|**58.3%**| |Audit.Wcf.Client.AuditBehavior|0|11|11|71|0%|0|0|| |Audit.Wcf.Client.AuditEndpointBehavior|9|6|15|48|60%|0|0|| |Audit.Wcf.Client.AuditEventExtensions|4|2|6|31|66.6%|2|6|33.3%| |Audit.Wcf.Client.AuditEventWcfClient|1|0|1|12|100%|0|0|| -|Audit.Wcf.Client.AuditMessageInspector|51|4|55|106|92.7%|26|42|61.9%| +|Audit.Wcf.Client.AuditMessageInspector|51|4|55|107|92.7%|26|42|61.9%| |Audit.Wcf.Client.WcfClientAction|10|0|10|50|100%|0|0|| -|**Audit.WebApi**|**187**|**143**|**330**|**1191**|**56.6%**|**99**|**222**|**44.5%**| +|**Audit.WebApi**|**189**|**143**|**332**|**1193**|**56.9%**|**102**|**226**|**45.1%**| |Audit.WebApi.ApiControllerExtensions|1|3|4|92|25%|0|0|| |Audit.WebApi.AuditApiAction|21|2|23|74|91.3%|0|0|| -|Audit.WebApi.AuditApiAdapter|96|29|125|247|76.8%|56|102|54.9%| +|Audit.WebApi.AuditApiAdapter|98|29|127|249|77.1%|59|106|55.6%| |Audit.WebApi.AuditApiAttribute|30|3|33|133|90.9%|16|22|72.7%| |Audit.WebApi.AuditApiGlobalFilter|0|39|39|140|0%|0|32|0%| |Audit.WebApi.AuditApiHelper|8|4|12|139|66.6%|9|18|50%| @@ -286,17 +286,17 @@ |Audit.WebApi.ConfigurationApi.AuditApiGlobalActionsSelector|0|22|22|64|0%|0|4|0%| |Audit.WebApi.ConfigurationApi.AuditApiGlobalConfigurator|0|26|26|109|0%|0|0|| |Audit.WebApi.ContextWrapper|25|11|36|136|69.4%|17|38|44.7%| -|**Audit.WebApi.Core**|**405**|**82**|**487**|**1439**|**83.1%**|**209**|**286**|**73%**| +|**Audit.WebApi.Core**|**407**|**82**|**489**|**1445**|**83.2%**|**216**|**294**|**73.4%**| |Audit.WebApi.ApiControllerExtensions|1|5|6|92|16.6%|0|0|| |Audit.WebApi.AuditApiAction|22|2|24|74|91.6%|0|0|| -|Audit.WebApi.AuditApiAdapter|115|27|142|305|80.9%|87|132|65.9%| +|Audit.WebApi.AuditApiAdapter|116|27|143|308|81.1%|91|136|66.9%| |Audit.WebApi.AuditApiAttribute|35|2|37|130|94.5%|16|18|88.8%| |Audit.WebApi.AuditApiGlobalFilter|32|1|33|140|96.9%|20|22|90.9%| |Audit.WebApi.AuditApiHelper|47|5|52|139|90.3%|32|44|72.7%| |Audit.WebApi.AuditEventExtensions|2|4|6|32|33.3%|1|6|16.6%| |Audit.WebApi.AuditEventWebApi|1|0|1|16|100%|0|0|| |Audit.WebApi.AuditIgnoreActionFilter|4|0|4|23|100%|0|0|| -|Audit.WebApi.AuditMiddleware|96|2|98|178|97.9%|44|50|88%| +|Audit.WebApi.AuditMiddleware|97|2|99|181|97.9%|47|54|87%| |Audit.WebApi.AuditMiddlewareExtensions|3|0|3|24|100%|0|0|| |Audit.WebApi.BodyContent|3|0|3|9|100%|0|0|| |Audit.WebApi.ConfigurationApi.AuditApiGlobalActionsSelector|10|12|22|64|45.4%|1|6|16.6%|