USAGE | OUTPUT | CUSTOMIZATION | DATA PROVIDERS | CREATION POLICY | CONFIGURATION | EXTENSIONS
An extensible framework to audit executing operations in .NET including support for .NET Framework ≥ 4.5 and NetCore ≥ 1.0 (NetStandard 1.3).
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, exceptions, including execution time and duration, 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, MongoDB, AzureBlob, DocumentDB or Redis.
Also extensions to audit different systems are provided, such as Entity Framework, MVC, WebAPI, WCF and SignalR.
To install the package run the following command on the Package Manager Console:
PM> Install-Package Audit.NET
Check the CHANELOG.md file.
The Audit Scope is the central object of this framework. It encapsulates an audit event, controlling its lifecycle. The Audit Event is an extensible information container of an audited operation. See the audit scope statechart.
Create an Audit Scope by calling the static AuditScope.Create
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);
}
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 to detect exceptions and calculate the duration by implicitly saving the event on disposal.
The first parameter of the 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<object>
to allow the library inspect the value at the beggining and at the disposal of the scope. It is not mandatory to supply a target object, pass null
when you don't want to track a specific object.
There is also a unified overload of the Create
method that accepts an instance of AuditScopeOptions
. Use this class to configure any of the available options for the scope:
var options = new AuditScopeOptions()
{
EventType = "MyEvent",
CreationPolicy = EventCreationPolicy.Manual,
ExtraFields = new { Action = this.Action },
AuditEvent = new MyCustomAuditEvent()
};
using (var scope = AuditScope.Create(options))
{
// ...
}
If you are not tracking an object, nor the duration of an event, you can use the CreateAndSave
shortcut method that logs an event immediately.
For example:
AuditScope.CreateAndSave("Event Type", new { ExtraField = "extra value" });
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("MyEvent", () => Status, 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 versions of the operations that saves 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);
// async manual saving
await auditScope.SaveAsync();
}
finally
{
// async disposal
await auditScope.DisposeAsync();
}
}
Note: Inside async methods, if you create the scope within a
using
statement, the event saving could take place when the scope is disposed, but theDispose
method is synchronous. As a workaround you can explicitly call theDisposeAsync()
method, or use combination ofSaveAsync()
andDiscard()
.
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"
},
"StartDate": "2016-08-23T11:33:14.653191-05:00",
"EndDate": "2016-08-23T11:33:23.1820786-05:00",
"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
}
}
}
The following tables describes 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 |
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 |
The AuditScope
object provides two methods to extend the event output.
- Use
SetCustomField()
method to add any object as a custom field of 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;
}
Custom fields are not limited to single properties, you can store any object as well, by default they will be JSON serialized.
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
}
The AuditScope
object has a Discard()
method to allow the user to discard an event.
For example, if you want to avoid saving the audit event under certain condition:
using (var scope = AuditScope.Create("SomeEvent", () => someTarget))
{
try
{
//some operation
Critical.Operation();
}
catch (Exception ex)
{
//If an exception is thown, discard the audit event
scope.Discard();
}
}
A data provider (or storage provider) 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 return a unique ID for the event.ReplaceEvent
: should update an event given its ID, this method is only called for Creation Policies Manual or InsertOnStartReplaceOnEnd.
If your data provider will support asynchronous operations, you must also implement the following methods:
InsertEventAsync
: Asynchoronous implementation of the InsertEvent method.ReplaceEventAsync
: Asynchoronous 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
: Asynchoronous 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)
{
var fileName = $"Log{Guid.NewGuid()}.json";
await File.WriteAllTextAsync(fileName, auditEvent.ToJson());
return fileName;
}
public override async Task ReplaceEventAsync(object eventId, AuditEvent auditEvent)
{
var fileName = eventId.ToString();
await File.WriteAllTextAsync(fileName, auditEvent.ToJson());
}
public override async Task<T> GetEventAsync<T>(object eventId)
{
var fileName = eventId.ToString();
return await GetFromFileAsync<T>(fileName);
}
}
You can set a default data provider assigning the DataProvider
property on the global Configuration
object. For example:
Audit.Core.Configuration.DataProvider = new MyCustomDataProvider();
Or using the fluent API UseCustomProvider
method:
Audit.Core.Configuration.Setup()
.UseCustomProvider(new MyCustomDataProvider());
See Configuration section for more information.
You can also set the data provider per-scope, by using an appropriate overload of the AuditScope.Create
method. For example:
AuditScope.Create("Order:Update", () => order, EventCreationPolicy.Manual, new MyCustomDataProvider());
Or:
AuditScope.Create(new AuditScopeOptions { DataProvider = new MyCustomDataProvider() });
As an anternative 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())));
The Data Providers included are summarized in the following table:
Data Provider | Package | Description | Configuration API |
---|---|---|---|
FileDataProvider | Audit.NET | Store the audit logs as files. Dynamically configure the directory and path. | .UseFileLogProvider() |
EventLogDataProvider | Audit.NET | Write the audit logs to the Windows EventLog. | .UseEventLogProvider() |
DynamicDataProvider | Audit.NET | Dynamically change the behavior at run-time. Define Insert and a Replace actions with lambda expressions. | .UseDynamicProvider() |
DynamicAsyncDataProvider | Audit.NET | Dynamically change the behavior at run-time. Define Insert and a Replace actions as asynchronous operations. | .UseDynamicAsyncProvider() |
SqlDataProvider | Audit.NET.SqlServer | Store the events as rows in a MS SQL Table, in JSON format. | .UseSqlServer() |
MySqlDataProvider | Audit.NET.MySql | Store the events as rows in a MySQL database table, in JSON format. | .UseMySql() |
PostgreSqlDataProvider | Audit.NET.PostgreSql | Store the events as rows in a PostgreSQL database table, in JSON format. | .UsePostgreSql() |
MongoDataProvider | Audit.NET.MongoDB | Store the events in a Mongo DB collection, in BSON format. | .UseMongoDB() |
AzureDbDataProvider | Audit.NET.AzureDocumentDB | Store the events in an Azure Document DB collection, in JSON format. | .UseAzureDocumentDB() |
AzureBlobDataProvider | Audit.NET.AzureStorage | Store the events in an Azure Blob Storage container, in JSON format. | .UseAzureBlobStorage() |
UdpDataProvider | Audit.NET.Udp | Send Audit Logs as UDP datagrams to a network. | .UseUdp() |
RedisDataProvider | Audit.NET.Redis | Store audit logs in Redis as Strings, Lists, SortedSets, Hashes or publish to a PubSub channel. | .UseRedis() |
Log4netDataProvider | Audit.NET.log4net | Store the audit events using Apache log4net™. | .UseLog4net() |
EntityFrameworkDataProvider | Audit.EntityFramework | Store EntityFramework audit events in the same EF context. | .UseEntityFramework() |
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 (on 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();
}
If you don't provide a Creation Policy, the default Creation Policy configured will be used (see the configuration section).
The following is the internal state machine representation of the AuditScope
object:
To change the default data provider, set the static property DataProvider
on Audit.Core.Configuration
class. This should be done prior to 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();
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.
To change the default creation policy, set the static property SetCreationPolicy
on Audit.Core.Configuration
class. This should be done prior to the AuditScope
creation, i.e. during application startup.
For example, to set the default creation policy to Manual:
Audit.Core.Configuration.CreationPolicy = EventCreationPolicy.Manual;
If you don't specify a Creation Policy, the default
Insert on End
will be used.
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 centain 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);
});
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.
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()
.UseFileLogProvider()
.WithCreationPolicy(EventCreationPolicy.Manual);
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")));
Audit.Core.Configuration.Setup()
.UseEventLogProvider(config => config
.SourcePath("My Audited Application")
.LogName("Application"))
.WithCreationPolicy(EventCreationPolicy.InsertOnEnd);
Audit.Core.Configuration.Setup()
.UseDynamicProvider(config => config
.OnInsert(ev => Console.WriteLine("{0}: {1}->{2}", ev.StartDate, ev.Environment.UserName, ev.EventType)));
The following packages are extensions to log interactions with different systems such as MVC, WebApi, WCF and Entity Framework:
Package | Description | |
---|---|---|
Audit.WCF | Generate detailed server-side audit logs for Windows Communication Foundation (WCF) service calls, by configuring a provided behavior. | |
Audit.EntityFramework | Generate detailed audit logs for CRUD operations on Entity Framework, by inheriting from a provided DbContext or IdentityDbContext . Includes support for EF 6 and EF 7 (EF Core). |
|
Audit.WebApi | Generate detailed audit logs by decorating Web API Methods and Controllers with an action filter attribute. Includes support for ASP.NET Core. | |
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.DynamicProxy | Generate detailed audit logs for any class without changing its code by using a proxy. | |
Audit.FileSystem | Generate audit logs by intercepting file system events via FileSystemWatcher. | |
Audit.SignalR | Generate audit logs for SignalR invokations by intercepting the hub processing |
Apart from the FileLog, EventLog and Dynamic event storage providers, there are others included in different packages:
Package | Description | |
---|---|---|
Audit.NET.SqlServer | Store the events as rows in a SQL Table, in JSON format. | |
Audit.NET.MySql | Store the events as rows in MySQL database, in JSON format. | |
Audit.NET.PostgreSql | Store the events as rows in a PostgreSQL database, in JSON format. | |
Audit.NET.MongoDB | Store the events in a Mongo DB Collection, in BSON format. | |
Audit.NET.AzureDocumentDB | Store the events in an Azure Document DB Collection, in JSON format. | |
Audit.NET.AzureStorage | Store the events in an Azure Blob Storage container, in JSON format. | |
Audit.NET.Udp | Send Audit Logs as UDP datagrams to a network. | |
Audit.NET.Redis | Store Audit Logs in a Redis database as String, List, Hash, Sorted Set or publishing to a Redis PubSub channel. | |
Audit.NET.log4net | Store the audit events using Apache log4net™. |
For detailed information on changes in new release refer to the change log.
If you like this project please contribute in any of the following ways:
- 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.
- Spread the word by blogging about it, or sharing it on social networks: