Automatically generates audit logs for Entity Framework operations. Special thanks to 'ZZZ Projects' for sponsoring this project.
$ dotnet add package Audit.EntityFrameworkEntity 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.
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
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.IdentityAudit.EntityFramework.Core / Audit.EntityFramework.Identity.Core |
|---|
Audit.EntityFramework.Core.v3 / Audit.EntityFramework.Identity.Core.v3 |
|---|
| .NET 4.5 | EntityFramework 6.1.3 | N/C | N/C |
| .NET 4.6.1 | EntityFramework 6.1.3 | Microsoft.EntityFrameworkCore 2.2.4 | N/C |
| .NET 4.7.2 | EntityFramework 6.3.0 | Microsoft.EntityFrameworkCore 3.1.0 | N/C |
| .NET Standard 1.5 | Microsoft.EntityFrameworkCore 1.1.2 | Microsoft.EntityFrameworkCore 1.1.2 | N/C |
| .NET Standard 2.0 | Microsoft.EntityFrameworkCore 2.2.4 | Microsoft.EntityFrameworkCore 3.1.0 | Microsoft.EntityFrameworkCore 3.1.0 |
| .NET Standard 2.1 | EntityFramework 6.3.0 | Microsoft.EntityFrameworkCore 5.0.1 | Microsoft.EntityFrameworkCore 3.1.0 |
| .NET 5.0 | EntityFramework 6.4.4 | Microsoft.EntityFrameworkCore 5.0.8 | Microsoft.EntityFrameworkCore 3.1.0 |
| .NET 6.0 | EntityFramework 6.4.4 | Microsoft.EntityFrameworkCore 6.0.0 | N/C |
| .NET 7.0 | EntityFramework 6.4.4 | Microsoft.EntityFrameworkCore 7.0.0 | N/C |
| .NET 8.0 | EntityFramework 6.4.4 | Microsoft.EntityFrameworkCore 8.0.0 | N/C |
N/C: Not Compatible
Examples:
Audit.EntityFramework.Core package.Audit.EntityFramework package.Audit.EntityFramework.Core.v3 package.In order to audit Insert, Delete and Update operations, you can use any of the three SaveChanges interception mechanisms provided:
AuditDbContextChange 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.IdentityorAudit.EntityFramework.Identity.Coreand inherit from the classAuditIdentityDbContextinstead ofAuditDbContext.
SaveChangesYou 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
SaveChangesoverride is needed, since all the other overloads will call one of these two.
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.
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.
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.
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
});The following settings for the high-level interceptor can be configured per DbContext or globally:
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.
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:
The Ignore, Override and Format settings are only applied to the Changes and ColumnValues collections on the EventEntry. The
Entityobject (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.
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; }
...
}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; }
...
}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.
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:
AuditDbContext base class.AuditDbContext attribute and your entity classes with AuditIgnore/AuditInclude attributes.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:
AuditScope creation.AuditScope saving.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
OnScopeSavingmethod, so we must bypass the Data Provider and this can be done by setting aNullDataProvider.
Audit.EntityFramework output includes:
IncludeEntityObjects configuration)With this information, you can measure performance, observe exceptions thrown or get statistics about usage of your database.
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 |
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) |
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.
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}");
}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
DbContextinstance 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.
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
})
)
);Mandatory:
The UseEntityFramework method provides several ways to indicate the Type Mapper and the Audit Action.
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;
}));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();
}));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;
})));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;
})));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;
})));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.
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
Blogtable -> Audit toAudit_Updates_Blogtable- Any other operation on
Blogtable -> Audit toAudit_Blogtable
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
MapTableis 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.
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());
);If you like this project please contribute in any of the following ways: