EntityFramework (EF) Audit Extension for Audit.NET library.
Automatically generates Audit Logs for EntityFramework's CRUD operations. Supporting EF 6 and EF 7 (EF Core).
Audit.EntityFramework provides the infrastructure to log interactions with the EF DbContext
. It can record detailed information about CRUD operations in your database.
NuGet Package
To install the package run the following command on the Package Manager Console:
PM> Install-Package Audit.EntityFramework
Or for the Strong-Named version:
PM> Install-Package Audit.EntityFramework.StrongName
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 MyEntities : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
}
to enable the audit log, you should change it to inherit from AuditDbContext
:
public class MyEntities : Audit.EntityFramework.AuditDbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
}
The library intercepts calls to SaveChanges
/ SaveChangesAsync
methods on the DbContext
and generates detailed audit logs.
Each call to SaveChanges
generates a new audit event that includes information of all the entities affected by the save operation.
The following settings 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.
Change the settings by decorating your DbContext with the AuditDbContext
attribute, for example:
[AuditDbContext(Mode = AuditOptionMode.OptOut, IncludeEntityObjects = false, AuditEventType = "{database}_{context}" )]
public class MyEntitites : Audit.EntityFramework.AuditDbContext
{
...
To exclude specific entities from the audit (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 (OptIn Mode), you can use the AuditInclude
attribute:
[AuditInclude]
public class Post
{
public int Id { get; set; }
...
}
You can also change the settings of your DbContext by accessing the properties with the same name as in the attribute. For example:
public class MyEntities : Audit.EntityFramework.AuditDbContext
{
public MyEntities()
{
AuditEventType = "{database}_{context}";
Mode = AuditOptionMode.OptOut;
IncludeEntityObjects = false;
}
}
You can also configure settings by using 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 MyEntities
, 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<MyEntities>(config => config
.IncludeEntityObjects()
.AuditEventType("{context}:{database}"))
.UseOptOut()
.IgnoreAny(t => t.Name.EndsWith("History"));
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.
To configure the output persistence mechanism please see Configuration and Data Providers sections.
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.
This is useful to, for example, save the audit logs in the same transaction as the CRUD operation being audited, so when the audit logging fails the audited operation is rolled back.
public class MyDbContext : AuditDbContext
{
public MyDbContext()
{
// Set an empty DynamicDataProvider to avoid saving on the data provider
AuditDataProvider = new DynamicDataProvider();
}
protected override void OnScopeCreated(AuditScope auditScope)
{
Database.BeginTransaction();
}
protected override void OnScopeSaving(AuditScope 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 that in the example above, since we want the event saving to be done on the
OnScopeSaving
method, we need to bypass the Data Provider and this can be done specifying an empty dynamic provider.
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 identifier (to group logs that are part of the same SQL transaction)
- Entity object graphs (optional with
IncludeEntityObjects
configuration) - Execution time and duration
With this information, you can measure performance, observe exceptions thrown or get statistics about usage of your database.
The following tables describes the Audit.EntityFramework output fields:
Field Name | Type | Description |
---|---|---|
Database | string | Name of the database affected |
ConnectionId | Guid | Unique client connection ID (only available when the connection is open at the beginning of the event) |
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. |
Entries | Array of EventEntry | Array with information about the entities affected by the audited operation |
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 |
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 | Bolean 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 |
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.
- Output sample for a failed insert operation:
{
"EventType": "Blogs_MyEntities",
"Environment": {
"UserName": "Federico",
"MachineName": "HP",
"DomainName": "HP",
"CallingMethodName": "Audit.UnitTest.AuditTests.TestEF()",
"Exception": "Exception: Exception from HRESULT: 0xE0434352",
"Culture": "en-GB"
},
"StartDate": "2016-09-06T21:11:57.7562152-05:00",
"EndDate": "2016-09-06T21:11:58.1039904-05:00",
"Duration": 348,
"EntityFrameworkEvent": {
"Database": "Blogs",
"ConnectionId": "593e082d-b6b5-440b-a048-ba223b247e9f",
"Entries": [{
"Table": "Posts",
"Action": "Insert",
"PrimaryKey": {
"Id": -2147482647
},
"ColumnValues": {
"Id": -2147482647,
"BlogId": 1,
"Content": "content",
"DateCreated": "2016-09-07T01:05:51.1972469-05:00",
"Title": "title VERY LONG_________________"
},
"Valid": false,
"ValidationResults": ["The field Title must be a string or array type with a maximum length of '20'."]
}],
"Result": 0,
"Success": false,
"ErrorMessage": "(DbUpdateException) An error occurred while updating the entries. See the inner exception for details. -> String or binary data would be truncated."
}
}
- Output sample for a successful multiple operation (update+delete) within a transaction:
{
"EventType": "Blogs_MyEntities",
"Environment": {
"UserName": "Federico",
"MachineName": "HP",
"DomainName": "HP",
"CallingMethodName": "Audit.UnitTest.AuditTests.TestEF()",
"Exception": null,
"Culture": "en-GB"
},
"StartDate": "2016-09-07T11:36:16.2643822-05:00",
"EndDate": "2016-09-07T11:36:20.410577-05:00",
"Duration": 4146,
"EntityFrameworkEvent": {
"Database": "Blogs",
"ConnectionId": "d37ddc34-8ecb-4f08-b95b-598807ff3cef",
"TransactionId": "d37ddc34-8ecb-4f08-b95b-598807ff3cef_1",
"Entries": [{
"Table": "Blogs",
"Action": "Update",
"PrimaryKey": {
"Id": 1
},
"Changes": [{
"ColumnName": "BloggerName",
"OriginalValue": "fede",
"NewValue": "Federico"
}],
"Valid": true
},
{
"Table": "Posts",
"Action": "Delete",
"PrimaryKey": {
"Id": 5
},
"ColumnValues": {
"Id": 5,
"BlogId": 2,
"Content": "this is an example",
"DateCreated": "2016-09-07T11:36:10.973",
"Title": "my post 5"
},
"Valid": true
}],
"Result": 2,
"Success": true
}
}