Audit.EntityFramework 25.0.4

dotnet add package Audit.EntityFramework --version 25.0.4
NuGet\Install-Package Audit.EntityFramework -Version 25.0.4
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Audit.EntityFramework" Version="25.0.4" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Audit.EntityFramework --version 25.0.4
#r "nuget: Audit.EntityFramework, 25.0.4"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Audit.EntityFramework as a Cake Addin
#addin nuget:?package=Audit.EntityFramework&version=25.0.4

// Install Audit.EntityFramework as a Cake Tool
#tool nuget:?package=Audit.EntityFramework&version=25.0.4

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

NuGet Status NuGet Count

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.4.4 N/C
.NET 4.7.2 EntityFramework 6.4.4 N/C
.NET Standard 2.1 EntityFramework 6.4.4 Microsoft.EntityFrameworkCore 5.0.17
.NET 6.0 EntityFramework 6.4.4 Microsoft.EntityFrameworkCore 6.0.25
.NET 7.0 EntityFramework 6.4.4 Microsoft.EntityFrameworkCore 7.0.14
.NET 8.0 EntityFramework 6.4.4 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 or Audit.EntityFramework.Identity.Core and inherit from the class AuditIdentityDbContext instead of AuditDbContext.

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 EF Core ≥ 3.0.

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

The EF audit events are stored using a Data Provider. You can use one of the available data providers or implement your own. This can be set per DbContext instance or globally. If you plan to store the audit logs with EF, you can use 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.
  • 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 with AuditIgnore/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 a NullDataProvider.

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 string The command type (Text, StoredProcedure)
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:

Audit entities

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 the UseDbcontext() 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 entities 2

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 type AuditLog.

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 to Audit_Updates_Blog table
  • Any other operation on Blog table → Audit to Audit_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:

Audit entities Many To Many

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: 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 paypal
Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
25.0.4 244 3/24/2024
25.0.3 340 3/13/2024
25.0.2 186 3/12/2024
25.0.1 262 2/28/2024
25.0.0 664 2/16/2024
24.0.1 311 2/12/2024
24.0.0 236 2/12/2024
23.0.0 786 12/14/2023
22.1.0 650 12/9/2023
22.0.2 817 12/1/2023
22.0.1 581 11/16/2023
22.0.0 473 11/14/2023
21.1.0 1,066 10/9/2023
21.0.4 1,190 9/15/2023
21.0.3 3,521 7/9/2023
21.0.2 793 7/6/2023
21.0.1 1,679 5/27/2023
21.0.0 1,470 4/15/2023
20.2.4 1,038 3/27/2023
20.2.3 1,026 3/17/2023
20.2.2 923 3/14/2023
20.2.1 901 3/11/2023
20.2.0 988 3/7/2023
20.1.6 6,091 2/23/2023
20.1.5 1,473 2/9/2023
20.1.4 1,132 1/28/2023
20.1.3 6,074 12/21/2022
20.1.2 2,801 12/14/2022
20.1.1 1,162 12/12/2022
20.1.0 1,165 12/4/2022
20.0.4 2,557 11/30/2022
20.0.3 1,761 10/28/2022
20.0.2 1,305 10/26/2022
20.0.1 1,413 10/21/2022
20.0.0 1,745 10/1/2022
19.4.1 6,146 9/10/2022
19.4.0 1,484 9/2/2022
19.3.0 1,603 8/23/2022
19.2.2 1,929 8/11/2022
19.2.1 1,480 8/6/2022
19.2.0 1,674 7/24/2022
19.1.4 6,345 5/23/2022
19.1.3 1,418 5/22/2022
19.1.2 1,493 5/18/2022
19.1.1 4,788 4/28/2022
19.1.0 1,697 4/10/2022
19.0.7 5,580 3/13/2022
19.0.6 1,509 3/7/2022
19.0.5 7,919 1/28/2022
19.0.4 1,607 1/23/2022
19.0.3 31,764 12/14/2021
19.0.2 1,209 12/11/2021
19.0.1 14,964 11/20/2021
19.0.0 1,364 11/11/2021
19.0.0-rc.net60.2 172 9/26/2021
19.0.0-rc.net60.1 214 9/16/2021
18.1.6 14,786 9/26/2021
18.1.5 2,003 9/7/2021
18.1.4 1,530 9/6/2021
18.1.3 7,007 8/19/2021
18.1.2 1,962 8/8/2021
18.1.1 1,336 8/5/2021
18.1.0 2,441 8/1/2021
18.0.1 1,410 7/30/2021
18.0.0 1,929 7/26/2021
17.0.8 1,898 7/7/2021
17.0.7 2,522 6/16/2021
17.0.6 2,328 6/5/2021
17.0.5 2,442 5/28/2021
17.0.4 3,179 5/4/2021
17.0.3 1,424 5/1/2021
17.0.2 20,189 4/22/2021
17.0.1 1,360 4/18/2021
17.0.0 3,199 3/26/2021
16.5.6 1,426 3/25/2021
16.5.5 2,259 3/23/2021
16.5.4 1,842 3/9/2021
16.5.3 1,523 2/26/2021
16.5.2 1,442 2/23/2021
16.5.1 1,315 2/21/2021
16.5.0 1,616 2/17/2021
16.4.5 1,699 2/15/2021
16.4.4 2,316 2/5/2021
16.4.3 1,702 1/27/2021
16.4.2 1,712 1/22/2021
16.4.1 1,399 1/21/2021
16.4.0 10,041 1/11/2021
16.3.3 1,592 1/8/2021
16.3.2 1,427 1/3/2021
16.3.1 1,845 12/31/2020
16.3.0 1,442 12/30/2020
16.2.1 1,471 12/27/2020
16.2.0 6,771 10/13/2020
16.1.5 2,344 10/4/2020
16.1.4 4,005 9/17/2020
16.1.3 2,866 9/13/2020
16.1.2 1,473 9/9/2020
16.1.1 1,624 9/3/2020
16.1.0 1,688 8/19/2020
16.0.3 2,224 8/15/2020
16.0.2 1,634 8/9/2020
16.0.1 1,520 8/8/2020
16.0.0 3,805 8/7/2020
15.3.0 10,244 7/23/2020
15.2.3 2,032 7/14/2020
15.2.2 8,655 5/19/2020
15.2.1 1,854 5/12/2020
15.2.0 1,649 5/9/2020
15.1.1 2,514 5/4/2020
15.1.0 2,265 4/13/2020
15.0.5 13,716 3/18/2020
15.0.4 4,848 2/28/2020
15.0.3 1,587 2/26/2020
15.0.2 8,047 1/20/2020
15.0.1 2,521 1/10/2020
15.0.0 3,316 12/17/2019
14.9.1 4,062 11/30/2019
14.9.0 1,671 11/29/2019
14.8.1 1,698 11/26/2019
14.8.0 2,879 11/20/2019
14.7.0 8,573 10/9/2019
14.6.6 1,635 10/8/2019
14.6.5 9,002 9/27/2019
14.6.4 4,732 9/21/2019
14.6.3 39,430 8/12/2019
14.6.2 3,386 8/3/2019
14.6.1 1,678 8/3/2019
14.6.0 22,887 7/26/2019
14.5.7 3,231 7/18/2019
14.5.6 10,669 7/10/2019
14.5.5 7,584 7/1/2019
14.5.4 2,437 6/17/2019
14.5.3 8,856 6/5/2019
14.5.2 6,456 5/30/2019
14.5.1 2,339 5/28/2019
14.5.0 11,264 5/24/2019
14.4.0 1,990 5/22/2019
14.3.4 8,013 5/14/2019
14.3.3 1,786 5/9/2019
14.3.2 2,504 4/30/2019
14.3.1 1,805 4/27/2019
14.3.0 1,919 4/24/2019
14.2.3 1,941 4/17/2019
14.2.2 8,479 4/10/2019
14.2.1 16,884 4/5/2019
14.2.0 14,347 3/16/2019
14.1.1 2,597 3/8/2019
14.1.0 5,395 2/11/2019
14.0.4 8,919 1/31/2019
14.0.3 8,261 1/22/2019
14.0.2 6,463 12/15/2018
14.0.1 2,394 11/29/2018
14.0.0 2,406 11/19/2018
13.3.0 2,024 11/16/2018
13.2.2 1,905 11/15/2018
13.2.1 1,917 11/13/2018
13.2.0 10,247 10/31/2018
13.1.5 1,896 10/31/2018
13.1.4 4,417 10/25/2018
13.1.3 2,385 10/18/2018
13.1.2 4,021 9/12/2018
13.1.1 1,892 9/11/2018
13.1.0 1,872 9/11/2018
13.0.0 2,090 8/29/2018
12.3.6 1,671 8/29/2018
12.3.5 3,219 8/22/2018
12.3.4 1,751 8/21/2018
12.3.3 26,528 8/21/2018
12.3.2 1,703 8/20/2018
12.3.1 1,756 8/20/2018
12.3.0 1,731 8/20/2018
12.2.2 1,828 8/15/2018
12.2.1 3,189 8/9/2018
12.2.0 1,797 8/8/2018
12.1.11 1,933 7/30/2018
12.1.10 1,913 7/20/2018
12.1.9 2,019 7/10/2018
12.1.8 1,921 7/2/2018
12.1.7 11,185 6/7/2018
12.1.6 20,209 6/4/2018
12.1.5 2,126 6/2/2018
12.1.4 2,231 5/25/2018
12.1.3 4,104 5/16/2018
12.1.2 2,263 5/15/2018
12.1.1 2,276 5/14/2018
12.1.0 2,599 5/9/2018
12.0.7 10,894 5/5/2018
12.0.6 2,413 5/4/2018
12.0.5 2,279 5/3/2018
12.0.4 3,225 4/30/2018
12.0.3 2,364 4/30/2018
12.0.2 2,332 4/27/2018
12.0.1 2,335 4/25/2018
12.0.0 2,299 4/22/2018
11.2.0 2,474 4/11/2018
11.1.0 2,359 4/8/2018
11.0.8 2,810 3/26/2018
11.0.7 2,362 3/20/2018
11.0.6 6,142 3/7/2018
11.0.5 2,213 2/22/2018
11.0.4 2,519 2/14/2018
11.0.3 2,371 2/12/2018
11.0.2 3,242 2/9/2018
11.0.1 3,510 1/29/2018
11.0.0 2,581 1/15/2018
10.0.3 2,734 12/29/2017
10.0.2 2,138 12/26/2017
10.0.1 2,554 12/18/2017
10.0.0 2,131 12/18/2017
9.3.0 2,342 12/17/2017
9.2.0 2,169 12/17/2017
9.1.3 6,056 12/5/2017
9.1.2 2,935 11/27/2017
9.1.1 2,378 11/21/2017
9.1.0 2,215 11/21/2017
9.0.1 2,265 11/11/2017
9.0.0 2,076 11/10/2017
8.7.0 2,181 11/9/2017
8.6.0 2,201 11/9/2017
8.5.0 7,450 10/3/2017
8.4.0 2,148 10/3/2017
8.3.1 2,355 9/8/2017
8.3.0 2,157 9/8/2017
8.2.0 2,244 9/4/2017
8.1.0 2,343 8/22/2017
8.0.0 2,302 8/19/2017
7.1.3 2,176 8/14/2017
7.1.2 2,268 8/2/2017
7.1.1 2,436 7/26/2017
7.1.0 2,741 7/5/2017
7.0.9 2,156 6/28/2017
7.0.8 1,965 6/19/2017
7.0.6 3,423 4/7/2017
7.0.5 2,177 3/21/2017
7.0.4 2,000 3/21/2017
7.0.3 1,955 3/20/2017
7.0.2 1,967 3/13/2017
7.0.0 2,167 3/1/2017
6.2.0 2,039 2/25/2017
6.1.0 6,054 2/14/2017
6.0.0 2,078 2/9/2017
5.3.0 1,900 2/5/2017
5.2.0 1,856 1/26/2017
5.1.0 1,868 1/19/2017
5.0.0 1,933 1/7/2017
4.11.0 1,943 1/5/2017
4.10.0 1,897 12/31/2016
4.9.0 1,899 12/26/2016
4.8.0 1,935 12/17/2016
4.7.0 2,004 12/8/2016
4.6.5 1,964 12/4/2016
4.6.4 1,945 11/25/2016
4.6.2 2,940 11/18/2016
4.6.1 1,933 11/15/2016
4.6.0 1,939 11/11/2016
4.5.9 2,036 11/2/2016
4.5.8 1,953 11/2/2016
4.5.7 1,859 10/26/2016
4.5.6 1,976 10/6/2016
4.5.5 1,867 10/3/2016
4.5.4 1,900 10/2/2016
4.5.3 1,835 9/30/2016
4.5.2 1,844 9/28/2016
4.5.1 1,904 9/28/2016
4.5.0 1,916 9/28/2016
4.4.0 2,016 9/23/2016
4.3.0 1,984 9/22/2016
4.2.0 2,143 9/19/2016
4.1.0 1,919 9/13/2016
4.0.2 2,089 9/9/2016
4.0.1 1,944 9/9/2016
4.0.0 1,904 9/9/2016
3.6.1 1,849 9/7/2016
3.6.0 1,865 9/7/2016
3.4.0 2,200 9/7/2016