Audit.EntityFramework
27.0.3
dotnet add package Audit.EntityFramework --version 27.0.3
NuGet\Install-Package Audit.EntityFramework -Version 27.0.3
<PackageReference Include="Audit.EntityFramework" Version="27.0.3" />
paket add Audit.EntityFramework --version 27.0.3
#r "nuget: Audit.EntityFramework, 27.0.3"
// Install Audit.EntityFramework as a Cake Addin #addin nuget:?package=Audit.EntityFramework&version=27.0.3 // Install Audit.EntityFramework as a Cake Tool #tool nuget:?package=Audit.EntityFramework&version=27.0.3
Audit.EntityFramework
Entity Framework Audit Extension for Audit.NET library.
Automatically generates Audit Logs for EntityFramework's operations. Supporting EntityFramework and EntityFramework Core
This library provides the infrastructure to log interactions with the EF DbContext
.
It can record detailed information about CRUD operations in your database.
Install
NuGet Package
To install the package run the following command on the Package Manager Console:
PM> Install-Package Audit.EntityFramework
Or, if you use EntityFramework core:
PM> Install-Package Audit.EntityFramework.Core
Or, if you want to audit ASP.NET Identity entities, you must also install the Audit.EntityFramework.Identity
library:
PM> Install-Package Audit.EntityFramework.Identity
EF library version
The following table shows the entity framework package version used for each .NET framework and audit library:
<sub>Target</sub> \ <sup>Library</sup> | Audit.EntityFramework / Audit.EntityFramework.Identity |
Audit.EntityFramework.Core / Audit.EntityFramework.Identity.Core |
---|---|---|
.NET 4.6.2 | EntityFramework 6.5.0 | N/C |
.NET 4.7.2 | EntityFramework 6.5.0 | N/C |
.NET Standard 2.1 | EntityFramework 6.5.0 | Microsoft.EntityFrameworkCore 5.0.17 |
.NET 6.0 | EntityFramework 6.5.0 | Microsoft.EntityFrameworkCore 6.0.25 |
.NET 7.0 | EntityFramework 6.5.0 | Microsoft.EntityFrameworkCore 7.0.14 |
.NET 8.0 | EntityFramework 6.5.0 | Microsoft.EntityFrameworkCore 8.0.0 |
N/C: Not Compatible
Usage
High-Level SaveChanges Interception
In order to audit Insert, Delete and Update operations, you can use any of the three SaveChanges
interception mechanisms provided:
1. Inheriting from AuditDbContext
Change your EF context class to inherit from Audit.EntityFramework.AuditDbContext
instead of DbContext
.
For example, if you have a context like this:
public class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
}
In order to enable the audit log, you should change it to inherit from AuditDbContext
:
public class MyContext : AuditDbContext // <-- inherit from Audit.EntityFramework.AuditDbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
}
Note
If you're using IdentityDbContext instead of DbContext, you can install the package
Audit.EntityFramework.Identity
orAudit.EntityFramework.Identity.Core
and inherit from the classAuditIdentityDbContext
instead ofAuditDbContext
.
2. Without inheritance, overriding SaveChanges
You can use the library without changing the inheritance of your DbContext
.
In order to to that, you can define your DbContext
in the following way, overriding SaveChanges
and SaveChangesAsync
:
public class MyContext : DbContext
{
private readonly DbContextHelper _helper = new DbContextHelper();
private readonly IAuditDbContext _auditContext;
public MyContext(DbContextOptions<MyContext> options) : base(options)
{
_auditContext = new DefaultAuditContext(this);
_helper.SetConfig(_auditContext);
}
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
return _helper.SaveChanges(_auditContext, () => base.SaveChanges(acceptAllChangesOnSuccess));
}
public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
return await _helper.SaveChangesAsync(_auditContext, () => base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken));
}
}
Note
No other
SaveChanges
override is needed, since all the other overloads will call one of these two.
3. With the provided save changes interceptor
Save Changes Interceptors were introduced in EF Core 5.0.
If you can't change the inheritance of your DbContext
, and/or can't override the SaveChanges
, you can attach an instance of AuditSaveChangesInterceptor
to your DbContext configuration.
For example:
public class MyContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.AddInterceptors(new AuditSaveChangesInterceptor());
}
// ...
}
Or alternatively, when creating your DbContext:
var options = new DbContextOptionsBuilder()
.AddInterceptors(new AuditSaveChangesInterceptor())
.Options;
using (var ctx = new MyContext(options))
{
// ...
}
Or using DI, such as with ASP.NET Core:
builder.Services.AddDbContext<MyContext>(c => c
.UseSqlServer(CONNECTION_STRING)
.AddInterceptors(new AuditSaveChangesInterceptor())
Note
Notice that a new instance of the interceptor is registered for each DbContext instance. This is because the auditing interceptor contains state linked to the current context instance.
Considerations
- All the Save Changes interception methods produces the same output. You should use only one of these methods, otherwise you could get duplicated audit logs.
Low-Level Command Interception
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.
For example:
var options = new DbContextOptionsBuilder()
.AddInterceptors(new AuditCommandInterceptor())
.Options;
using (var ctx = new MyContext(options))
{
// ...
}
Or inside DbContext configuration:
public class MyDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.AddInterceptors(new AuditCommandInterceptor());
}
// ...
}
Or using DI, such as with ASP.NET Core:
builder.Services.AddDbContext<MyContext>(c => c
.UseSqlServer(CONNECTION_STRING)
.AddInterceptors(new AuditCommandInterceptor())
Note
The Command Interceptor generates a different type of audit output than the Save Changes Interceptor. Nevertheless, you can combine the Command Interceptor with any of the Save Changes interception mechanisms.
Configuration
Output
EF audit events are stored via a Data Provider. You can either use one of the available data providers or implement your own.
The Audit Data Provider can be configured in several ways:
Per
DbContext
instance by explicitly setting theAuditDataProvider
property. For example:public class MyContext : AuditDbContext { public MyContext() { AuditDataProvider = new SqlDataProvider(config => config...); } }
By registering an
AuditDataProvider
instance in the dependency injection container.For example:
public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); builder.Services.AddSingleton<AuditDataProvider>(new SqlDataProvider(config => config...)); } }
Globally, by setting the
AuditDataProvider
instance through theAudit.Core.Configuration.DataProvider
static property or theAudit.Core.Configuration.Use()
methods.For example:
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.
Settings (low-Level interceptor)
The low-level command interceptor can be configured by setting the AuditCommandInterceptor
properties, for example:
optionsBuilder.AddInterceptors(new AuditCommandInterceptor()
{
ExcludeNonQueryEvents = true,
AuditEventType = "{database}",
IncludeReaderResults = true
});
- LogParameterValues: Boolean value to indicate whether to log the command parameter values. By default (when null) it will depend on EnableSensitiveDataLogging setting on the DbContext.
- ExcludeReaderEvents: Boolean value to indicate whether to exclude the events handled by ReaderExecuting. Default is false to include the ReaderExecuting events.
- IncludeReaderEventsPredicate: Predicate to include the ReaderExecuting events based on the event data. By default, all the ReaderExecuting events are included. This predicate is ignored if ExcludeReaderEvents is set to true.
- ExcludeNonQueryEvents: Boolean value to indicate whether to exclude the events handled by NonQueryExecuting. Default is false to include the NonQueryExecuting events.
- ExcludeScalarEvents: Boolean value to indicate whether to exclude the events handled by ScalarExecuting. Default is false to include the ScalarExecuting events.
- AuditEventType: To indicate the event type to use on the audit event. (Default is the execute method name). Can contain the following placeholders:
- {context}: replaced with the Db Context type name.
- {database}: Replaced with the database name
- {method}: Replaced with the execute method name (ExecuteReader, ExecuteNonQuery or ExecuteScalar)
- IncludeReaderResults: Boolean value to indicate whether to include the query results to the audit output. Default is false.
Settings (High-Level interceptor)
The following settings for the high-level interceptor can be configured per DbContext or globally:
- Mode: To indicate the audit operation mode
- Opt-Out: All the entities are tracked by default, except those explicitly ignored. (Default)
- Opt-In: No entity is tracked by default, except those explicitly included.
- IncludeEntityObjects: To indicate if the output should contain the complete entity object graphs. (Default is false)
- AuditEventType: To indicate the event type to use on the audit event. (Default is the context name). Can contain the following placeholders:
- {context}: replaced with the Db Context type name.
- {database}: replaced with the database name.
- IncludeIndependantAssociations: Value to indicate if the Independant Associations should be included. Default is false. (Only for EF ⇐ 6.2)
Note
Note: EF Core ⇐ 3 does not support many-to-many relations without a join entity, and for EF Core 5 the many-to-many relations are normally included on the audit event entries.
- ExcludeTransactionId: Value to indicate if the Transaction IDs should be excluded from the output and not be retrieved (default is false to include the Transaction IDs).
- ExcludeValidationResults: Value to indicate if the entity validations should be avoided and excluded from the audit output. (Default is false)
- EarlySavingAudit: Value to indicate if the audit event should be saved before the entity saving operation takes place. (Default is false to save the audit event after the entity saving operation completes or fails)
- ReloadDatabaseValues: Value to indicate if the original values of the audited entities should be queried from database before saving the audit event.
The ReloadDatabaseValues configuration is beneficial for making modifications without explicitly retrieving the entity first. It can be enabled when using DbSet.Update or DbSet.Remove with an object that wasn't retrieved from the database. When enabled, it queries the database prior to any entity modification to record the original values in the audit event.
The following settings can be configured per entity type:
- IgnoredProperties: To indicate the entity's properties (columns) to be ignored on the audit logs.
- OverrideProperties: To override property values on the audit logs.
- FormatProperties: To indicate replacement functions for the property's values on the audit logs.
The Ignore, Override and Format settings are only applied to the Changes and ColumnValues collections on the EventEntry. The
Entity
object (if included) will not be affected by these settings.
Change the settings for a DbContext by decorating it with the AuditDbContext
attribute, for example:
[AuditDbContext(Mode = AuditOptionMode.OptOut, IncludeEntityObjects = false, AuditEventType = "{database}_{context}" )]
public class MyEntitites : Audit.EntityFramework.AuditDbContext
{
...
You can also use the Fluent API to configure the high-level interceptor settings globally.
Include/Ignore entities (tables)
To ignore specific entities on the audit (when using OptOut Mode), you can decorate your entity classes with the AuditIgnore
attribute, for example:
[AuditIgnore]
public class Blog
{
public int Id { get; set; }
...
}
Instead, to include specific entities to the audit (when using OptIn Mode), you can use the AuditInclude
attribute:
[AuditInclude]
public class Post
{
public int Id { get; set; }
...
}
Exclude properties (columns)
The AuditIgnore
attribute can be used on the entity's properties to indicate that its value should
not be included on the audit logs. For example to prevent storing passwords on the logs:
public class User
{
public int Id { get; set; }
[AuditIgnore]
public string Password { get; set; }
...
}
Override properties (columns)
The AuditOverride
attribute can be used to override a column value with a constant value.
For example to override the password values with a NULL value:
public class User
{
[AuditOverride(null)]
public string Password { get; set; }
...
}
Note you can also provide a replacement function of the value, please see next section.
Fluent API
You can configure the settings via a convenient Fluent API provided by the method Audit.EntityFramework.Configuration.Setup()
, this is the most straightforward way to configure the library.
For example, to configure a context called MyContext
, that should include the objects on the output, using the OptOut mode, excluding from the audit the entities whose name ends with History
:
Audit.EntityFramework.Configuration.Setup()
.ForContext<MyContext>(config => config
.IncludeEntityObjects()
.AuditEventType("{context}:{database}"))
.UseOptOut()
.IgnoreAny(t => t.Name.EndsWith("History"));
Another example configuring ignored, overriden and formatted column values. In this example, the Photo column is ignored, the OldPassword will be always null and the Password will be set to a number of stars equal to the number of password characters.
Audit.EntityFramework.Configuration.Setup()
.ForContext<MyContext>(config => config
.ForEntity<User>(_ => _
.Ignore(user => user.Photo)
.Override(user => user.OldPassword, null)
.Format(user => user.Password, pass => new String('*', pass.Length))));
In summary, you have three ways to configure the audit for the contexts:
- By accessing the properties on the
AuditDbContext
base class. - By decorating your context classes with
AuditDbContext
attribute and your entity classes withAuditIgnore
/AuditInclude
attributes. - By using the fluent API provided by the method
Audit.EntityFramework.Configuration.Setup()
All three can be used at the same time, and the precedence order is the order exposed in the above list.
Event Output
To configure the output persistence mechanism please see Configuration and Data Providers sections.
Overrides
The AuditDbContext
has the following virtual methods that can be overriden to provide your custom logic:
- OnScopeCreated: Called before the EF operation execution and after the
AuditScope
creation. - OnScopeSaving: Called after the EF operation execution and before the
AuditScope
saving. - OnScopeSaved: Called after the
AuditScope
saving.
This is useful to, for example, save the audit logs in the same transaction as the operation being audited, so when the audit logging fails the audited operation is rolled back.
public class MyDbContext : AuditDbContext
{
public MyDbContext()
{
// Set a NULL data provider, since log saving is done in this class
AuditDataProvider = new NullDataProvider();
}
public override void OnScopeCreated(IAuditScope auditScope)
{
Database.BeginTransaction();
}
public override void OnScopeSaving(IAuditScope auditScope)
{
try
{
// ... custom log saving ...
}
catch
{
// Rollback call is not mandatory. If exception thrown, the transaction won't get commited
Database.CurrentTransaction.Rollback();
throw;
}
Database.CurrentTransaction.Commit();
}
}
Note
In this example we want the event saving to be done on the
OnScopeSaving
method, so we must bypass the Data Provider and this can be done by setting aNullDataProvider
.
Output
Audit.EntityFramework output includes:
- Execution time and duration
- Environment information such as user, machine, domain and locale.
- Affected SQL database and table names
- Affected column data including primary key, original and new values
- Model validation results
- Exception details
- Transaction identifiers (to group logs that are part of the same SQL or ambient transaction)
- Entity object graphs (optional with
IncludeEntityObjects
configuration)
With this information, you can measure performance, observe exceptions thrown or get statistics about usage of your database.
Output details
SaveChanges audit output
The following tables describes the output fields for the SaveChanges interception:
Field Name | Type | Description |
---|---|---|
Database | string | Name of the database affected |
ConnectionId | Guid | A unique identifier for the database connection. |
ContextId | string | A unique identifier for the context instance and pool lease. |
TransactionId | string | Unique identifier for the DB transaction used on the audited operation (if any). To group events that are part of the same transaction. |
AmbientTransactionId | string | Unique identifier for the ambient transaction used on the audited operation (if any). To group events that are part of the same ambient transaction. |
Entries | Array of EventEntry | Array with information about the entities affected by the audited operation |
Associations | Array of AssociationEntry | Independant associations changes, many-to-many relations without a join table with changes (only for EF ⇐6.2, not available on EF Core) |
Result | integer | Result of the SaveChanges call. Is the number of objects affected by the operation. |
Success | boolean | Boolean to indicate if the operation was successful |
ErrorMessage | string | The exception thrown details (if any) |
Field Name | Type | Description |
---|---|---|
Table | string | Name of the affected table |
Name | string | The entity friendly name (only for EF Core ≥ 3) |
Action | string | Action type (Insert, Update or Delete) |
PrimaryKey | Object | Object with the affected entity's primary key value(s) |
ColumnValues | Object | Object with the affected entity's column values |
Changes | Array of ChangeObject | An array containing the modified columns with the original and new values (only available for Update operations) |
Entity | Object | The object representation of the .NET entity affected (optional) |
Valid | boolean | Boolean indicating if the entity passes the validations |
ValidationResults | Array of string | The validation messages when the entity validation fails |
Field Name | Type | Description |
---|---|---|
ColumnName | string | The column name that was updated |
OriginalValue | string | The original value before the update |
NewValue | string | The new value after the update |
Command Interceptor audit output
The following table describes the output fields for the low-level command interception:
Field Name | Type | Description |
---|---|---|
Database | string | Name of the database affected |
ConnectionId | Guid | A unique identifier for the database connection. |
ContextId | string | A unique identifier for the context instance and pool lease. |
Method | string | The command method executed (NonQuery, Scalar, Reader) |
CommandType | CommandType | The command type (Text, StoredProcedure, etc) |
CommandSource | CommandSource | The command source type (SaveChanges, LinqQuery, etc) |
CommandText | string | The command text |
Parameters | Dictionary | The parameter values, if any, when EnableSensitiveDataLogging is enabled |
IsAsync | boolean | Indicates whether the call was asynchronous |
Result | object | Result of the operation. Query results are only included when IncludeReaderResults is set to true. |
Success | boolean | Boolean to indicate if the operation was successful |
ErrorMessage | string | The exception thrown details (if any) |
Customization
Custom fields
You can add extra information to the events by calling the method AddAuditCustomField
on the DbContext
. For example:
using(var context = new MyEntitites())
{
...
context.AddAuditCustomField("UserName", userName);
...
context.SaveChanges();
}
Another way to customize the output is by using global custom actions, please see custom actions for more information.
Getting the entity framework event
The AuditDbContext
provides an alternative Save Changes operation (SaveChangesGetAudit()
method) to save the changes and get the generated EntityFrameworkEvent
object.
This is useful when you want to get the audit event information generated by a particular Save Changes operation.
For example:
// Save the changes and get the generated audit event
var efEvent = await _dbContext.SaveChangesGetAuditAsync();
// Log all the operations to the tables affected
foreach(var entry in efEvent.Entries)
{
Console.WriteLine($"{entry.Action} {entry.Table}");
}
Entity Framework Data Provider
If you plan to store the audit logs via EntityFramework, you can use the provided EntityFrameworkDataProvider
.
Use this to store the logs on audit tables handled by EntityFramework.
Note
Only the high-level audit events are processed by this data provider. Any other audit event, including the low-level events generated by the command interceptor, are ignored by the entity framework data provider.
For example, you want to audit Order
and OrderItem
tables into Audit_Order
and Audit_OrderItem
tables respectively,
and the structure of the Audit_*
tables mimic the audited table plus some fields like the event date, an action and the username:
Note
By default, the library uses the same
DbContext
instance audited to store the audit logs. This is not mandatory and the recommendation is to provide a different DbContext instance per audit event by using theUseDbcontext()
fluent API.
EF Provider configuration
To set the EntityFramework data provider globally, set the static Audit.Core.Configuration.DataProvider
property to a new EntityFrameworkDataProvider
:
Audit.Core.Configuration.DataProvider = new EntityFrameworkDataProvider()
{
DbContextBuilder = ev => new OrderDbContext(),
AuditTypeMapper = (t, ee) => t == typeof(Order) ? typeof(OrderAudit) : t == typeof(Orderline) ? typeof(OrderlineAudit) : null,
AuditEntityAction = (evt, entry, auditEntity) =>
{
var a = (dynamic)auditEntity;
a.AuditDate = DateTime.UtcNow;
a.UserName = evt.Environment.UserName;
a.AuditAction = entry.Action; // Insert, Update, Delete
return Task.FromResult(true); // return false to ignore the audit
}
};
Or use the fluent API UseEntityFramework
method, this is the recommended approach:
Audit.Core.Configuration.Setup()
.UseEntityFramework(ef => ef
.UseDbContext<OrderDbContext>()
.AuditTypeExplicitMapper(m => m
.Map<Order, OrderAudit>()
.Map<Orderline, OrderlineAudit>()
.AuditEntityAction<IAudit>((evt, entry, auditEntity) =>
{
auditEntity.AuditDate = DateTime.UtcNow;
auditEntity.UserName = evt.Environment.UserName;
auditEntity.AuditAction = entry.Action; // Insert, Update, Delete
})
)
);
EF Provider Options
Mandatory:
- UseDbContext: A function that returns the DbContext to use for storing the audit events, by default it will use the same context being audited.
- DisposeDbContext: A boolean value to indicate if the audit DbContext should be disposed after saving the audit. Default is false.
- AuditTypeMapper: A function that maps an entity type to its audited type (i.e. Order → Audit_Order, etc).
- ExplicitMapper: An alternative mapper, as a function that excplicitly maps an entry to its audited type, useful to configure mapping when no entity type is associated with a table, or to setup complex mapping rules.
- AuditEntityCreator: An alternative to the mapper, as a function that creates the audit entity instance from the Event Entry and the Audit DbContext. Useful to control the Audit Entity object creation for example when using change-tracking proxies.
- AuditEntityAction: An action to perform on the audit entity before saving it, for example to update specific audit properties like user name or the audit date. It can also be used to filter out audit entities. Make this function return a boolean value to indicate whether to include the entity on the output log.
- IgnoreMatchedProperties: Set to true to avoid the property values copy from the entity to the audited entity (default is false).
- IgnoreMatchedPropertiesFunc: Allows to selectively ignore property matching on certain types. It's a function that receives the audit entity type and returns a boolean to indicate if the property matching must be ignored for that type.
EF Provider configuration examples
The UseEntityFramework
method provides several ways to indicate the Type Mapper and the Audit Action.
Map by type name:
You can map the audited entity to its audit log entity by the entity name using the AuditTypeNameMapper
method, for example to prepend Audit_
to the entity name.
This assumes both entity types are defined on the same assembly and namespace:
Audit.Core.Configuration.Setup()
.UseEntityFramework(x => x
.AuditTypeNameMapper(typeName => "Audit_" + typeName)
.AuditEntityAction((ev, ent, auditEntity) =>
{
// auditEntity is object
((dynamic)auditEntity).AuditDate = DateTime.UtcNow;
}));
the AuditEvent (shown here as ev
) in an instance of AuditEventEntityFramework
. As such, it can be casted to that type or by using the helper method ev.GetEntityFrameworkEvent()
.
Audit.Core.Configuration.Setup()
.UseEntityFramework(x => x
.AuditTypeNameMapper(typeName => "Audit_" + typeName)
.AuditEntityAction<IAudit>((ev, ent, auditEntity) =>
{
var entityFrameworkEvent = ev.GetEntityFrameworkEvent();
auditEntity.TransactionId = entityFrameworkEvent.TransactionId;
}));
Common action:
If your audit log entities implements a common interface or base class, you can use the generic version of the AuditEntityAction
method
to configure the action to be performed to each audit trail entity before saving. Also this action can be asynchronous, for example:
Audit.Core.Configuration.Setup()
.UseEntityFramework(x => x
.AuditTypeNameMapper(typeName => "Audit_" + typeName)
.AuditEntityAction<IAudit>(async (ev, ent, auditEntity) =>
{
// auditEntity is of IAudit type
auditEntity.AuditDate = DateTime.UtcNow;
auditEntity.SomeValue = await GetValueAsync();
}));
Use the explicit mapper to provide granular configuration per audit type:
Audit.Core.Configuration.Setup()
.UseEntityFramework(x => x
.AuditTypeExplicitMapper(m => m
.Map<Order, Audit_Order>((order, auditOrder) =>
{
// This action is executed only for Audit_Order entities
auditOrder.Status = "Order-" + order.Status;
})
.Map<OrderItem, Audit_OrderItem>()
.AuditEntityAction<IAudit>((ev, ent, auditEntity) =>
{
// This common action executes for every audited entity
auditEntity.AuditDate = DateTime.UtcNow;
})));
Ignore certain entities on the audit log:
Audit.Core.Configuration.Setup()
.UseEntityFramework(x => x
.AuditTypeExplicitMapper(m => m
.Map<Order, Audit_Order>((order, auditOrder) =>
{
if (auditOrder.Status == "Expired")
{
return false; // don't want to audit orders in "expired" status
}
auditOrder.AuditDate = DateTime.UtcNow;
return true;
})));
Custom DbContext instance:
To set a custom DbContext instance for storing the audit events, for example when your Audit_* entities
are defined in a different database and context (i.e. AuditDatabaseDbContext
):
Audit.Core.Configuration.Setup()
.UseEntityFramework(x => x
.UseDbContext<AuditDatabaseDbContext>()
.DisposeDbContext()
.AuditTypeExplicitMapper(m => m
.Map<Order, Audit_Order>()
.AuditEntityAction<IAudit>((ev, ent, auditEntity) =>
{
auditEntity.AuditDate = DateTime.UtcNow;
})));
Map multiple entity types to the same audit type with independent actions:
When you want to store the audit logs of different entities in the same audit table, for example:
Audit.Core.Configuration.Setup()
.UseEntityFramework(ef => ef
.AuditTypeExplicitMapper(m => m
.Map<Blog, AuditLog>((blog, audit) =>
{
// Action for Blog -> AuditLog
audit.TableName = "Blog";
audit.TablePK = blog.Id;
audit.Title = blog.Title;
})
.Map<Post, AuditLog>((post, audit) =>
{
// Action for Post -> AuditLog
audit.TableName = "Post";
audit.TablePK = post.Id;
audit.Title = post.Title;
})
.AuditEntityAction<AuditLog>((evt, entry, audit) =>
{
// Common action on AuditLog
audit.AuditDate = DateTime.UtcNow;
audit.AuditAction = entry.Action;
audit.AuditUsername = Environment.UserName;
}))
.IgnoreMatchedProperties(true));
Another example for all entities mapping to a single audit log table that stores the changes in a JSON column:
Audit.Core.Configuration.Setup()
.UseEntityFramework(_ => _
.AuditTypeMapper(t => typeof(AuditLog))
.AuditEntityAction<AuditLog>((ev, entry, entity) =>
{
entity.AuditData = entry.ToJson();
entity.EntityType = entry.EntityType.Name;
entity.AuditDate = DateTime.Now;
entity.AuditUser = Environment.UserName;
entity.TablePk = entry.PrimaryKey.First().Value.ToString();
})
.IgnoreMatchedProperties(true));
Note
Notice the use of
.IgnoreMatchedProperties(true)
to avoid the library trying to set properties automatically by matching names between the audited entities and the typeAuditLog
.
Map an entity type to multiple audit types, depending on the modified entry:
When you want to save audit logs to different tables for the same entity, for example, if you have different audit tables per operation:
Audit.Core.Configuration.Setup()
.UseEntityFramework(ef => ef.AuditTypeExplicitMapper(m => m
.Map<Blog>(
mapper: entry => entry.Action == "Update" ? typeof(Audit_Updates_Blog) : typeof(Audit_Blog),
entityAction: (ev, entry, entity) =>
{
if (entity is Audit_Updates_Blog upd)
{
// action for updates
}
else if (entity is Audit_Blog etc)
{
// action for insert/delete
}
})
.AuditEntityAction<IAuditLog>((evt, entry, auditEntity) =>
{
// common action...
})));
- Updates to
Blog
table → Audit toAudit_Updates_Blog
table- Any other operation on
Blog
table → Audit toAudit_Blog
table
Map Many to Many relations without join entity:
When you want to audit many to many relations which are not mapped to an entity type, i.e. implicitly created join tables.
You have to use the AuditTypeExplicitMapper
and set up the mapping of the relation table by using MapTable
or MapExplicit
methods.
For example, consider the following model:
There are two entities, Post
and Tag
with a Many to Many relation between them (note there is no relation entity).
Also you want to audit the Post
and Tag
tables to the Audit_Post
and Audit_Tag
tables respectively, and
you want to audit the PostTag
relation table to an Audit_PostTag
table.
Audit.Core.Configuration.Setup()
.UseEntityFramework(_ => _
.UseDbContext<YourAuditDbContext>()
.DisposeDbContext()
.AuditTypeExplicitMapper(map => map
.Map<Post, Audit_Post>()
.Map<Tag, Audit_Tag>()
.MapTable<Audit_PostTag>("PostTag", (EventEntry ent, Audit_PostTag auditPostTag) =>
{
auditPostTag.PostId = ent.ColumnValues["PostsId"];
auditPostTag.TagId = ent.ColumnValues["TagsId"];
})
.AuditEntityAction((ev, entry, auditEntity) =>
{
((dynamic)auditEntity).AuditAction = entry.Action;
((dynamic)auditEntity).AuditDate = DateTime.UtcNow;
})));
The first parameter of
MapTable
is the table name to which the mapping will apply. The generic parameter is the target audit type. You can optionally pass an action to execute on the audit entity as the second parameter. If property matching is enabled for the target type, the framework will map the Column values to the entity Property values.
Map via Factory:
When you need to control the Audit Entity creation, for example when using change-tracking proxies,
you can use the AuditEntityCreator
to specify a factory that creates the Audit Entity for a given entry.
Audit.Core.Configuration.Setup()
.UseEntityFramework(ef => ef
.UseDbContext<YourAuditDbContext>()
.DisposeDbContext()
.AuditEntityCreator(auditDbContext => auditDbContext.CreateProxy<AuditLog>())
.AuditEntityAction<AuditLog>((ev, ent, auditEntity) =>
{
auditEntity.DateTime = DateTime.Now;
auditEntity.Action = ent.Action;
auditEntity.Table = ent.Table;
})
.IgnoreMatchedProperties());
Another example of an audit Entity factory, but mapping to different entity types depending on the audited table:
Audit.Core.Configuration.Setup()
.UseEntityFramework(ef => ef
.UseDbContext<YourAuditDbContext>()
.DisposeDbContext()
.AuditEntityCreator((auditDbContext, entry) => entry.Table switch
{
"Customer" => auditDbContext.CreateProxy<AuditCustomer>(),
"User" => auditDbContext.CreateProxy<AuditUser>(),
_ => auditDbContext.CreateProxy<AuditLog>()
})
.AuditEntityAction<IAuditLog>((ev, ent, auditEntity) =>
{
auditEntity.DateTime = DateTime.Now;
auditEntity.Action = ent.Action;
auditEntity.Table = ent.Table;
})
.IgnoreMatchedProperties());
);
Contribute
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.
- Support the project by becoming a Backer:
- Spread the word by blogging about it, or sharing it on social networks: <p class="share-buttons"> <a href="https://www.facebook.com/sharer/sharer.php?u=https://nuget.org/packages/Audit.NET/&t=Check+out+Audit.NET" target="_blank"> <img width="24" height="24" alt="Share this package on Facebook" src="https://nuget.org/Content/gallery/img/facebook.svg" / > </a> <a href="https://twitter.com/intent/tweet?url=https://nuget.org/packages/Audit.NET/&text=Check+out+Audit.NET" target="_blank"> <img width="24" height="24" alt="Tweet this package" src="https://nuget.org/Content/gallery/img/twitter.svg" /> </a> </p>
- Make a donation via PayPal
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.1 is compatible. |
.NET Framework | net462 is compatible. net463 was computed. net47 was computed. net471 was computed. net472 is compatible. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETFramework 4.6.2
- Audit.NET (>= 27.0.3)
- EntityFramework (>= 6.5.0)
- System.Text.Json (>= 6.0.9)
-
.NETFramework 4.7.2
- Audit.NET (>= 27.0.3)
- EntityFramework (>= 6.5.0)
- System.Data.DataSetExtensions (>= 4.5.0)
- System.Text.Json (>= 6.0.9)
-
.NETStandard 2.1
- Audit.NET (>= 27.0.3)
- EntityFramework (>= 6.5.0)
- System.Text.Json (>= 6.0.9)
-
net6.0
- Audit.NET (>= 27.0.3)
- EntityFramework (>= 6.5.0)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Audit.EntityFramework:
Package | Downloads |
---|---|
Audit.EntityFramework.Identity
Generate Audit Logs from EntityFramework identity context changes |
|
Anthology.Data.Infrastructure
Package Description |
|
RezisFramework
Package Description |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
27.0.3 | 151 | 9/25/2024 |
27.0.2 | 150 | 9/19/2024 |
27.0.1 | 176 | 9/4/2024 |
27.0.0 | 954 | 9/3/2024 |
26.0.1 | 227 | 8/22/2024 |
26.0.0 | 1,240 | 7/19/2024 |
25.0.7 | 486 | 7/4/2024 |
25.0.6 | 187 | 6/24/2024 |
25.0.5 | 188 | 6/18/2024 |
25.0.4 | 1,783 | 3/24/2024 |
25.0.3 | 868 | 3/13/2024 |
25.0.2 | 249 | 3/12/2024 |
25.0.1 | 331 | 2/28/2024 |
25.0.0 | 2,632 | 2/16/2024 |
24.0.1 | 370 | 2/12/2024 |
24.0.0 | 277 | 2/12/2024 |
23.0.0 | 885 | 12/14/2023 |
22.1.0 | 933 | 12/9/2023 |
22.0.2 | 1,429 | 12/1/2023 |
22.0.1 | 634 | 11/16/2023 |
22.0.0 | 538 | 11/14/2023 |
21.1.0 | 1,463 | 10/9/2023 |
21.0.4 | 1,250 | 9/15/2023 |
21.0.3 | 3,961 | 7/9/2023 |
21.0.2 | 836 | 7/6/2023 |
21.0.1 | 1,732 | 5/27/2023 |
21.0.0 | 1,587 | 4/15/2023 |
20.2.4 | 1,089 | 3/27/2023 |
20.2.3 | 1,079 | 3/17/2023 |
20.2.2 | 951 | 3/14/2023 |
20.2.1 | 948 | 3/11/2023 |
20.2.0 | 1,044 | 3/7/2023 |
20.1.6 | 8,280 | 2/23/2023 |
20.1.5 | 1,737 | 2/9/2023 |
20.1.4 | 1,175 | 1/28/2023 |
20.1.3 | 7,714 | 12/21/2022 |
20.1.2 | 4,464 | 12/14/2022 |
20.1.1 | 1,208 | 12/12/2022 |
20.1.0 | 1,208 | 12/4/2022 |
20.0.4 | 2,754 | 11/30/2022 |
20.0.3 | 2,011 | 10/28/2022 |
20.0.2 | 1,347 | 10/26/2022 |
20.0.1 | 1,437 | 10/21/2022 |
20.0.0 | 1,788 | 10/1/2022 |
19.4.1 | 7,165 | 9/10/2022 |
19.4.0 | 1,530 | 9/2/2022 |
19.3.0 | 1,649 | 8/23/2022 |
19.2.2 | 2,064 | 8/11/2022 |
19.2.1 | 1,525 | 8/6/2022 |
19.2.0 | 1,717 | 7/24/2022 |
19.1.4 | 7,910 | 5/23/2022 |
19.1.3 | 1,452 | 5/22/2022 |
19.1.2 | 1,523 | 5/18/2022 |
19.1.1 | 4,824 | 4/28/2022 |
19.1.0 | 1,765 | 4/10/2022 |
19.0.7 | 5,906 | 3/13/2022 |
19.0.6 | 1,557 | 3/7/2022 |
19.0.5 | 8,113 | 1/28/2022 |
19.0.4 | 1,656 | 1/23/2022 |
19.0.3 | 38,099 | 12/14/2021 |
19.0.2 | 1,246 | 12/11/2021 |
19.0.1 | 18,362 | 11/20/2021 |
19.0.0 | 1,399 | 11/11/2021 |
19.0.0-rc.net60.2 | 176 | 9/26/2021 |
19.0.0-rc.net60.1 | 218 | 9/16/2021 |
18.1.6 | 16,248 | 9/26/2021 |
18.1.5 | 2,046 | 9/7/2021 |
18.1.4 | 1,550 | 9/6/2021 |
18.1.3 | 8,180 | 8/19/2021 |
18.1.2 | 2,010 | 8/8/2021 |
18.1.1 | 1,375 | 8/5/2021 |
18.1.0 | 2,469 | 8/1/2021 |
18.0.1 | 1,451 | 7/30/2021 |
18.0.0 | 2,010 | 7/26/2021 |
17.0.8 | 1,943 | 7/7/2021 |
17.0.7 | 2,952 | 6/16/2021 |
17.0.6 | 2,507 | 6/5/2021 |
17.0.5 | 2,486 | 5/28/2021 |
17.0.4 | 3,425 | 5/4/2021 |
17.0.3 | 1,451 | 5/1/2021 |
17.0.2 | 21,443 | 4/22/2021 |
17.0.1 | 1,396 | 4/18/2021 |
17.0.0 | 3,548 | 3/26/2021 |
16.5.6 | 1,454 | 3/25/2021 |
16.5.5 | 2,304 | 3/23/2021 |
16.5.4 | 1,888 | 3/9/2021 |
16.5.3 | 1,562 | 2/26/2021 |
16.5.2 | 1,478 | 2/23/2021 |
16.5.1 | 1,345 | 2/21/2021 |
16.5.0 | 1,643 | 2/17/2021 |
16.4.5 | 1,746 | 2/15/2021 |
16.4.4 | 2,861 | 2/5/2021 |
16.4.3 | 1,745 | 1/27/2021 |
16.4.2 | 1,747 | 1/22/2021 |
16.4.1 | 1,432 | 1/21/2021 |
16.4.0 | 10,809 | 1/11/2021 |
16.3.3 | 1,619 | 1/8/2021 |
16.3.2 | 1,471 | 1/3/2021 |
16.3.1 | 1,964 | 12/31/2020 |
16.3.0 | 1,479 | 12/30/2020 |
16.2.1 | 1,509 | 12/27/2020 |
16.2.0 | 7,516 | 10/13/2020 |
16.1.5 | 2,381 | 10/4/2020 |
16.1.4 | 4,043 | 9/17/2020 |
16.1.3 | 2,946 | 9/13/2020 |
16.1.2 | 1,522 | 9/9/2020 |
16.1.1 | 1,663 | 9/3/2020 |
16.1.0 | 1,724 | 8/19/2020 |
16.0.3 | 2,302 | 8/15/2020 |
16.0.2 | 1,669 | 8/9/2020 |
16.0.1 | 1,564 | 8/8/2020 |
16.0.0 | 3,852 | 8/7/2020 |
15.3.0 | 10,289 | 7/23/2020 |
15.2.3 | 2,085 | 7/14/2020 |
15.2.2 | 9,695 | 5/19/2020 |
15.2.1 | 2,023 | 5/12/2020 |
15.2.0 | 1,687 | 5/9/2020 |
15.1.1 | 2,550 | 5/4/2020 |
15.1.0 | 2,320 | 4/13/2020 |
15.0.5 | 15,817 | 3/18/2020 |
15.0.4 | 4,877 | 2/28/2020 |
15.0.3 | 1,629 | 2/26/2020 |
15.0.2 | 8,119 | 1/20/2020 |
15.0.1 | 2,552 | 1/10/2020 |
15.0.0 | 3,356 | 12/17/2019 |
14.9.1 | 4,267 | 11/30/2019 |
14.9.0 | 1,704 | 11/29/2019 |
14.8.1 | 1,719 | 11/26/2019 |
14.8.0 | 2,910 | 11/20/2019 |
14.7.0 | 8,642 | 10/9/2019 |
14.6.6 | 1,680 | 10/8/2019 |
14.6.5 | 9,052 | 9/27/2019 |
14.6.4 | 4,767 | 9/21/2019 |
14.6.3 | 39,601 | 8/12/2019 |
14.6.2 | 3,456 | 8/3/2019 |
14.6.1 | 1,723 | 8/3/2019 |
14.6.0 | 23,851 | 7/26/2019 |
14.5.7 | 3,278 | 7/18/2019 |
14.5.6 | 11,018 | 7/10/2019 |
14.5.5 | 7,990 | 7/1/2019 |
14.5.4 | 2,478 | 6/17/2019 |
14.5.3 | 8,898 | 6/5/2019 |
14.5.2 | 6,531 | 5/30/2019 |
14.5.1 | 2,383 | 5/28/2019 |
14.5.0 | 11,557 | 5/24/2019 |
14.4.0 | 2,026 | 5/22/2019 |
14.3.4 | 8,345 | 5/14/2019 |
14.3.3 | 1,841 | 5/9/2019 |
14.3.2 | 2,889 | 4/30/2019 |
14.3.1 | 1,855 | 4/27/2019 |
14.3.0 | 1,957 | 4/24/2019 |
14.2.3 | 1,984 | 4/17/2019 |
14.2.2 | 8,527 | 4/10/2019 |
14.2.1 | 17,003 | 4/5/2019 |
14.2.0 | 14,436 | 3/16/2019 |
14.1.1 | 2,807 | 3/8/2019 |
14.1.0 | 5,546 | 2/11/2019 |
14.0.4 | 9,107 | 1/31/2019 |
14.0.3 | 8,296 | 1/22/2019 |
14.0.2 | 6,750 | 12/15/2018 |
14.0.1 | 2,449 | 11/29/2018 |
14.0.0 | 2,469 | 11/19/2018 |
13.3.0 | 2,076 | 11/16/2018 |
13.2.2 | 1,946 | 11/15/2018 |
13.2.1 | 1,940 | 11/13/2018 |
13.2.0 | 11,930 | 10/31/2018 |
13.1.5 | 1,933 | 10/31/2018 |
13.1.4 | 4,475 | 10/25/2018 |
13.1.3 | 2,432 | 10/18/2018 |
13.1.2 | 4,161 | 9/12/2018 |
13.1.1 | 1,937 | 9/11/2018 |
13.1.0 | 1,901 | 9/11/2018 |
13.0.0 | 2,150 | 8/29/2018 |
12.3.6 | 1,698 | 8/29/2018 |
12.3.5 | 3,260 | 8/22/2018 |
12.3.4 | 1,779 | 8/21/2018 |
12.3.3 | 26,561 | 8/21/2018 |
12.3.2 | 1,760 | 8/20/2018 |
12.3.1 | 1,772 | 8/20/2018 |
12.3.0 | 1,770 | 8/20/2018 |
12.2.2 | 1,861 | 8/15/2018 |
12.2.1 | 3,273 | 8/9/2018 |
12.2.0 | 1,823 | 8/8/2018 |
12.1.11 | 1,960 | 7/30/2018 |
12.1.10 | 1,942 | 7/20/2018 |
12.1.9 | 2,051 | 7/10/2018 |
12.1.8 | 1,943 | 7/2/2018 |
12.1.7 | 11,821 | 6/7/2018 |
12.1.6 | 20,742 | 6/4/2018 |
12.1.5 | 2,152 | 6/2/2018 |
12.1.4 | 2,259 | 5/25/2018 |
12.1.3 | 4,130 | 5/16/2018 |
12.1.2 | 2,292 | 5/15/2018 |
12.1.1 | 2,306 | 5/14/2018 |
12.1.0 | 2,646 | 5/9/2018 |
12.0.7 | 10,941 | 5/5/2018 |
12.0.6 | 2,449 | 5/4/2018 |
12.0.5 | 2,307 | 5/3/2018 |
12.0.4 | 3,236 | 4/30/2018 |
12.0.3 | 2,397 | 4/30/2018 |
12.0.2 | 2,359 | 4/27/2018 |
12.0.1 | 2,357 | 4/25/2018 |
12.0.0 | 2,328 | 4/22/2018 |
11.2.0 | 2,499 | 4/11/2018 |
11.1.0 | 2,393 | 4/8/2018 |
11.0.8 | 2,841 | 3/26/2018 |
11.0.7 | 2,394 | 3/20/2018 |
11.0.6 | 6,163 | 3/7/2018 |
11.0.5 | 2,234 | 2/22/2018 |
11.0.4 | 2,552 | 2/14/2018 |
11.0.3 | 2,402 | 2/12/2018 |
11.0.2 | 3,291 | 2/9/2018 |
11.0.1 | 3,543 | 1/29/2018 |
11.0.0 | 2,614 | 1/15/2018 |
10.0.3 | 2,759 | 12/29/2017 |
10.0.2 | 2,149 | 12/26/2017 |
10.0.1 | 2,584 | 12/18/2017 |
10.0.0 | 2,142 | 12/18/2017 |
9.3.0 | 2,372 | 12/17/2017 |
9.2.0 | 2,201 | 12/17/2017 |
9.1.3 | 6,102 | 12/5/2017 |
9.1.2 | 2,957 | 11/27/2017 |
9.1.1 | 2,417 | 11/21/2017 |
9.1.0 | 2,238 | 11/21/2017 |
9.0.1 | 2,321 | 11/11/2017 |
9.0.0 | 2,098 | 11/10/2017 |
8.7.0 | 2,214 | 11/9/2017 |
8.6.0 | 2,243 | 11/9/2017 |
8.5.0 | 7,488 | 10/3/2017 |
8.4.0 | 2,174 | 10/3/2017 |
8.3.1 | 2,379 | 9/8/2017 |
8.3.0 | 2,180 | 9/8/2017 |
8.2.0 | 2,271 | 9/4/2017 |
8.1.0 | 2,386 | 8/22/2017 |
8.0.0 | 2,338 | 8/19/2017 |
7.1.3 | 2,222 | 8/14/2017 |
7.1.2 | 2,295 | 8/2/2017 |
7.1.1 | 2,711 | 7/26/2017 |
7.1.0 | 2,781 | 7/5/2017 |
7.0.9 | 2,183 | 6/28/2017 |
7.0.8 | 1,989 | 6/19/2017 |
7.0.6 | 3,462 | 4/7/2017 |
7.0.5 | 2,207 | 3/21/2017 |
7.0.4 | 2,024 | 3/21/2017 |
7.0.3 | 2,001 | 3/20/2017 |
7.0.2 | 2,001 | 3/13/2017 |
7.0.0 | 2,232 | 3/1/2017 |
6.2.0 | 2,064 | 2/25/2017 |
6.1.0 | 6,104 | 2/14/2017 |
6.0.0 | 2,111 | 2/9/2017 |
5.3.0 | 1,943 | 2/5/2017 |
5.2.0 | 1,888 | 1/26/2017 |
5.1.0 | 1,900 | 1/19/2017 |
5.0.0 | 1,953 | 1/7/2017 |
4.11.0 | 1,973 | 1/5/2017 |
4.10.0 | 1,910 | 12/31/2016 |
4.9.0 | 1,931 | 12/26/2016 |
4.8.0 | 1,961 | 12/17/2016 |
4.7.0 | 2,039 | 12/8/2016 |
4.6.5 | 1,989 | 12/4/2016 |
4.6.4 | 1,962 | 11/25/2016 |
4.6.2 | 2,983 | 11/18/2016 |
4.6.1 | 1,960 | 11/15/2016 |
4.6.0 | 1,990 | 11/11/2016 |
4.5.9 | 2,052 | 11/2/2016 |
4.5.8 | 1,987 | 11/2/2016 |
4.5.7 | 1,893 | 10/26/2016 |
4.5.6 | 2,006 | 10/6/2016 |
4.5.5 | 1,886 | 10/3/2016 |
4.5.4 | 1,931 | 10/2/2016 |
4.5.3 | 1,858 | 9/30/2016 |
4.5.2 | 1,880 | 9/28/2016 |
4.5.1 | 1,930 | 9/28/2016 |
4.5.0 | 1,933 | 9/28/2016 |
4.4.0 | 2,035 | 9/23/2016 |
4.3.0 | 2,011 | 9/22/2016 |
4.2.0 | 2,167 | 9/19/2016 |
4.1.0 | 1,953 | 9/13/2016 |
4.0.2 | 2,119 | 9/9/2016 |
4.0.1 | 1,964 | 9/9/2016 |
4.0.0 | 1,932 | 9/9/2016 |
3.6.1 | 1,876 | 9/7/2016 |
3.6.0 | 1,873 | 9/7/2016 |
3.4.0 | 2,243 | 9/7/2016 |