Entity Framework Classic Audit

Description

The EF Audit feature allows you to create an audit trail of all changes that occured when saving in Entity Framework.

The audit trail can be automatically saved in a database or log file.

public class EntityContext : DbContext
{
	public EntityContext() : base(FiddleHelper.GetConnectionStringSqlServer())
	{
		// Add your configuration here
		this.Configuration.Audit.AutoSaveSet = this.XmlAuditEntries;
		this.Configuration.Audit.IsEnabled = true;
	}
	
	public DbSet<Customer> Customers { get; set; }
	public DbSet<XmlAuditEntry> XmlAuditEntries { get; set; }
}

public static void Main()
{
	using (var context = new EntityContext())
	{
		context.Customers.Add(new Customer() { Name = "Customer_A", Description = "Description" });
		
		// Save changes and Audit trail in database
		context.SaveChanges();
		
		// Display Audit trail
		var auditEntries = context.XmlAuditEntries.ToAuditEntries();
		FiddleHelper.WriteTable("1 - Audit Entries", auditEntries);
		FiddleHelper.WriteTable("2 - Audit Properties", auditEntries.SelectMany(x => x.Properties));
	}
}

Try it

This feature allows you to handle various scenario such as:

What is supported?

  • SaveChanges()
  • BatchSaveChanges()
  • BulkSaveChanges()
  • All types of modifications:
    • EntityAdded
    • EntityModified
    • EntityDeleted
    • EntitySoftDeleted
    • RelationshipAdded
    • RelationshipDeleted

Advantage

  • Track what, who, and when a value is changed
  • Keep the audit trail of all changes
  • Display the audit trail of all changes

Community vs Enterprise

The Audit feature is free to use in the Community version.

The Enterprise version offer performance enhancement by automatically saving audit entries using the BulkInsert.

Getting Started

Enable Auditing

By default, not to impact performance, the Audit feature is disabled. You can activate it by enabling the following configuration context.Configuration.Audit.IsEnabled = true.

Always enabled

To have the Audit feature always enabled, you activate it in your context constructor.

public class EntityContext : DbContext
{
	public EntityContext() : base(FiddleHelper.GetConnectionStringSqlServer())
	{
		// Add your configuration here
		this.Configuration.Audit.AutoSaveSet = this.XmlAuditEntries;
		this.Configuration.Audit.IsEnabled = true;
	}
	
	public DbSet<Customer> Customers { get; set; }
	public DbSet<XmlAuditEntry> XmlAuditEntries { get; set; }
}

public static void Main()
{
	using (var context = new EntityContext())
	{
		context.Customers.Add(new Customer() { Name = "Customer_A", Description = "Description" });
		
		// Save changes and Audit trail in database
		context.SaveChanges();
		
		// Display Audit trail
		var auditEntries = context.XmlAuditEntries.ToAuditEntries();
		FiddleHelper.WriteTable("1 - Audit Entries", auditEntries);
		FiddleHelper.WriteTable("2 - Audit Properties", auditEntries.SelectMany(x => x.Properties));
	}
}

Try it

On Demand enabled

To have the Audit feature on demand enabled, you activate it after the context is created.

public class EntityContext : DbContext
{
	public EntityContext() : base(FiddleHelper.GetConnectionStringSqlServer())
	{
		// Add your configuration here
		this.Configuration.Audit.AutoSaveSet = this.XmlAuditEntries;
	}
	
	public DbSet<Customer> Customers { get; set; }
	public DbSet<XmlAuditEntry> XmlAuditEntries { get; set; }
}

public static void Main()
{
	using (var context = new EntityContext())
	{
		// You can activate the Audit feature on demand by enabling it after the context is created.
		context.Configuration.Audit.IsEnabled = true;
		
		context.Customers.Add(new Customer() { Name = "Customer_A", Description = "Description" });
		
		// Save changes and Audit trail in database
		context.SaveChanges();
		
		// Display Audit trail
		var auditEntries = context.XmlAuditEntries.ToAuditEntries();
		FiddleHelper.WriteTable("1 - Audit Entries", auditEntries);
		FiddleHelper.WriteTable("2 - Audit Properties", auditEntries.SelectMany(x => x.Properties));
	}
}

Try it

Last Audit

The latest audit can be accessed with the LastAudit property.

The LastAudit property gives you additional information that are not saved such as:

  • Entity
  • Entry
  • OldValueRaw
  • NewValueRaw
context.Customers.Add(new Customer() { Name = "Customer_A", Description = "Description" });
context.Customers.Add(new Customer() { Name = "Customer_B", Description = "Description" });
context.SaveChanges();

var lastAudit = context.Configuration.Audit.LastAudit;
FiddleHelper.WriteTable("1 - LastAudit - Entries", lastAudit.Entries);
FiddleHelper.WriteTable("2 - LastAudit - Properties", lastAudit.Entries.SelectMany(x => x.Properties));

Try it

AutoSave Set

To automatically save the audit trail in your database, you need to specify the DbSet<> in which audit entries will be added then saved.

One of those following set must be added to your context:

Name Description Example
AuditEntry The AuditEntry class allows you to save one row per property (Only recommended for Enterprise version). The DbSet<AuditEntry> and DbSet<AuditEntryProperty> must be added to your context. Try it
XmlAuditEntry The XmlAuditEntry class allows you to save your properties in an XML format. The DbSet<XmlAuditEntry> must be added to your context. Try it

AutoSave Action

To automatically save the audit trail in another context or a log file, you need to specify an AutoSaveAction. This action will be executed after all saves are completed.

public EntityContext() : base(FiddleHelper.GetConnectionStringSqlServer())
{
	var audit = this.Configuration.Audit;			
	audit.AutoSaveAction = (context, auditing) => {
		foreach(var entry in auditing.EntriesXml)
		{
			Log.AppendLine("EntitySetName: " + entry.EntitySetName);
			Log.AppendLine("EntityTypeName: " + entry.EntityTypeName);
			Log.AppendLine("State: " + entry.State);
			Log.AppendLine("StateName: " + entry.StateName);
			Log.AppendLine("CreatedBy: " + entry.CreatedBy);
			Log.AppendLine("CreatedDate: " + entry.CreatedDate);
			Log.AppendLine("XmlProperties: " + entry.XmlProperties);
			Log.AppendLine("---");
			Log.AppendLine("---");
			Log.AppendLine("---");
		}				
	};
	audit.IsEnabled = true;
}

// ...code...

using (var context = new EntityContext())
{
	context.Customers.Add(new Customer() { Name = "Customer_A", Description = "Description" });
	context.Customers.Add(new Customer() { Name = "Customer_B", Description = "Description" });			
	context.Customers.Add(new Customer() { Name = "Customer_C", Description = "Description" });

	// Save changes with Audit Enabled
	context.SaveChanges();
}

// Display Audit Trail
Console.WriteLine(Log.ToString());

Try it

Real Life Scenarios

Saving audit trail in a database

Your application need to keep an audit trail of all changes in a database. You can automatically save the audit trail by specifying an AutoSaveSet.

public class EntityContext : DbContext
{
	public EntityContext() : base(FiddleHelper.GetConnectionStringSqlServer())
	{
		// Add your configuration here
		// Saving audit trail in a database
		this.Configuration.Audit.AutoSaveSet = this.XmlAuditEntries;
		this.Configuration.Audit.IsEnabled = true;
	}
	
	public DbSet<Customer> Customers { get; set; }
	public DbSet<XmlAuditEntry> XmlAuditEntries { get; set; }
}

public static void Main()
{
	// Many changes
	using (var context = new EntityContext())
	{
		context.Customers.Add(new Customer() { Name = "Customer_A", Description = "Description" });
		context.Customers.Add(new Customer() { Name = "Customer_B", Description = "Description" });
		
		// Save changes and Audit trail in database
		context.SaveChanges();
	}
	
	using (var context = new EntityContext())
	{
		var customers = context.Customers.ToList();
		
		context.Customers.Remove(customers.Where(x => x.Name == "Customer_B").First());
		customers.Where(x => x.Name == "Customer_A").First().Description = "updated";
		
		// Save changes and Audit trail in database
		context.SaveChanges();
	}
	
	// Display
	using (var context = new EntityContext())
	{
		// Displaying audit trail history
		var auditEntries = context.XmlAuditEntries.ToAuditEntries();
		FiddleHelper.WriteTable("1 - Audit Entries", auditEntries);
		FiddleHelper.WriteTable("2 - Audit Properties", auditEntries.SelectMany(x => x.Properties));
	}
}

Try it

Saving audit trail in a log file

Your application need to keep an audit trail of all changes in a log file. You can automatically save the audit trail in a log file by specifying an AutoSaveAction.

public class EntityContext : DbContext
{
	public EntityContext() : base(FiddleHelper.GetConnectionStringSqlServer())
	{
		// Add your configuration here
		this.Configuration.Audit.AutoSaveAction = (context,auditing) => {
			using (System.IO.StreamWriter file = 
		new System.IO.StreamWriter(@"Log.txt"))
		{
				foreach(var entry in auditing.EntriesXml)
				{
					file.WriteLine("EntitySetName: " + entry.EntitySetName);
					file.WriteLine("EntityTypeName: " + entry.EntityTypeName);
					file.WriteLine("State: " + entry.State);
					file.WriteLine("StateName: " + entry.StateName);
					file.WriteLine("CreatedBy: " + entry.CreatedBy);
					file.WriteLine("CreatedDate: " + entry.CreatedDate);
					file.WriteLine("XmlProperties: ");
					file.WriteLine(entry.XmlProperties);
					file.WriteLine("---");
					file.WriteLine("---");
					file.WriteLine("---");
				}	
			}
		};
		this.Configuration.Audit.IsEnabled = true;
	}
	
	public DbSet<Customer> Customers { get; set; }
}

public static void Main()
{
	using (var context = new EntityContext())
	{
		context.Customers.Add(new Customer() { Name = "Customer_A", Description = "Description" });
		context.Customers.Add(new Customer() { Name = "Customer_B", Description = "Description" });			
		context.Customers.Add(new Customer() { Name = "Customer_C", Description = "Description" });
		
		// Save changes with Audit Enabled
		context.SaveChanges();
	}
	
	// Display Audit Trail in the Log.txt
	string[] lines = System.IO.File.ReadAllLines(@"Log.txt");
	foreach (string line in lines)
	{
		Console.WriteLine(line);
	}
}

Try it

Saving audit trail in a different database

Your application need to keep an audit trail of all changes in a different database. You can automatically save the audit trail in a different database file by specifying an AutoSaveAction.

public class EntityContext : DbContext
{
	public EntityContext() : base(FiddleHelper.GetConnectionStringSqlServer())
	{
		// Add your configuration here			
		this.Configuration.Audit.AutoSaveAction = (context, auditing) => {
			var auditContext = new AuditContext();
			auditContext.XmlAuditEntries.AddRange(auditing.EntriesXml);
			auditContext.SaveChanges();
		};
		
		this.Configuration.Audit.IsEnabled = true;
	}
	
	public DbSet<Customer> Customers { get; set; }
}

public class AuditContext : DbContext
{
	public AuditContext() : base(FiddleHelper.GetConnectionStringSqlServer())
	{
	}

	public DbSet<XmlAuditEntry> XmlAuditEntries { get; set; }
}

public static void Main()
{
	using (var context = new EntityContext())
	{
		context.Customers.Add(new Customer() { Name = "Customer_A", Description = "Description" });
		
		// Save changes and Audit trail in database
		context.SaveChanges();
	}
	
	using(var auditContext = new AuditContext())
	{
		// Display Audit trail
		var auditEntries = auditContext.XmlAuditEntries.ToAuditEntries();
		FiddleHelper.WriteTable("1 - Audit Entries", auditEntries);
		FiddleHelper.WriteTable("2 - Audit Properties", auditEntries.SelectMany(x => x.Properties));
	}
}

Try it

Displaying audit trail history

Your application need to display the audit trail of all changes in a user interface. If your audit trail is saved in a database, you can retrieve and display audit entries by using your audit DbSet<>.

// Display
using (var context = new EntityContext())
{
	// Displaying audit trail history
	var auditEntries = context.XmlAuditEntries.ToAuditEntries();
	FiddleHelper.WriteTable("1 - Audit Entries", auditEntries);
	FiddleHelper.WriteTable("2 - Audit Properties", auditEntries.SelectMany(x => x.Properties));
}

Try it

Documentation

AuditManager

The AuditManager allows you to configure how the audit trail will be created, saved, and retrieved.

