Audit Trail Generator
Overview
Automatic audit trail tracking for entities:
- IAuditable interface - Standard audit fields
- SaveChanges interceptor - Automatic field population
- User context integration - Track who made changes
- Soft delete support - Track deletions without removing
Quick Reference
| Component | Purpose |
|---|---|
IAuditable | Interface for auditable entities |
AuditableEntity | Base class with audit fields |
AuditSaveChangesInterceptor | Auto-populates audit fields |
SoftDeletable | Interface for soft delete |
Audit Structure
/Domain/Abstractions/
├── IAuditable.cs
├── ISoftDeletable.cs
└── AuditableEntity.cs
/Infrastructure/
├── Interceptors/
│ └── AuditSaveChangesInterceptor.cs
└── ApplicationDbContext.cs
Template: Audit Interfaces
// src/{name}.domain/Abstractions/IAuditable.cs
namespace {name}.domain.abstractions;
/// <summary>
/// Interface for entities that track creation and modification metadata
/// </summary>
public interface IAuditable
{
/// <summary>
/// UTC timestamp when the entity was created
/// </summary>
DateTime CreatedAtUtc { get; }
/// <summary>
/// ID of the user who created the entity
/// </summary>
Guid? CreatedBy { get; }
/// <summary>
/// UTC timestamp when the entity was last modified
/// </summary>
DateTime? UpdatedAtUtc { get; }
/// <summary>
/// ID of the user who last modified the entity
/// </summary>
Guid? UpdatedBy { get; }
}
// src/{name}.domain/Abstractions/ISoftDeletable.cs
namespace {name}.domain.abstractions;
/// <summary>
/// Interface for entities that support soft delete
/// </summary>
public interface ISoftDeletable
{
/// <summary>
/// Whether the entity has been soft deleted
/// </summary>
bool IsDeleted { get; }
/// <summary>
/// UTC timestamp when the entity was deleted
/// </summary>
DateTime? DeletedAtUtc { get; }
/// <summary>
/// ID of the user who deleted the entity
/// </summary>
Guid? DeletedBy { get; }
}
Template: Auditable Entity Base Class
// src/{name}.domain/Abstractions/AuditableEntity.cs
namespace {name}.domain.abstractions;
/// <summary>
/// Base class for entities that track audit information
/// </summary>
public abstract class AuditableEntity : Entity, IAuditable, ISoftDeletable
{
// ═══════════════════════════════════════════════════════════════
// AUDIT FIELDS (IAuditable)
// ═══════════════════════════════════════════════════════════════
public DateTime CreatedAtUtc { get; private set; }
public Guid? CreatedBy { get; private set; }
public DateTime? UpdatedAtUtc { get; private set; }
public Guid? UpdatedBy { get; private set; }
// ═══════════════════════════════════════════════════════════════
// SOFT DELETE FIELDS (ISoftDeletable)
// ═══════════════════════════════════════════════════════════════
public bool IsDeleted { get; private set; }
public DateTime? DeletedAtUtc { get; private set; }
public Guid? DeletedBy { get; private set; }
protected AuditableEntity() : base()
{
}
protected AuditableEntity(Guid id) : base(id)
{
}
// ═══════════════════════════════════════════════════════════════
// AUDIT METHODS (called by interceptor or manually)
// ═══════════════════════════════════════════════════════════════
/// <summary>
/// Sets creation audit fields. Called automatically by interceptor.
/// </summary>
internal void SetCreatedAudit(DateTime utcNow, Guid? userId)
{
CreatedAtUtc = utcNow;
CreatedBy = userId;
}
/// <summary>
/// Sets modification audit fields. Called automatically by interceptor.
/// </summary>
internal void SetModifiedAudit(DateTime utcNow, Guid? userId)
{
UpdatedAtUtc = utcNow;
UpdatedBy = userId;
}
/// <summary>
/// Soft deletes the entity
/// </summary>
public virtual void SoftDelete(DateTime utcNow, Guid? userId)
{
if (IsDeleted)
{
return;
}
IsDeleted = true;
DeletedAtUtc = utcNow;
DeletedBy = userId;
}
/// <summary>
/// Restores a soft-deleted entity
/// </summary>
public virtual void Restore()
{
IsDeleted = false;
DeletedAtUtc = null;
DeletedBy = null;
}
}
Template: SaveChanges Interceptor
// src/{name}.infrastructure/Interceptors/AuditSaveChangesInterceptor.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Diagnostics;
using {name}.application.abstractions.authentication;
using {name}.domain.abstractions;
namespace {name}.infrastructure.interceptors;
/// <summary>
/// Interceptor that automatically populates audit fields on SaveChanges
/// </summary>
public sealed class AuditSaveChangesInterceptor : SaveChangesInterceptor
{
private readonly IUserContext _userContext;
public AuditSaveChangesInterceptor(IUserContext userContext)
{
_userContext = userContext;
}
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
if (eventData.Context is not null)
{
UpdateAuditFields(eventData.Context);
}
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
public override InterceptionResult<int> SavingChanges(
DbContextEventData eventData,
InterceptionResult<int> result)
{
if (eventData.Context is not null)
{
UpdateAuditFields(eventData.Context);
}
return base.SavingChanges(eventData, result);
}
private void UpdateAuditFields(DbContext context)
{
var utcNow = DateTime.UtcNow;
var userId = GetCurrentUserId();
foreach (var entry in context.ChangeTracker.Entries<IAuditable>())
{
switch (entry.State)
{
case EntityState.Added:
SetCreatedAudit(entry, utcNow, userId);
break;
case EntityState.Modified:
SetModifiedAudit(entry, utcNow, userId);
break;
}
}
// Handle soft delete
foreach (var entry in context.ChangeTracker.Entries<ISoftDeletable>())
{
if (entry.State == EntityState.Deleted)
{
// Convert hard delete to soft delete
entry.State = EntityState.Modified;
if (entry.Entity is AuditableEntity auditableEntity)
{
auditableEntity.SoftDelete(utcNow, userId);
}
}
}
}
private void SetCreatedAudit(EntityEntry<IAuditable> entry, DateTime utcNow, Guid? userId)
{
if (entry.Entity is AuditableEntity auditableEntity)
{
auditableEntity.SetCreatedAudit(utcNow, userId);
}
else
{
// For entities implementing IAuditable but not inheriting AuditableEntity
entry.Property(nameof(IAuditable.CreatedAtUtc)).CurrentValue = utcNow;
entry.Property(nameof(IAuditable.CreatedBy)).CurrentValue = userId;
}
}
private void SetModifiedAudit(EntityEntry<IAuditable> entry, DateTime utcNow, Guid? userId)
{
if (entry.Entity is AuditableEntity auditableEntity)
{
auditableEntity.SetModifiedAudit(utcNow, userId);
}
else
{
entry.Property(nameof(IAuditable.UpdatedAtUtc)).CurrentValue = utcNow;
entry.Property(nameof(IAuditable.UpdatedBy)).Curr