The Entity Framework data providers allow storing audit events generated by EF operations back into the same database (or a different database) using Entity Framework. This enables querying audit trails using LINQ and maintaining referential integrity between audited entities and their audit records.
This page documents three EF-based data providers:
EntityFrameworkDataProvider - Stores high-level SaveChanges audit events in dedicated audit tablesDbContextDataProvider<T, TEntity> - Generic provider for mapping any audit event type to a specific EF entityDbContextDataProvider - Non-generic provider for dynamic multi-entity audit storageFor information on EF auditing integration (interceptors, AuditDbContext), see Integration Approaches and AuditDbContext. For custom audit entity mapping, see Entity Mapping.
Sources: src/Audit.EntityFramework/README.md621-842 src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs19-28
The following table summarizes the three EF data providers and their use cases:
| Provider | Primary Use Case | Event Types | Mapping Approach |
|---|---|---|---|
EntityFrameworkDataProvider | Store high-level EF audit events (SaveChanges) with entity-to-audit-entity mapping | AuditEventEntityFramework only | Type mapping (Order → OrderAudit) or explicit mapping |
DbContextDataProvider<T, TEntity> | Store any audit event type into a single EF entity type | Any AuditEvent subtype | Mapper function transforms event to entity |
DbContextDataProvider | Store audit events across multiple entity types dynamically | Any AuditEvent subtype | Per-event factory determines target entity |
When to use each:
EntityFrameworkDataProvider: Use when auditing EF operations and storing audit data in parallel tables (e.g., Order → OrderAudit, Customer → CustomerAudit). This provider automatically copies entity properties to audit entities.
DbContextDataProvider<T, TEntity>: Use when storing any type of audit event (Web API, SignalR, custom events) into a single audit table using EF. Supports event updates via ReplaceEvent.
DbContextDataProvider: Use when different audit event types need to be stored in different tables, determined at runtime. Does not support event replacement.
Sources: src/Audit.EntityFramework/README.md623-628
The EntityFrameworkDataProvider class (in src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs) extends AuditDataProvider and provides three primary capabilities:
Order → OrderAudit)DbContext for storing audit recordsSources: src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs28-111
The provider is configured through the fluent API exposed by IEntityFrameworkProviderConfigurator. Configuration typically occurs in application startup via Audit.Core.Configuration.Setup().
The simplest configuration maps entity types by name convention:
This configuration maps Order → OrderAudit, Customer → CustomerAudit, etc., by appending "Audit" to the type name. The AuditEntityAction populates common audit fields.
Sources: src/Audit.EntityFramework/ConfigurationApi/EntityFrameworkProviderConfigurator.cs51-66 test/Audit.EntityFramework.Core.UnitTest/EfCoreInMemoryTests.cs214-231
For more control over mapping, use AuditTypeExplicitMapper:
Sources: src/Audit.EntityFramework/ConfigurationApi/IAuditEntityMapping.cs1-222 src/Audit.EntityFramework/ConfigurationApi/EntityFrameworkProviderConfigurator.cs68-76
The Map<TSourceEntity, TAuditEntity>() method provides strongly-typed mapping with optional per-entity actions:
| Method Signature | Purpose |
|---|---|
Map<TSource, TAudit>() | Simple type mapping with automatic property copy |
Map<TSource, TAudit>(Action<AuditEvent, EventEntry, TAudit>) | Type mapping with sync action |
Map<TSource, TAudit>(Func<AuditEvent, EventEntry, TAudit, Task>) | Type mapping with async action |
Map<TSource, TAudit>(Func<AuditEvent, EventEntry, TAudit, bool>) | Type mapping with inclusion filter |
Map<TSource, TAudit>(Action<TSource, TAudit>) | Type mapping with entity-level action |
Sources: src/Audit.EntityFramework/ConfigurationApi/IAuditEntityMapping.cs12-85 src/Audit.EntityFramework/ConfigurationApi/AuditEntityMapping.cs15-117
MapExplicit handles entities without explicit type mappings, such as EF-generated join tables:
The predicate receives an EventEntry and returns true if the entry should be mapped to the specified audit type. This is evaluated before type-based mappings.
Sources: src/Audit.EntityFramework/ConfigurationApi/IAuditEntityMapping.cs126-144 test/Audit.EntityFramework.Core.UnitTest/EfCoreInMemoryTests.cs500-563
MapTable provides a shorthand for matching by table name:
This is equivalent to MapExplicit with a predicate checking entry.Table == "User".
Sources: src/Audit.EntityFramework/ConfigurationApi/IAuditEntityMapping.cs146-164 src/Audit.EntityFramework/ConfigurationApi/AuditEntityMapping.cs207-236
By default, the provider copies properties with matching names from the source entity to the audit entity using reflection:
The property copying logic uses GetPropertiesToSet() to identify writable properties on the audit entity and GetPropertiesToGet() (EF6) or metadata (EF Core) to read from the source entity.
Sources: src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs308-346 src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs348-371
Use IgnoreMatchedProperties() to disable automatic property copying:
Or use a predicate to disable selectively:
Sources: src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs286-290 src/Audit.EntityFramework/ConfigurationApi/EntityFrameworkProviderConfigurator.cs174-182
For complex scenarios (e.g., EF change tracking proxies), provide a custom factory:
The AuditEntityCreator receives the audit DbContext and EventEntry, returning a newly instantiated audit entity. Property copying still occurs unless disabled.
Sources: src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs61-65 src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs293-306 test/Audit.EntityFramework.Core.UnitTest/EfCoreInMemoryTests.cs631-657
Multiple actions can be configured at different levels, executing in a specific order:
Actions can return false to exclude the entity from the audit. The common action executes only if the specific action (explicit or type-based) returns true.
Sources: src/Audit.EntityFramework/ConfigurationApi/AuditEntityMapping.cs323-361 src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs124-164
All action signatures support both synchronous and asynchronous execution:
| Action Type | Signature Example |
|---|---|
| Sync Action | Action<AuditEvent, EventEntry, TAudit> |
| Async Action | Func<AuditEvent, EventEntry, TAudit, Task> |
| Sync Function | Func<AuditEvent, EventEntry, TAudit, bool> |
| Async Function | Func<AuditEvent, EventEntry, TAudit, Task<bool>> |
Sources: src/Audit.EntityFramework/ConfigurationApi/IAuditEntityMapping.cs14-44
The common audit action executes for all mapped entities, useful for setting common fields:
The generic AuditEntityAction<T>() only executes when the audit entity is of type T, allowing interface-based shared logic.
Sources: src/Audit.EntityFramework/ConfigurationApi/IAuditEntityMapping.cs166-220 src/Audit.EntityFramework/ConfigurationApi/AuditEntityMapping.cs238-292
By default, the provider uses the same DbContext that was audited:
This is the most common scenario where audit tables exist in the same database as the audited tables.
Sources: src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs120-121
For storing audits in a different database, provide a DbContextBuilder:
Or create a new context per audit:
Sources: src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs98-111 src/Audit.EntityFramework/ConfigurationApi/EntityFrameworkProviderConfigurator.cs25-43
When the audit DbContext inherits from AuditDbContext, the provider uses SaveChangesBypassAudit() to prevent recursive auditing:
This ensures that saving audit entities doesn't trigger another audit event.
Sources: src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs167-174 src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs242-249
The InsertEvent method is called by the audit pipeline when EventCreationPolicy requires persistence:
Sources: src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs113-186
The provider resolves the audit type in three stages:
AuditEntityCreator is provided, use its resultMapExplicit predicates in orderAuditTypeMapper based on entity typeSources: src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs126-151 src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs199-226
The provider uses conditional compilation to handle platform differences:
| Feature | EF6 Implementation | EF Core Implementation |
|---|---|---|
| Type resolution | ObjectContext.GetObjectType() | DbContext.Model.FindEntityType() |
| Property reading | Reflection-based GetPropertiesToGet() | Metadata-based GetProperties() |
| Adding entities | DbContext.Set(type).Add() | DbContext.Add() |
| Async dispose | DbContext.Dispose() | DbContext.DisposeAsync() |
Sources: src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs9-15 src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs156-160 src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs267-281
EF Core 5 and later introduce owned entity types that are handled specially:
Sources: src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs271-273
EF Core 8 added complex types (formerly owned entities), which are copied separately:
Sources: src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs334-345
The provider handles EF proxy types (lazy loading proxies, change tracking proxies) by unwrapping them:
This ensures type mapping works correctly even when EF wraps entities in proxy classes.
Sources: src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs373-384
Sources: src/Audit.EntityFramework/ConfigurationApi/EntityFrameworkProviderConfigurator.cs15-16
This class accumulates configuration settings and passes them to EntityFrameworkDataProvider:
| Property | Type | Purpose |
|---|---|---|
_auditTypeMapper | Func<Type, EventEntry, Type> | Maps source type to audit type |
_auditEntityAction | Func<AuditEvent, EventEntry, object, Task<bool>> | Enriches audit entities |
_dbContextBuilder | Func<AuditEventEntityFramework, DbContext> | Provides audit DbContext |
_explicitMapper | Func<EventEntry, Type> | Handles explicit mappings |
_auditEntityCreator | Func<DbContext, EventEntry, object> | Custom entity factory |
_ignoreMatchedPropertiesFunc | Func<Type, bool> | Controls property copying |
_disposeDbContext | bool | Whether to dispose audit context |
Sources: src/Audit.EntityFramework/ConfigurationApi/EntityFrameworkProviderConfigurator.cs17-23
This class builds the mapping configuration through its fluent API:
| Internal Collection | Purpose |
|---|---|
_mapping | Dictionary<Type, MappingInfo> - type-based mappings |
_explicitMapping | List<KeyValuePair<Func<EventEntry, bool>, MappingInfo>> - predicate-based mappings |
_commonAction | Func<AuditEvent, EventEntry, object, Task<bool>> - global action |
Sources: src/Audit.EntityFramework/ConfigurationApi/AuditEntityMapping.cs8-13
Internal class holding mapping metadata:
Sources: src/Audit.EntityFramework/ConfigurationApi/MappingInfo.cs7-29
The EntityFrameworkDataProvider does not support the ReplaceEvent operation:
This is because audit entities are persisted as part of the original transaction when using EventCreationPolicy.InsertOnEnd or InsertOnStartReplaceOnEnd. The initial insert is atomic with the audited changes, so replacement is not applicable.
Sources: src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs386-394
Sources: test/Audit.EntityFramework.Core.UnitTest/EfCoreInMemoryTests.cs214-231
Sources: test/Audit.EntityFramework.Core.UnitTest/EfCoreInMemoryTests.cs435-462
Sources: test/Audit.EntityFramework.Core.UnitTest/EfCoreInMemoryTests.cs500-563
Sources: test/Audit.EntityFramework.Core.UnitTest/EfCoreInMemoryTests.cs631-657
Sources: test/Audit.EntityFramework.Core.UnitTest/EfCoreInMemoryTests.cs212-252
When combined with EF interceptors (see Command and Transaction Interceptors), the provider can receive configuration from the audited DbContext:
Sources: test/Audit.EntityFramework.Core.UnitTest/DbCommandInterceptorTests.cs613-646
AuditTypeNameMapper with suffix convention reduces configurationAuditEntityAction<IAudit>() for shared logicDbContextBuilder when audit tables reside in a different databaseIgnoreMatchedProperties(true) for audit entities with custom schemasDbContext instances, set DisposeDbContext(false) to avoid premature disposalSources: src/Audit.EntityFramework/Providers/EntityFrameworkDataProvider.cs19-28 test/Audit.EntityFramework.Core.UnitTest/EfCoreInMemoryTests.cs212-625
Refresh this wiki