Properties
Name Description Default Example
IsEnabled Gets or sets if the Audit feature is enabled. By default, this feature is disabled not to impact the performance. false Try it
AutoSaveAction Gets or sets the AutoSaveAction. This option is usually used to automatically save the audit trail in a log file or different database. null Try it
AutoSaveSet Gets or sets the AutoSaveSet. This option is usually used to automatically save the audit trail in a database. null Try it
LastAudit Gets the last Audit trail. null Try it
Methods (Display)
Name Description Example
DisplayDBNullAsNull(bool value) Format DBNull.Value as 'null' value. True by default. Try it
DisplayFormatType<Type>(Func<Type, string> formatter) Format the specified type into a string. Try it
DisplayEntityName<TEntityType>(string name) Use the specified name for the EntityName value. Try it
DisplayEntityName<TEntityType>(Func<TEntityType, string, string> formatter) Use the factory specified name for the EntityName value. Try it
DisplayPropertyName<TEntityType>(Expression<Func<TEntityType, object>> propertySelector, string name) Use the specified name for the PropertyName value. Try it
DisplayPropertyName<TEntityType>(Expression<Func<TEntityType, object>> propertySelector, Func<TEntityType, string, string> formatter) Use the factory specified name for the PropertyName value. Try it
Methods (Include & Exclude)
Name Description Example
ExcludeEntity() Excludes all entities from the audit. Try it
ExcludeEntity<TEntityType>() Excludes all entities of TEntityType type from the audit. Try it
ExcludeEntity(Func<object, bool> predicate) Excludes all entities that satisfy the predicate from the audit. Try it
ExcludeEntity<TEntityType>(Func<TEntityType, bool> predicate) Excludes all entities of TEntityType that satisfy the predicate from the audit. Try it
ExcludeEntity(AuditEntryState entryState) Excludes all entities with specified AuditEntryState from the audit. Try it
ExcludeEntity<TEntityType>(AuditEntryState entryState) Excludes all entities of 'TEntityType' type and with specified AuditEntryState from the audit. Try it
ExcludeEntity(Func<object, bool> predicate, AuditEntryState entryState) Excludes all entities that satisfy the predicate and with specified AuditEntryState from the audit. Try it
ExcludeEntity<TEntityType>(Func<TEntityType, bool> predicate, AuditEntryState entryState) Excludes all entities of TEntityType that satisfy the predicate and with specified AuditEntryState from the audit. Try it
ExcludeProperty() Excludes all properties from the audit. Key properties are never excluded. Try it
ExcludeProperty<TEntityType>() Excludes all properties of a TEntityType type from the audit. Key properties are never excluded. Try it
ExcludeProperty<TEntityType>(Expression<Func<TEntityType, object>> propertySelector) Excludes specified properties of a TEntityType type from the audit. Key properties are never excluded. Try it
ExcludePropertyUnchanged() Excludes all properties value unchanged from the audit. Key values are never excluded. Try it
ExcludePropertyUnchanged<TEntityType>() Excludes all properties value unchanged of a TEntityType type from the audit. Key values are never excluded. Try it
ExcludePropertyUnchanged(Func<object, bool> predicate) Excludes all properties value that satistfy the predicate from the audit. Key values are never excluded. Try it
IncludeEntity() Includes all entities in the audit. Try it
IncludeEntity<TEntityType>() Includes all entities of TEntityType type in the audit. Try it
IncludeEntity(Func<object, bool> predicate) Includes all entities that satisfy the predicate in the audit. Try it
IncludeEntity<TEntityType>(Func<TEntityType, bool> predicate) Includes all entities of TEntityType that satisfy the predicate in the audit. Try it
IncludeEntity(AuditEntryState entryState) Includes all entities with specified AuditEntryState in the audit. Try it
IncludeEntity<TEntityType>(AuditEntryState entryState) Includes all entities of 'TEntityType' type and with specified AuditEntryState in the audit. Try it
IncludeEntity(Func<object, bool> predicate, AuditEntryState entryState) Includes all entities that satisfy the predicate and with specified AuditEntryState in the audit. Try it
IncludeEntity<TEntityType>(Func<TEntityType, bool> predicate, AuditEntryState entryState) Includes all entities of TEntityType that satisfy the predicate and with specified AuditEntryState in the audit. Try it
IncludeProperty() Includes all properties in the audit. Try it
IncludeProperty<TEntityType>() Includes all properties of a TEntityType type in the audit. Try it
IncludeProperty<TEntityType>(Expression<Func<TEntityType, object>> propertySelector) Includes specified properties of a TEntityType type in the audit. Try it
IncludePropertyUnchanged() Includes all property values unchanged in the audit. Try it
IncludePropertyUnchanged<TEntityType>() Includes all property values unchanged of a TEntityType type in the audit. Try it
IncludePropertyUnchanged(Func<object, bool> predicate) Includes all property values that satistfy the predicate in the audit. Try it
UseIncludeDataAnnotation(bool value) Exclude all entities and properties from the Audit. Only entities and properties with Include data annotations or specified with the fluent API will be included. Try it
Methods (Soft Delete)
Name Description Example
SoftDeleted(Func<object, bool> predicate) Change the AuditEntryState from EntityModified to EntitySoftDeleted for all entities that satisfy the soft delete predicate. Try it
SoftDeleted<TEntityType>(Func<TEntityType, bool> predicate) Change the AuditEntryState from EntityModified to EntitySoftDeleted for all entities of TEntityType type and that satisfy the soft delete predicate. Try it

Audit

The Audit class provide information about the audit trail.

Properties
Name Description Example
Entries Gets a list of AuditEntry. Try it
EntriesXml Gets a list of XmlAuditEntry. Try it
Manager Gets the AuditManager Try it

AuditEntry

The AuditEntry class contains information about the entry and a list of AuditEntryProperty.

Properties (Mapped)

This properties values are saved in a database.

Name Description Example
AuditEntryID Gets or sets the AuditEntryID. Try it
EntitySetName Gets or sets the EntitySet name. Try it
EntityTypeName Gets or sets the EntityType name. Try it
State Gets or sets the AuditEntryState. Try it
StateName Gets or sets the AuditEntryState name. Try it
CreatedBy Gets or sets the AuditEntry created user. Try it
CreatedDate Gets or sets the AuditEntry created date. Try it
Properties Gets or sets the AuditEntry properties. Try it
Properties (Unmapped)

This properties values are only accessible via the LastAudit property.

Name Description Example
Parent Gets or sets the parent Audit. Try it
Entity Gets or sets the audited Entity. Try it
Entry Gets or sets the audited ObjectStateEntry. Try it
Database First SQL
Coming soon...

AuditEntryProperty

The AuditEntryProperty contains information about property.

Properties (Mapped)

These property values are saved in a database.

Name Description Example
Parent Gets or sets the parent AuditEntry. Try it
AuditEntryPropertyID Gets or sets the AuditEntryPropertyID. Try it
AuditEntryID Gets or sets the AuditEntryID. Try it
RelationName Gets or sets the relation name. Only available for RelationshipAdded and RelationshipDeleted state Try it
PropertyName Gets or sets the property name. Try it
OldValue Gets or sets the old value formatted as string. Avalable for Modified, Deleted, and RelationshipDeleted state. Try it
NewValue Gets or sets the new value formatted as string. Avalable for Insert, Modified, and RelationshipModified state. Try it
Properties (Unmapped)

These property values are only accessible via the LastAudit property.

Name Description Example
OldValueRaw Gets or sets the old raw value. This is the original raw value without being formatted. Try it
NewValueRaw Gets or sets the new raw value. This is the original raw value without being formatted. Try it
Database First SQL
Coming soon...

XmlAuditEntry

The XmlAuditEntry class contains information about the entry and all properties in a Xml format.

Properties (Mapped)
Name Description Example
XmlAuditEntryID Gets or sets the AuditEntryID. Try it
EntitySetName Gets or sets the EntitySet name. Try it
EntityTypeName Gets or sets the EntityType name. Try it
State Gets or sets the AuditEntryState. Try it
StateName Gets or sets the AuditEntryState name. Try it
CreatedBy Gets or sets the AuditEntry created user. Try it
CreatedDate Gets or sets the AuditEntry created date. Try it
XmlProperties Gets or sets audit properties formatted as Xml. Try it
Database First SQL
Coming soon...

Data Annotations

Entity
Name Description Example
AuditDisplay(string name) Attribute to change the Audit entity or property display name. Try it
AuditExclude Attribute to exclude from the audit the entity or property. Try it
AuditInclude Attribute to include in the audit the entity or property. Require to enable Include with AuditManager UseIncludeDataAnnotation(true) method. Try it
Property
Name Description Example
AuditDisplay(string name) Attribute to change the Audit entity or property display. Try it
AuditDisplayFormat(string dataFormatString) Attribute to change the Audit property display format. Try it
AuditExclude Attribute to exclude from the audit the entity or property. Try it
AuditInclude Attribute to include in the audit the entity or property. Require to enable Include with AuditManager UseIncludeDataAnnotation(true) method. Try it

Extension Methods

Name Description Example
Where<TEntityType>(this DbSet<AuditEntry> set) Gets the audit trail of all entries entry of TEntityType type. Try it
Where<TEntityType>(this DbSet<AuditEntry> set, TEntityType entry) Gets the audit trail of the specific entry. Try it
Where<TEntityType>(this DbSet<AuditEntry> set, params object[] keyValues) Gets the audit trail of the specific key. Try it
Where<TEntityType>(this DbSet<XmlAuditEntry> set) Gets the audit trail of all entries entry of TEntityType type. Try it
ToAuditEntries(this IEnumerable<XmlAuditEntry> items) Return a list of XmlAuditEntry converted into AuditEntry. Try it

Limitations

Bulk Operations & Batch Operations

All operations that doesn't use the ChangeTracker (required to create the audit trail) are currently not supported: