Audit.NET 24.0.0
Audit.NET
USAGE | OUTPUT | CUSTOMIZATION | DATA PROVIDERS | CREATION POLICY | CONFIGURATION | EXTENSIONS
issues | build status | chat / support | donations |
---|---|---|---|
An extensible framework to audit executing operations in .NET and .NET Core.
Generate audit logs with evidence for reconstruction and examination of activities that have affected specific operations or procedures.
With Audit.NET you can generate tracking information about operations being executed. It gathers environmental information such as the caller user ID, machine name, method name, and exceptions, including execution time and exposing an extensible mechanism to enrich the logs and handle the audit output.
Output extensions are provided to log to JSON Files, Event Log, SQL, MySQL, PostgreSQL, RavenDB, MongoDB, AzureBlob, AzureTables, AzureCosmos, Redis, Elasticsearch, DynamoDB, UDP datagrams and more.
Interaction extensions to audit different systems are provided, such as Entity Framework, MVC, WebAPI, WCF, File System, SignalR, MongoClient and HttpClient.
NuGet
To install the package run the following command on the Package Manager Console:
PM> Install-Package Audit.NET
Changelog
Check the CHANGELOG.md file.
Introduction
The Audit Scope and Audit Event are the central objects of this framework.
Audit Scope
The AuditScope
serves as the central object in this framework, representing the scope of an audited operation or event.
It acts as a context for auditing, capturing pertinent details like the start time, involved entities, and any additional
custom information. Essentially, the AuditScope
encapsulates an AuditEvent
, controlling its life cycle.
The AuditScope
is a disposable object, commonly utilized within a using statement to ensure proper finalization and recording of audit information upon exiting the scope.
See the audit scope statechart.
Audit Event
The AuditEvent
functions as an extensible information container that captures the details of the audited operation,
is the representation of the audited information within an Audit Scope.
It includes details about the audited operation, such as the event type, timestamp, execution duration,
and any custom fields or properties.
The AuditEvent
is typically serialized into a format suitable for storage or transmission, such as JSON.
The audit events are stored using a Data Provider. You can use one of the available data providers or implement your own.
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).
For reference, please consult the following links:
This library and its extensions will maintain support for the following minimum .NET framework versions:
- .NET Framework 4.6.2 (net462)
- .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
andAudit.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 anAuditScopeFactory
instance, for example:var factory = new AuditScopeFactory(); var scope = factory.Create(new AuditScopeOptions(...));
Using the overloads of the static methods
Create()
/CreateAsync()
onAuditScope
, for example: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 aFunc<object>
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:var scope = AuditScope.Create(new AuditScopeOptions() { EventType = "Order:Update", TargetGetter = () => order, ExtraFields = new { MyProperty = "value" } });
Using the provided fluent API, for example:
var scope = AuditScope.Create(_ => _ .EventType("Order:Update") .ExtraFields(new { MyProperty = "value" }) .Target(() => order));
AuditScope options
Option | Type | Description |
---|---|---|
EventType | string |
A string representing the type of the event |
TargetGetter | Func<object> |
Target object getter (a func that returns the object to track) |
ExtraFields | object |
Anonymous object that contains additional fields to be merged into the audit event |
DataProvider | AuditDataProvider |
The data provider to use. Defaults to the DataProvider configured on Audit.Core.Configuration.DataProvider |
CreationPolicy | EventCreationPolicy |
The creation policy to use. Default is InsertOnEnd |
IsCreateAndSave | bool |
Value indicating whether this scope should be immediately ended and saved after creation. Default is false |
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. |
Suppose you have the following code to cancel an order that you want to audit:
Order order = Db.GetOrder(orderId);
order.Status = -1;
order.OrderItems = null;
order = Db.OrderUpdate(order);
To audit this operation, you can surround the code with a using
block that creates an AuditScope
, indicating a target object to track:
Order order = Db.GetOrder(orderId);
using (AuditScope.Create("Order:Update", () => order))
{
order.Status = -1;
order.OrderItems = null;
order = Db.OrderUpdate(order);
}
Note
It is not mandatory to use a
using
block, but it simplifies the syntax when the code to audit is on a single block, allowing the detection of exceptions and calculating the duration by implicitly saving the event on disposal.
Note
When using the extensions that logs interactions with different systems, like Audit.EntityFramework, Audit.WebApi, etc. you don't need to explicitly create the
AuditScope
orAuditEvent
, they are created internally by the extension.
Simple logging
If you are not tracking an object or the duration of an event, you can use the Log
shortcut method that logs an event immediately.
For example:
AuditScope.Log("Event Type", new { ExtraField = "extra value" });
Manual Saving
You can control the creation and saving logic, by creating a manual AuditScope
. For example to log a pair of Start
/End
method calls as a single event:
public class SomethingThatStartsAndEnds
{
private AuditScope auditScope;
public int Status { get; set; }
public void Start()
{
// Create a manual scope
auditScope = AuditScope.Create(new AuditScopeOptions()
{
EventType = "MyEvent",
TargetGetter = () => this.Status,
CreationPolicy = EventCreationPolicy.Manual
});
}
public void End()
{
// Save the event
auditScope.Save();
// Discard to avoid further saving
auditScope.Discard();
}
}
For more information about the EventCreationPolicy
please see Event Creation Policy section.
Asynchronous operations
Asynchronous versions of the operations that save audit logs are also provided. For example:
public async Task SaveOrderAsync(Order order)
{
AuditScope auditScope = null;
try
{
// async scope creation
auditScope = await AuditScope.CreateAsync("order", () => order);
}
finally
{
// async disposal
await auditScope.DisposeAsync();
}
}
Output
The library will generate an output (AuditEvent
) for each operation, including:
- Tracked object's state before and after the operation.
- Execution time and duration.
- Environment information such as user, machine, domain, locale, etc.
- Comments and Custom Fields provided.
An example of the output in JSON:
{
"EventType": "Order:Update",
"Environment": {
"UserName": "Federico",
"MachineName": "HP",
"DomainName": "HP",
"CallingMethodName": "Audit.UnitTest.AuditTests.TestUpdate()",
"Exception": null,
"Culture": "en-GB"
},
"Activity": {
"StartTimeUtc": "2023-12-01T17:36:52.2256288Z",
"SpanId": "23a93b9e8cbc457f",
"TraceId": "2d3e5e90f790c7d2274d9bb047531f66",
"ParentId": "0000000000000000",
"Operation": "Update"
},
"StartDate": "2016-08-23T11:33:14.653191Z",
"EndDate": "2016-08-23T11:33:23.1820786Z",
"Duration": 8529,
"Target": {
"Type": "Order",
"Old": {
"OrderId": "39dc0d86-d5fc-4d2e-b918-fb1a97710c99",
"Status": 2,
"OrderItems": [{
"Sku": "1002",
"Quantity": 3.0
}]
},
"New": {
"OrderId": "39dc0d86-d5fc-4d2e-b918-fb1a97710c99",
"Status": -1,
"OrderItems": null
}
}
}
Output details
The following tables describe the output fields:
Field Name | Type | Description |
---|---|---|
EventType | string | User-defined string to group the events |
Environment | Environment | Contains information about the execution environment |
StartDate | DateTime | Date and time when the event has started |
EndDate | DateTime | Date and time when the event has ended |
Duration | integer | Duration of the event in milliseconds |
Target | Target | User-defined tracked object |
Comments | Array of strings | User-defined comments |
CustomFields | Dictionary | User-defined custom fields |
Field Name | Type | Description |
---|---|---|
UserName | string | Current logged user name |
MachineName | string | Executing machine name |
DomainName | string | Current user domain |
CallingMethodName | string | Calling method signature information |
StackTrace | string | The full stack trace at the moment of the audit scope creation (NULL unless it's enabled by configuration) |
Exception | string | Indicates if an Exception has been detected (NULL if no exception has been thrown) |
Culture | string | Current culture identifier |
Field Name | Type | Description |
---|---|---|
Type | string | Tracked object type name |
Old | Object | Value of the tracked object at the beginning of the event |
New | Object | Value of the tracked object at the end of the event |
Custom Fields and Comments
The AuditScope
object provides two methods to extend the event output.
- Use
SetCustomField()
method to add any object as an extra field to the event. - Use
Comment()
to add textual comments to the event'sComments
array.
For example:
Order order = Db.GetOrder(orderId);
using (var audit = AuditScope.Create("Order:Update", () => order))
{
audit.SetCustomField("ReferenceId", orderId);
order.Status = -1;
order = Db.OrderUpdate(order);
audit.Comment("Status Updated to Cancelled");
}
You can also set Custom Fields when creating the AuditScope
, by passing an anonymous object with the properties you want as extra fields. For example:
using (var audit = AuditScope.Create("Order:Update", () => order, new { ReferenceId = orderId }))
{
order.Status = -1;
order = Db.OrderUpdate(order);
audit.Comment("Status Updated to Cancelled");
}
You can also access the Custom Fields directly from Event.CustomFields
property of the scope. For example:
using (var audit = AuditScope.Create("Order:Update", () => order, new { ReferenceId = orderId }))
{
audit.Event.CustomFields["ReferenceId"] = orderId;
}
Note
Custom fields are not limited to single properties, you can store any object as well, by default they will be JSON serialized.
Extending AuditEvent
Another way to enrich the event output is to create a class inheriting from the AuditEvent
class, then you can pass an instance of your class to the AuditScope.Create method. For example:
public class YourAuditEvent : AuditEvent
{
public Guid ReferenceId { get; set; } = Guid.NewGuid();
}
using (var scope = AuditScope.Create(new AuditScopeOptions { AuditEvent = new YourAuditEvent() }))
{
//...
}
The output of the previous examples would be:
{
"EventType": "Order:Update",
"Environment": {
"UserName": "Federico",
"MachineName": "HP",
"DomainName": "HP",
"CallingMethodName": "Audit.UnitTest.AuditTests.TestUpdate()",
"Exception": null,
"Culture": "en-GB"
},
"Target": {
"Type": "Order",
"Old": {
"OrderId": "39dc0d86-d5fc-4d2e-b918-fb1a97710c99",
"Status": 2,
},
"New": {
"OrderId": "39dc0d86-d5fc-4d2e-b918-fb1a97710c99",
"Status": -1,
}
},
"ReferenceId": "39dc0d86-d5fc-4d2e-b918-fb1a97710c99", // <-- Custom Field
"Comments": ["Status Updated to Cancelled"], // <-- Comments
"StartDate": "2016-08-23T11:34:44.656101-05:00",
"EndDate": "2016-08-23T11:34:55.1810821-05:00",
"Duration": 8531
}
Discard option
The AuditScope
object has a Discard()
method to allow the user to discard an event. Discarding an event means it won't be saved.
For example, if you want to avoid saving the audit event under certain conditions:
using (var scope = AuditScope.Create(new AuditScopeOptions("SomeEvent", () => someTarget)))
{
try
{
//some operation
Critical.Operation();
}
catch (Exception ex)
{
//If an exception is thrown, discard the audit event
scope.Discard();
}
}
Data providers
A data provider (or storage sink) contains the logic to handle the audit event output, where you define what to do with the audit logs.
You can use one of the data providers included or inject your own mechanism
by creating a class that inherits from AuditDataProvider
and overrides its methods:
InsertEvent
: should store the event and return a unique ID.ReplaceEvent
: should update an event given its ID. This method is only used for Creation Policies Manual or InsertOnStartReplaceOnEnd.
If your data provider will support asynchronous operations, you must also implement the following methods:
InsertEventAsync
: Asynchronous implementation of the InsertEvent method.ReplaceEventAsync
: Asynchronous implementation of the ReplaceEvent method.
Also, if your data provider will support event retrieval, you should implement the following methods:
GetEvent
: Retrieves an event by id.GetEventAsync
: Asynchronous implementation of the GetEvent method.
For example:
public class MyCustomDataProvider : AuditDataProvider
{
public override object InsertEvent(AuditEvent auditEvent)
{
var fileName = $"Log{Guid.NewGuid()}.json";
File.WriteAllText(fileName, auditEvent.ToJson());
return fileName;
}
public override void ReplaceEvent(object eventId, AuditEvent auditEvent)
{
var fileName = eventId.ToString();
File.WriteAllText(fileName, auditEvent.ToJson());
}
public override T GetEvent<T>(object eventId)
{
var fileName = eventId.ToString();
return JsonConvert.DeserializeObject<T>(File.ReadAllText(fileName));
}
// async implementation:
public override async Task<object> InsertEventAsync(AuditEvent auditEvent, CancellationToken cancellationToken = default)
{
var fileName = $"Log{Guid.NewGuid()}.json";
await File.WriteAllTextAsync(fileName, auditEvent.ToJson(), cancellationToken);
return fileName;
}
public override async Task ReplaceEventAsync(object eventId, AuditEvent auditEvent, CancellationToken cancellationToken = default)
{
var fileName = eventId.ToString();
await File.WriteAllTextAsync(fileName, auditEvent.ToJson(), cancellationToken);
}
public override async Task<T> GetEventAsync<T>(object eventId, CancellationToken cancellationToken = default)
{
var fileName = eventId.ToString();
return await GetFromFileAsync<T>(cancellationToken);
}
}
Data provider selection
The data provider can be set globally for the entire application or per audit scope.
Note
If you don't specify a global data provider, it will default to a
FileDataProvider
that logs events as .json files into the current working directory.
To set the global data provider assign the DataProvider
property on the static Audit.Core.Configuration
object, or call the fluent API Use()
. For example:
Audit.Core.Configuration.DataProvider = new MyCustomDataProvider();
Or using the fluent API Use()
method:
Audit.Core.Configuration.Setup()
.Use(new MyCustomDataProvider());
Lazy Factory data provider
You can set the global data provider as a lazy factory method invoked until it is first used. For example, when you require resolving dependencies from the DI container to instantiate the data provider:
Audit.Core.Configuration.Setup()
.UseLazyFactory(() => app.ApplicationServices.GetService<CustomDataProvider>());
Deferred Factory data provider
You can defer creating the data provider for each Audit Event until it is ready to be saved by using a deferred factory method. For example:
var sqlDataProvider = new SqlDataProvider(config => config...);
var fileDataProvider = new FileDataProvider(config => config...);
Audit.Core.Configuration.Setup()
.UseDeferredFactory(auditEvent => auditEvent is AuditEventWebApi ? sqlDataProvider : fileDataProvider);
See Configuration section for more information.
To set the data provider per-scope, use the AuditScopeOptions
when creating an AuditScope
. For example:
var scope = AuditScope.Create(new AuditScopeOptions
{
DataProvider = new MyCustomDataProvider(), ... }
);
Dynamic data providers
As an alternative to creating a data provider class, you can define the mechanism at run time by using the DynamicDataProvider
or DynamicAsyncDataProvider
classes. For example:
var dataProvider = new DynamicDataProvider();
// Attach an action for insert
dataProvider.AttachOnInsert(ev => Console.Write(ev.ToJson()));
Audit.Core.Configuration.DataProvider = dataProvider;
Or by using the fluent API:
Audit.Core.Configuration.Setup()
.UseDynamicProvider(config => config
.OnInsert(ev => Console.Write(ev.ToJson())));
For async operations, you should use the DynamicAsyncDataProvider
, for example:
var dataProvider = new DynamicAsyncDataProvider();
dataProvider.AttachOnInsert(async ev => await File.WriteAllTextAsync(filePath, ev.ToJson()));
Audit.Core.Configuration.DataProvider = dataProvider;
Or by using the fluent API:
Audit.Core.Configuration.Setup()
.UseDynamicAsyncProvider(config => config
.OnInsert(async ev => await File.WriteAllTextAsync(filePath, ev.ToJson())));
Wrapper data providers
A special type of Data Providers that allows wrapping other Data Providers with different purposes:
LazyDataProvider
Allows to lazily instantiate the data provider to use. The data provider factory method will be called only once; the first time it's needed.
Configuration.DataProvider = new LazyDataProvider(() => app.ApplicationServices.GetService<MyCustomDataProvider>());
DeferredDataProvider
Allows to defer the data provider instantiation until the audit event is about to be saved. The data provider factory method will be called for each audit event being saved.
Configuration.DataProvider = new DeferredDataProvider(auditEvent => auditEvent is AuditEventWebApi ? new FileDataProvider() : EventLogDataProvider());
ConditionalDataProvider
Enables the configuration of different data providers based on conditions related to the audit event.
Configuration.DataProvider = new ConditionalDataProvider(config => config .When(auditEvent => auditEvent.EventType.Equals("A"), new MyCustomDataProvider()) .When(auditEvent => auditEvent.EventType.Equals("B"), new SqlDataProvider()) .Otherwise(new FileDataProvider()));
Data providers included
The Data Providers included are summarized in the following table:
Type | Technology | Package / Class | Description | Configuration API |
---|---|---|---|---|
SQL | Amazon QLDB | Audit.NET.AmazonQLDB / AmazonQldbDataProvider | Store the audit events using Amazon QLDB. | .UseAmazonQldb() |
SQL | Entity Framework | Audit.EntityFramework / EntityFrameworkDataProvider | Store EntityFramework audit events in the same EF context. (This data provider can only be used for Entity Framework audits) | .UseEntityFramework() |
SQL | MySql | Audit.NET.MySql / MySqlDataProvider | Store the events as rows in a MySQL database table, in JSON format. | .UseMySql() |
SQL | Postgre SQL | Audit.NET.PostgreSql / PostgreSqlDataProvider | Store the events as rows in a PostgreSQL database table, in JSON format. | .UsePostgreSql() |
SQL | SQL Server | Audit.NET.SqlServer / SqlDataProvider | Store the events as rows in a MS SQL Table, in JSON format. | .UseSqlServer() |
NoSQL | Azure Cosmos | Audit.NET.AzureCosmos / AzureCosmosDataProvider | Store the events in an Azure Cosmos DB container, in JSON format. | .UseAzureCosmos() |
NoSQL | Azure Storage | Audit.NET.AzureStorageBlobs / AzureStorageBlobDataProvider | Store the events in an Azure Blob Storage container, in JSON format. | .UseAzureStorageBlobs() |
NoSQL | Azure Tables | Audit.NET.AzureStorageTables / AzureStorageTableDataProvider | Store the events in an Azure Table Storage. | .UseAzureTableStorage() |
NoSQL | Dynamo DB | Audit.NET.DynamoDB / DynamoDataProvider | Store audit events in Amazon DynamoDB™ tables. | .UseDynamoDB() |
NoSQL | Elasticsearch | Audit.NET.Elasticsearch / ElasticsearchDataProvider | Store audit events in Elasticsearch indices. | .UseElasticsearch() |
NoSQL | Kafka | Audit.NET.Kafka / KafkaDataProvider | Stream the audit events to Apache Kafka topics. | .UseKafka() / .UseKafka<TKey>() |
NoSQL | Mongo DB | Audit.NET.MongoDB / MongoDataProvider | Store the events in a Mongo DB collection, in BSON format. | .UseMongoDB() |
NoSQL | Raven DB | Audit.NET.RavenDB / RavenDbDataProvider | Store the events as documents in a Raven DB database table, in JSON format. | .UseRavenDB() |
NoSQL | Redis | Audit.NET.Redis / RedisDataProvider | Store audit logs in Redis as Strings, Lists, SortedSets, Hashes, Streams or publish to a PubSub channel. | .UseRedis() |
Local | Windows Event Log | Audit.NET / Audit.NET.EventLog.Core 7 EventLogDataProvider | Write the audit logs to the Windows EventLog. | .UseEventLogProvider() |
Local | File System | Audit.NET / FileDataProvider | Store the audit logs as files. Dynamically configure the directory and path. | .UseFileLogProvider() |
Local | In-Memory | Audit.NET / InMemoryDataProvider | Store the audit logs in memory in a thread-safe list. Useful for testing purposes. | .UseInMemoryProvider() |
Logging | Log4net | Audit.NET.log4net / Log4netDataProvider | Store the audit events using Apache log4net™. | .UseLog4net() |
Logging | NLog | Audit.NET.NLog / NLogDataProvider | Store the audit events using NLog. | .UseNLog() |
Logging | Serilog | Audit.NET.Serilog / SerilogDataProvider | Store the audit events using Serilog™ | .UseSerilog() |
Network | UDP | Audit.NET.Udp / UdpDataProvider | Send Audit Logs as UDP datagrams to a network. | .UseUdp() |
Wrapper | Conditional | Audit.NET / ConditionalDataProvider | Allows the configuration of different data providers based on conditions related to the audit event. | .UseConditional() |
Wrapper | Deferred | Audit.NET / DeferredDataProvider | Facilitates delayed data provider instantiation via a factory method that is invoked for each Audit Event. | .UseDeferredFactory() |
Wrapper | Dynamic | Audit.NET / DynamicDataProvider / DynamicAsyncDataProvider | Dynamically change the behavior at run-time. Define Insert and a Replace actions with lambda expressions. | .UseDynamicProvider() / .UseDynamicAsyncProvider() |
Wrapper | Lazy | Audit.NET / LazyDataProvider | Facilitates delayed data provider instantiation via a factory method that is invoked just once, upon the initial need. | .UseLazyFactory() |
Wrapper | Polly | Audit.NET.Polly / PollyDataProvider | Allows to define Polly resilience strategies to any Data Provider | .UsePolly() |
Event Creation Policy
The audit scope can be configured to call its data provider in different ways:
Insert on End: (default) The audit event is inserted when the scope is disposed.
Insert on Start, Replace on End: The event (in its initial state) is inserted when the scope is created, and then the complete event information is replaced when the scope is disposed.
Insert on Start, Insert on End: Two versions of the event are inserted, the initial when the scope is created, and the final when the scope is disposed.
Manual: The event saving (insert/replace) should be explicitly invoked by calling the
Save()
method on theAuditScope
.
You can set the Creation Policy per scope, for example, to explicitly set the Creation Policy to Manual:
using (var scope = AuditScope.Create(new AuditScopeOptions { CreationPolicy = EventCreationPolicy.Manual }))
{
//...
scope.Save();
}
Note
If you don't provide a Creation Policy, the default Creation Policy configured will be used (see the configuration section).
AuditScope statechart
The following is the internal state machine representation of the AuditScope
object:
Configuration
Data provider
To change the default data provider, set the static property DataProvider
on Audit.Core.Configuration
class. This should be done before the AuditScope
creation, i.e. during application startup.
For example, to set your own provider as the default data provider:
Audit.Core.Configuration.DataProvider = new MyCustomDataProvider();
Note
If you don't specify a Data Provider, a default
FileDataProvider
will be used to write the events as .json files into the current working directory.
Creation Policy
To change the default creation policy, set the static property CreationPolicy
on Audit.Core.Configuration
class. This should be done before the AuditScope
creation, i.e. during application startup.
For example, to set the default creation policy to Manual:
Audit.Core.Configuration.CreationPolicy = EventCreationPolicy.Manual;
Note
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.
Call the static AddCustomAction()
method on Audit.Core.Configuration
class to attach a custom action.
For example, to globally discard the events under a certain condition:
Audit.Core.Configuration.AddCustomAction(ActionType.OnScopeCreated, scope =>
{
if (DateTime.Now.Hour == 17) // Tea time
{
scope.Discard();
}
});
Or to add custom fields/comments globally to all scopes:
Audit.Core.Configuration.AddCustomAction(ActionType.OnEventSaving, scope =>
{
if (scope.Event.Environment.Exception != null)
{
scope.SetCustomField("Oops", true);
}
scope.Comment("Saved at " + DateTime.Now);
});
Custom actions can also be asynchronous, for example:
Audit.Core.Configuration.AddCustomAction(ActionType.OnScopeCreated, async scope =>
{
var result = await svcProvider.GetService<InfoService>().GetInfoAsync();
scope.SetCustomField("Info", result);
});
The ActionType
indicates when to perform the action. The allowed values are:
OnScopeCreated
: When the Audit Scope is being created, before any saving. This is executed once per Audit Scope.OnEventSaving
: When an Audit Scope's Event is about to be saved.OnEventSaved
: After an Audit Scope's Event is saved.
Stack Trace
To include the stack trace details into the event environment, ensure that the IncludeStackTrace
configuration is set to true
. Default is false
.
Audit.Core.Configuration.IncludeStackTrace = true;
or
Audit.Core.Configuration.Setup()
.IncludeStackTrace();
Activity Trace
To include the activity trace details from System.Diagnostics.Activity
API into the event, ensure that the IncludeActivityTrace
configuration is set to true
. The default is false
.
It will include the current Activity
operation name, ID, and StartTime, along with associated Tags and Events.
Audit.Core.Configuration.IncludeActivityTrace = true;
or
Audit.Core.Configuration.Setup()
.IncludeActivityTrace();
Global switch off
You can disable audit logging by setting the static property Configuration.AuditDisabled
to true
.
The audit events are globally ignored while this flag is set. For example to disable the audits on certain environments:
if (environment.IsDevelopment())
{
Audit.Core.Configuration.AuditDisabled = true;
}
Global serialization settings
Most of the data providers serialize audit events in JSON format. Audit.NET uses System.Text.Json
by default for the serialization and deserialization of audit events.
If you want to change the behavior, you can change the settings via the static property Configuration.JsonSettings
.
For example:
Audit.Core.Configuration.JsonSettings = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
AllowTrailingCommas = true
};
Custom serialization mechanism
If you want to use a custom JSON serialization mechanism for the Audit Events, you can create a class implementing IJsonAdapter
and assign it to the
static property Configuration.JsonAdapter
.
For example:
Audit.Core.Configuration.JsonAdapter = new MyCustomAdapter();
Or by using the fluent API:
Audit.Core.Configuration.Setup()
.JsonAdapter<MyCustomAdapter>()
...
Note
Take into account that some of the
AuditEvent
properties rely on attribute decoration for serialization and deserialization. The recommendation is to use the default adapter and, when needed, use the Newtonsoft Json adapter provided (see next section).
Alternative serialization mechanism
This library offers the option to configure an alternative JSON serialization mechanism through the following adapter:
Audit.NET.JsonNewtonsoftAdapter
Use this when you prefer employing Newtonsoft.Json as the serialization mechanism.
Assign an instance of
JsonNewtonsoftAdapter
to the static configuration propertyConfiguration.JsonAdapter
. For example:var settings = new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.All }; Audit.Core.Configuration.JsonAdapter = new JsonNewtonsoftAdapter(settings);
Alternatively, you can use the fluent configuration API with the
JsonNewtonsoftAdapter()
method, like this:Audit.Core.Configuration.Setup() .JsonNewtonsoftAdapter(settings) ...
NOTE: This
JsonNewtonsoftAdapter
takes into accountJsonExtensionDataAttribute
andJsonIgnoreAttribute
decorators from bothSystem.Text.Json
andNewtonsoft.Json
, so the Audit Events will be properly serialized.
Configuration Fluent API
Alternatively to the properties/methods mentioned before, you can configure the library using a convenient Fluent API provided by the method Audit.Core.Configuration.Setup()
, this is the most straightforward way to configure the library.
For example, to set the FileLog Provider with its default settings using a Manual creation policy:
Audit.Core.Configuration.Setup
.IncludeStackTrace()
.IncludeActivityTrace()
.UseFileLogProvider()
.WithCreationPolicy(EventCreationPolicy.Manual);
Configuration examples
File log provider with dynamic directory path and filename:
Audit.Core.Configuration.Setup()
.UseFileLogProvider(config => config
.DirectoryBuilder(_ => $@"C:\Logs\{DateTime.Now:yyyy-MM-dd}")
.FilenameBuilder(auditEvent => $"{auditEvent.Environment.UserName}_{DateTime.Now.Ticks}.json"));
File log provider with an InsertOnStart-ReplaceOnEnd creation policy, and a global custom field set in a custom action:
Audit.Core.Configuration.Setup()
.UseFileLogProvider(config => config
.FilenamePrefix("Event_")
.Directory(@"C:\AuditLogs\1"))
.WithCreationPolicy(EventCreationPolicy.InsertOnStartReplaceOnEnd)
.WithAction(x => x.OnScopeCreated(scope => scope.SetCustomField("ApplicationId", "MyApplication")));
Event log provider with an InsertOnEnd creation policy:
Audit.Core.Configuration.Setup()
.UseEventLogProvider(config => config
.SourcePath("My Audited Application")
.LogName("Application"))
.WithCreationPolicy(EventCreationPolicy.InsertOnEnd);
Dynamic provider to log to the console:
Audit.Core.Configuration.Setup()
.UseDynamicProvider(config => config
.OnInsert(ev => Console.WriteLine("{0}: {1}->{2}", ev.StartDate, ev.Environment.UserName, ev.EventType)));
Multiple providers with conditional logic:
var sqlDataProvider = new SqlDataProvider(sql => sql.ConnectionString(CnnString).TableName("Logs"));
var fileDataProvider = new FileDataProvider(file => file.Directory(@"C:\logs"));
Audit.Core.Configuration.Setup()
.UseConditional(c => c
.When(ev => ev.EventType == "API", sqlDataProvider)
.Otherwise(fileDataProvider));
Extensions
The following packages are extensions to log interactions with different systems such as MVC, WebApi, WCF, and Entity Framework:
Package | Description | |
---|---|---|
Audit.DynamicProxy | Generate detailed audit logs for any class without changing its code by using a proxy. | |
Audit.EntityFramework | Generate detailed audit logs for saving operations on Entity Framework, by inheriting from a provided DbContext or IdentityDbContext . Includes support for EF 6 and EF 7 (EF Core). |
|
Audit.FileSystem | Generate audit logs by intercepting file system events via FileSystemWatcher. | |
Audit.HttpClient | Generate detailed client-side audit logs for HttpClient REST calls, by configuring a provided message handler. | |
Audit.MVC | Generate detailed audit logs by decorating MVC Actions and Controllers with an action filter attribute. Includes support for ASP.NET Core MVC. | |
Audit.SignalR | Generate audit logs for SignalR and SignalR Core invocations by intercepting the hub processing | |
Audit.WCF | Generate detailed server-side audit logs for Windows Communication Foundation (WCF) service calls, by configuring a provided behavior. | |
Audit.WCF.Client | Generate detailed client-side audit logs for Windows Communication Foundation (WCF) service calls, by configuring a provided behavior. | |
Audit.WebApi | Generate detailed audit logs by decorating Web API Methods and Controllers with an action filter attribute, or by using a middleware. Includes support for ASP.NET Core. | |
Audit.MongoClient | Generate detailed audit logs by adding a Command Event Subscriber into the configuration of the MongoDB Driver. |
Storage providers
Apart from the FileLog, EventLog and Dynamic event storage providers, there are others included in different packages:
Package | Description | |
---|---|---|
Audit.NET.AmazonQLDB | Store the audit events in Amazon QLDB (Quantum Ledger Database). | |
Audit.NET.AzureCosmos | Store the events in an Azure Cosmos DB container, in JSON format. | |
Audit.NET.AzureStorage | Store the events in an Azure Blob Storage container or an Azure Table using the legacy client WindowsAzure.Storage. | |
Audit.NET.AzureStorageBlobs | Store the events in an Azure Blob Storage container using the latest client Azure.Storage.Blobs. | |
Audit.NET.AzureStorageTables | Store the events in an Azure Table Storage using the latest client Azure.Data.Tables. | |
Audit.NET.DynamoDB | Store the audit events in Amazon DynamoDB tables. | |
Audit.NET.Elasticsearch | Store the audit events in Elasticsearch indices. | |
Audit.NET.Kafka | Stream the audit events to an Apache Kafka server. | |
Audit.NET.log4net | Store the audit events using Apache log4net™. | |
Audit.NET.MongoDB | Store the events in a Mongo DB Collection, in BSON format. | |
Audit.NET.MySql | Store the events as rows in MySQL database, in JSON format. | |
Audit.NET.NLog | Store the audit events using NLog™. | |
Audit.NET.Polly | Define Polly resiliencie strategies to any data provider. | |
Audit.NET.PostgreSql | Store the events as rows in a PostgreSQL database, in JSON format. | |
Audit.NET.RavenDB | Store the events as documents in a Raven DB database, in JSON format. | |
Audit.NET.Redis | Store Audit Logs in a Redis database as String, List, Hash, Sorted Set, Streams or publishing to a Redis PubSub channel. | |
Audit.NET.SqlServer | Store the events as rows in a SQL Table, in JSON format. | |
Audit.NET.Serilog | Store the audit events using Serilog™ | |
Audit.NET.Udp | Send Audit Logs as UDP datagrams to a network. |
Change Log
For detailed information on changes in the new release refer to the change log.
Contribute
If you like this project please contribute in any of the following ways:
- Sponsoring this project.
- Star this project on GitHub.
- Request a new feature or expose any bug you found by creating a new issue.
- Ask any questions about the library on StackOverflow.
- Subscribe to and use the Gitter Audit.NET channel.
- Support the project by becoming a Backer:
- Spread the word by blogging about it, or sharing it on social networks:
- Donate via PayPal:
Showing the top 20 packages that depend on Audit.NET.
Packages | Downloads |
---|---|
Audit.NET.MongoDB
Store Audit.NET Trail Logs into a MongoDB database
|
40 |
Audit.NET.MongoDB
Store Audit.NET Trail Logs into a MongoDB database
|
16 |
Audit.NET.MongoDB
Store Audit.NET Trail Logs into a MongoDB database
|
14 |
Audit.NET.MongoDB
Store Audit.NET Trail Logs into a MongoDB database
|
13 |
Audit.NET.MongoDB
Store Audit.NET Trail Logs into a MongoDB database
|
12 |
Audit.NET.MongoDB
Store Audit.NET Trail Logs into a MongoDB database
|
11 |
Audit.NET.MongoDB
Store Audit.NET Trail Logs into a MongoDB database
|
10 |
Audit.NET.MongoDB
Store Audit.NET Trail Logs into a MongoDB database
|
9 |
Audit.NET.MongoDB
Store Audit.NET Trail Logs into a MongoDB database
|
8 |
Audit.NET.MongoDB
Mongo DB provider for Audit.NET library
|
8 |
.NET Framework 4.6.2
- System.Text.Json (>= 6.0.9)
.NET Framework 4.7.2
- System.Text.Json (>= 6.0.9)
.NET 6.0
- No dependencies.
.NET 7.0
- No dependencies.
.NET 8.0
- No dependencies.
.NET Standard 2.0
- Microsoft.Bcl.AsyncInterfaces (>= 8.0.0)
- System.Text.Json (>= 6.0.9)
.NET Standard 2.1
- System.Text.Json (>= 6.0.9)
Version | Downloads | Last updated |
---|---|---|
27.2.0 | 0 | 11/23/2024 |
27.1.1 | 18 | 11/05/2024 |
27.1.0 | 2 | 11/05/2024 |
27.0.3 | 2 | 10/06/2024 |
27.0.2 | 3 | 09/20/2024 |
27.0.1 | 3 | 09/05/2024 |
27.0.0 | 3 | 09/04/2024 |
26.0.1 | 5 | 08/22/2024 |
26.0.0 | 3 | 07/23/2024 |
25.0.7 | 3 | 07/04/2024 |
25.0.6 | 5 | 06/25/2024 |
25.0.5 | 6 | 06/24/2024 |
25.0.4 | 5 | 04/04/2024 |
25.0.3 | 12 | 03/17/2024 |
25.0.2 | 11 | 03/17/2024 |
25.0.1 | 11 | 03/17/2024 |
25.0.0 | 11 | 02/20/2024 |
24.0.1 | 13 | 02/20/2024 |
24.0.0 | 11 | 02/20/2024 |
23.0.0 | 5 | 12/15/2023 |
22.1.0 | 3 | 12/15/2023 |
22.0.2 | 5 | 12/07/2023 |
22.0.1 | 6 | 12/07/2023 |
22.0.0 | 4 | 12/15/2023 |
21.1.0 | 3 | 02/20/2024 |
21.0.4 | 6 | 12/15/2023 |
21.0.3 | 2 | 07/22/2024 |
21.0.2 | 4 | 01/07/2024 |
21.0.1 | 7 | 08/25/2023 |
21.0.0 | 5 | 08/25/2023 |
20.2.4 | 4 | 08/25/2023 |
20.2.3 | 4 | 08/25/2023 |
20.2.2 | 12 | 08/25/2023 |
20.2.1 | 6 | 08/25/2023 |
20.2.0 | 6 | 08/25/2023 |
20.1.6 | 5 | 08/27/2023 |
20.1.5 | 5 | 08/25/2023 |
20.1.4 | 5 | 08/26/2023 |
20.1.3 | 4 | 08/28/2023 |
20.1.2 | 8 | 08/24/2023 |
20.1.1 | 5 | 08/29/2023 |
20.1.0 | 5 | 02/20/2024 |
20.0.4 | 7 | 08/26/2023 |
20.0.3 | 6 | 08/30/2023 |
20.0.2 | 7 | 08/24/2023 |
20.0.1 | 5 | 08/26/2023 |
20.0.0 | 6 | 02/20/2024 |
19.4.1 | 4 | 02/20/2024 |
19.4.0 | 4 | 08/24/2023 |
19.3.0 | 4 | 08/31/2023 |
19.2.2 | 4 | 08/25/2023 |
19.2.1 | 7 | 08/27/2023 |
19.2.0 | 4 | 02/20/2024 |
19.1.4 | 7 | 08/26/2023 |
19.1.3 | 5 | 08/31/2023 |
19.1.2 | 5 | 08/26/2023 |
19.1.1 | 8 | 08/29/2023 |
19.1.0 | 5 | 08/26/2023 |
19.0.7 | 4 | 02/20/2024 |
19.0.6 | 6 | 08/26/2023 |
19.0.5 | 7 | 08/28/2023 |
19.0.4 | 6 | 08/24/2023 |
19.0.3 | 5 | 02/20/2024 |
19.0.2 | 5 | 08/24/2023 |
19.0.1 | 7 | 08/30/2023 |
19.0.0 | 5 | 08/27/2023 |
19.0.0-rc.net60.2 | 6 | 08/27/2023 |
19.0.0-rc.net60.1 | 5 | 08/26/2023 |
18.1.6 | 4 | 08/25/2023 |
18.1.5 | 2 | 07/22/2024 |
18.1.4 | 5 | 08/25/2023 |
18.1.3 | 7 | 08/28/2023 |
18.1.2 | 3 | 02/20/2024 |
18.1.1 | 5 | 08/25/2023 |
18.1.0 | 7 | 08/26/2023 |
18.0.1 | 5 | 08/25/2023 |
18.0.0 | 5 | 08/26/2023 |
17.0.8 | 6 | 08/29/2023 |
17.0.7 | 8 | 08/29/2023 |
17.0.6 | 6 | 12/30/2023 |
17.0.5 | 5 | 08/30/2023 |
17.0.4 | 6 | 08/31/2023 |
17.0.3 | 6 | 08/30/2023 |
17.0.2 | 6 | 08/31/2023 |
17.0.1 | 8 | 08/29/2023 |
17.0.0 | 7 | 08/28/2023 |
16.5.6 | 4 | 02/20/2024 |
16.5.5 | 10 | 08/26/2023 |
16.5.4 | 5 | 02/20/2024 |
16.5.3 | 9 | 08/25/2023 |
16.5.2 | 6 | 08/31/2023 |
16.5.1 | 5 | 08/29/2023 |
16.5.0 | 6 | 02/20/2024 |
16.4.5 | 5 | 08/25/2023 |
16.4.4 | 4 | 08/26/2023 |
16.4.3 | 7 | 08/30/2023 |
16.4.2 | 7 | 08/27/2023 |
16.4.1 | 2 | 08/24/2023 |
16.4.0 | 3 | 08/29/2023 |
16.3.3 | 3 | 08/29/2023 |
16.3.2 | 4 | 08/27/2023 |
16.3.1 | 4 | 02/20/2024 |
16.3.0 | 2 | 08/24/2023 |
16.2.1 | 4 | 08/27/2023 |
16.2.0 | 2 | 08/26/2023 |
16.1.5 | 3 | 08/27/2023 |
16.1.4 | 5 | 08/26/2023 |
16.1.3 | 2 | 08/30/2023 |
16.1.2 | 3 | 02/20/2024 |
16.1.1 | 6 | 08/25/2023 |
16.1.0 | 1 | 02/20/2024 |
16.0.3 | 3 | 02/20/2024 |
16.0.2 | 3 | 08/24/2023 |
16.0.1 | 2 | 08/26/2023 |
16.0.0 | 6 | 08/24/2023 |
15.3.0 | 2 | 08/28/2023 |
15.2.3 | 3 | 08/25/2023 |
15.2.2 | 3 | 08/25/2023 |
15.2.1 | 6 | 08/27/2023 |
15.2.0 | 7 | 08/25/2023 |
15.1.1 | 5 | 02/20/2024 |
15.1.0 | 3 | 08/28/2023 |
15.0.5 | 5 | 08/26/2023 |
15.0.4 | 2 | 08/24/2023 |
15.0.3 | 5 | 08/31/2023 |
15.0.2 | 4 | 08/25/2023 |
15.0.1 | 5 | 08/27/2023 |
15.0.0 | 8 | 08/27/2023 |
14.9.1 | 2 | 02/20/2024 |
14.9.0 | 4 | 08/28/2023 |
14.8.1 | 2 | 08/25/2023 |
14.8.0 | 4 | 08/25/2023 |
14.7.0 | 2 | 08/29/2023 |
14.6.6 | 3 | 08/30/2023 |
14.6.5 | 3 | 08/25/2023 |
14.6.4 | 1 | 02/20/2024 |
14.6.3 | 4 | 08/26/2023 |
14.6.2 | 4 | 08/30/2023 |
14.6.1 | 5 | 08/27/2023 |
14.6.0 | 5 | 08/25/2023 |
14.5.7 | 6 | 08/31/2023 |
14.5.6 | 3 | 08/27/2023 |
14.5.5 | 7 | 08/25/2023 |
14.5.4 | 4 | 08/26/2023 |
14.5.3 | 7 | 08/26/2023 |
14.5.2 | 2 | 08/25/2023 |
14.5.1 | 3 | 08/24/2023 |
14.5.0 | 4 | 08/25/2023 |
14.4.0 | 1 | 02/20/2024 |
14.3.4 | 2 | 02/20/2024 |
14.3.3 | 2 | 08/24/2023 |
14.3.2 | 3 | 08/25/2023 |
14.3.1 | 5 | 08/27/2023 |
14.3.0 | 3 | 08/27/2023 |
14.2.3 | 3 | 08/25/2023 |
14.2.2 | 2 | 08/24/2023 |
14.2.1 | 3 | 08/26/2023 |
14.2.0 | 45 | 11/29/2019 |
14.1.1 | 4 | 08/26/2023 |
14.1.0 | 4 | 08/24/2023 |
14.0.4 | 1 | 08/25/2023 |
14.0.3 | 2 | 08/31/2023 |
14.0.2 | 5 | 08/25/2023 |
14.0.1 | 5 | 08/30/2023 |
14.0.0 | 4 | 12/18/2023 |
13.3.0 | 5 | 02/20/2024 |
13.2.2 | 2 | 08/24/2023 |
13.2.1 | 5 | 08/27/2023 |
13.2.0 | 3 | 08/28/2023 |
13.1.5 | 2 | 02/20/2024 |
13.1.4 | 3 | 08/25/2023 |
13.1.3 | 5 | 08/27/2023 |
13.1.2 | 5 | 08/25/2023 |
13.1.1 | 3 | 02/20/2024 |
13.1.0 | 2 | 08/27/2023 |
13.0.0 | 5 | 08/29/2023 |
12.3.6 | 4 | 08/24/2023 |
12.3.5 | 4 | 08/29/2023 |
12.3.4 | 4 | 08/26/2023 |
12.3.3 | 6 | 08/24/2023 |
12.3.2 | 5 | 08/24/2023 |
12.3.1 | 7 | 08/25/2023 |
12.3.0 | 5 | 08/28/2023 |
12.2.2 | 3 | 02/20/2024 |
12.2.1 | 4 | 08/26/2023 |
12.2.0 | 4 | 08/28/2023 |
12.1.11 | 7 | 08/28/2023 |
12.1.10 | 4 | 08/25/2023 |
12.1.9 | 4 | 08/27/2023 |
12.1.8 | 2 | 02/20/2024 |
12.1.7 | 3 | 02/11/2024 |
12.1.6 | 5 | 02/20/2024 |
12.1.5 | 8 | 08/31/2023 |
12.1.4 | 4 | 12/30/2023 |
12.1.3 | 7 | 08/25/2023 |
12.1.2 | 4 | 08/25/2023 |
12.1.1 | 2 | 02/20/2024 |
12.1.0 | 4 | 08/27/2023 |
12.0.7 | 6 | 12/26/2023 |
12.0.6 | 4 | 08/26/2023 |
12.0.5 | 6 | 08/28/2023 |
12.0.4 | 5 | 08/27/2023 |
12.0.3 | 3 | 08/30/2023 |
12.0.2 | 7 | 08/25/2023 |
12.0.1 | 8 | 08/28/2023 |
12.0.0 | 4 | 08/24/2023 |
11.2.0 | 3 | 02/20/2024 |
11.1.0 | 2 | 02/20/2024 |
11.0.8 | 5 | 08/26/2023 |
11.0.7 | 6 | 08/25/2023 |
11.0.6 | 5 | 08/27/2023 |
11.0.5 | 3 | 08/27/2023 |
11.0.4 | 4 | 08/29/2023 |
11.0.3 | 4 | 08/25/2023 |
11.0.2 | 5 | 08/25/2023 |
11.0.1 | 5 | 08/26/2023 |
11.0.0 | 5 | 08/28/2023 |
10.0.3 | 2 | 08/25/2023 |
10.0.2 | 4 | 08/31/2023 |
10.0.1 | 7 | 08/31/2023 |
10.0.0 | 5 | 08/25/2023 |
9.3.0 | 4 | 06/13/2023 |
9.2.0 | 4 | 06/12/2023 |
9.1.3 | 7 | 06/11/2023 |
9.1.2 | 6 | 06/13/2023 |
9.1.1 | 4 | 12/30/2023 |
9.1.0 | 6 | 06/13/2023 |
9.0.1 | 8 | 06/12/2023 |
9.0.0 | 4 | 06/12/2023 |
8.7.0 | 3 | 06/13/2023 |
8.6.0 | 3 | 06/12/2023 |
8.5.0 | 6 | 06/12/2023 |
8.4.0 | 3 | 06/12/2023 |
8.3.1 | 7 | 06/12/2023 |
8.3.0 | 3 | 06/13/2023 |
8.2.0 | 5 | 06/11/2023 |
8.1.0 | 6 | 06/12/2023 |
8.0.0 | 6 | 06/11/2023 |
7.1.3 | 4 | 02/20/2024 |
7.1.2 | 2 | 06/12/2023 |
7.1.1 | 4 | 06/12/2023 |
7.1.0 | 6 | 06/13/2023 |
7.0.9 | 4 | 06/14/2023 |
7.0.8 | 3 | 06/12/2023 |
7.0.6 | 5 | 06/12/2023 |
7.0.5 | 5 | 06/12/2023 |
7.0.4 | 3 | 06/12/2023 |
7.0.3 | 5 | 06/12/2023 |
7.0.2 | 6 | 06/12/2023 |
7.0.0 | 6 | 06/12/2023 |
6.2.0 | 4 | 06/12/2023 |
6.1.0 | 4 | 06/13/2023 |
6.0.0 | 2 | 06/12/2023 |
5.3.0 | 3 | 06/12/2023 |
5.2.0 | 6 | 06/14/2023 |
5.1.0 | 5 | 06/13/2023 |
5.0.0 | 4 | 06/14/2023 |
4.11.0 | 4 | 08/25/2023 |
4.10.0 | 5 | 02/20/2024 |
4.9.0 | 6 | 06/12/2023 |
4.8.0 | 4 | 06/13/2023 |
4.7.0 | 5 | 06/12/2023 |
4.6.5 | 4 | 06/12/2023 |
4.6.4 | 5 | 06/12/2023 |
4.6.2 | 3 | 06/13/2023 |
4.6.1 | 5 | 02/20/2024 |
4.6.0 | 5 | 06/12/2023 |
4.5.9 | 5 | 06/12/2023 |
4.5.8 | 4 | 06/11/2023 |
4.5.7 | 2 | 02/20/2024 |
4.5.6 | 3 | 06/12/2023 |
4.5.5 | 5 | 06/14/2023 |
4.5.4 | 4 | 06/12/2023 |
4.5.3 | 7 | 06/12/2023 |
4.5.2 | 5 | 06/12/2023 |
4.5.1 | 5 | 06/12/2023 |
4.5.0 | 5 | 06/12/2023 |
4.4.0 | 4 | 06/12/2023 |
4.3.0 | 3 | 06/12/2023 |
4.2.0 | 4 | 06/12/2023 |
4.1.0 | 4 | 06/13/2023 |
4.0.1 | 5 | 06/12/2023 |
4.0.0 | 6 | 06/13/2023 |
3.6.0 | 5 | 06/13/2023 |
3.4.0 | 4 | 06/13/2023 |
3.3.0 | 5 | 06/12/2023 |
3.2.0 | 7 | 06/13/2023 |
3.1.0 | 4 | 06/12/2023 |
3.0.0 | 4 | 06/12/2023 |
2.5.0 | 4 | 06/13/2023 |
2.4.0 | 4 | 06/12/2023 |
2.3.0 | 4 | 06/12/2023 |
2.2.0 | 6 | 06/13/2023 |
2.1.0 | 3 | 06/14/2023 |
2.0.0 | 3 | 06/13/2023 |
1.0.0.4 | 2 | 02/20/2024 |
1.0.0.3 | 3 | 02/20/2024 |
1.0.0.1 | 6 | 10/14/2023 |
1.0.0 | 4 | 06/12/2023 |