-
Notifications
You must be signed in to change notification settings - Fork 0
EF Core Integration
DataVigil hooks into EF Core through interceptors. This page covers how the interceptors work, how to mark entities for auditing, and how to control what gets tracked.
When you call AddAuditInterceptors(sp) on your DbContextOptionsBuilder, two interceptors get registered:
-
AuditSaveChangesInterceptor- fires on everySaveChanges/SaveChangesAsync. This is the main CUD (create, update, delete) auditing path. -
AuditCommandInterceptor- fires after SELECT queries complete (relational databases only). This is opt-in and only active whenIncludeReads()is enabled.
Both interceptors have safety guards built in:
- They skip
AuditDbContextBasedescendants, so writing audit records doesn't trigger more audit records (infinite recursion prevention). - They only process
DbContexttypes that were registered viaIntercept<TContext>(). - They only look at entities that implement
IAuditable.
The most basic approach. Every create, update, and delete gets logged:
using RzR.DataVigil.Abstractions.Contracts;
public class Order : IAuditable
{
public int Id { get; set; }
public string Status { get; set; }
public decimal TotalAmount { get; set; }
}When you want to pick which actions to audit or exclude certain properties:
using RzR.DataVigil.Abstractions.Contracts;
using RzR.DataVigil.Abstractions.Enums;
public class SensitiveDocument : IAuditableEntity
{
public int Id { get; set; }
public string Title { get; set; }
public string InternalNotes { get; set; }
public string Body { get; set; }
// Only audit Create and Delete, skip Update
public bool ShouldAudit(AuditAction action)
=> action != AuditAction.Update;
// Keep InternalNotes out of audit records
public IEnumerable<string> GetExcludedFields()
=> new[] { nameof(InternalNotes) };
}IAuditableEntity extends IAuditable, so you get the marker interface for free. The interceptor calls ShouldAudit() for each detected change and GetExcludedFields() when building the audit entry.
There are multiple levels where you can exclude entities from auditing.
Implement this on your DbContext to exclude entire entity types from auditing within that specific context:
using RzR.DataVigil.Abstractions.Contracts;
public class AppDbContext : DbContext, IAuditableContext
{
public IEnumerable<Type> GetExcludedEntityTypes()
=> new[] { typeof(AuditLog), typeof(MigrationHistory) };
}Exclude types across all contexts:
services.AddAuditTrail(options =>
{
options.Exclude<HealthCheckResult>();
options.Exclude<TempData>();
});The interceptor checks in this order and skips the entity if any check says "no":
- Is the entity type in
GlobalExclusions? -> skip - Does the
DbContextimplementIAuditableContextand list this type? -> skip - Does the entity implement
IAuditableEntityandShouldAudit()returns false? -> skip - Otherwise -> audit it
The ChangeTrackerEntryBuilder handles the mapping from EF's EntityEntry to an AuditEntry:
| Entity State | OldValue | NewValue | Properties captured |
|---|---|---|---|
Added |
null | current value | All properties |
Modified |
original value | current value | Only properties where old ≠ new |
Deleted |
original value | null | All properties |
Primary keys are extracted automatically (works with single and composite keys). The entity ID in the AuditEntry is the stringified PK - for composite keys, values are joined with _.
Fields listed in IAuditableEntity.GetExcludedFields() are removed after building, so they never show up in the audit entry.
Read auditing is off by default. Turn it on when you need to track who queried what data.
options.EfCore
.Intercept<AppDbContext>()
.IncludeReads()
.IncludeReadProperties(); // optional: also log column namesThe AuditCommandInterceptor fires after ReaderExecutedAsync. It:
- Parses table names from the SQL (FROM and JOIN clauses, regex-based)
- Maps table names back to entity types via EF Core metadata
- Skips non-
IAuditableentities - Extracts entity IDs from WHERE clause parameters when possible
- Optionally captures selected column names
- Adds read entries to the scoped
AuditReadCollector
MongoDB uses a different mechanism because queries don't go through DbCommand. Instead, AuditMaterializationInterceptor (an IMaterializationInterceptor) fires every time EF Core materializes an entity. If it's IAuditable, a read entry gets collected.
Read entries accumulate during a request. In ASP.NET Core, call UseAuditReadFlush() in your middleware pipeline:
app.UseRouting();
app.UseAuditReadFlush(); // flushes after downstream completes
app.MapControllers();The middleware resolves AuditReadCollector, wraps all collected entries into an AuditTransaction, and sends it through AuditPipeline.ProcessAsync().
If you want to explicitly log a read (bypassing the interceptor), use AuditReadService:
var readService = serviceProvider.GetRequiredService<AuditReadService>();
var order = await dbContext.Orders.FindAsync(42);
await readService.LogReadAsync<Order>(dbContext, order);
// Or for collections:
var orders = await dbContext.Orders.Where(o => o.Status == "Active").ToListAsync();
await readService.LogReadAsync<Order>(dbContext, orders);The AddAuditInterceptors extension method wires both interceptors into your DbContextOptionsBuilder:
builder.Services.AddDbContext<AppDbContext>((sp, opts) =>
{
opts.UseSqlServer(connectionString);
opts.AddAuditInterceptors(sp); // adds SaveChanges + Command interceptors
});For MongoDB, there's a separate extension for the materialization interceptor:
builder.Services.AddDbContext<AppDbContext>((sp, opts) =>
{
opts.UseMongoDB(connectionString, databaseName);
opts.AddAuditInterceptors(sp); // SaveChanges interceptor
opts.AddAuditReadInterceptor(sp); // Materialization interceptor for reads
});You can intercept more than one DbContext. Just call Intercept<T>() for each:
options.EfCore
.Intercept<OrderDbContext>()
.Intercept<InventoryDbContext>();Each context still needs AddAuditInterceptors(sp) in its own registration. The interceptor checks at runtime whether the context type is in the list and skips those that aren't registered.
RzR.DataVigil · Source Code · NuGet Packages · Built with .NET Standard 2.1
Getting started
Core features
Reference
Resources