Interceptors Pattern¶
EF Core SaveChanges interceptors for cross-cutting entity concerns.
Overview¶
flowchart TB
A[SaveChangesAsync] --> B[AuditableInterceptor]
B --> C[SoftDeleteInterceptor]
C --> D[Database]
B -.->|Sets| E[CreatedDate, CreatedByUserId]
B -.->|Sets| F[UpdatedDate, UpdatedByUserId]
C -.->|Converts| G[Delete → Update IsDeleted] Auditable Interceptor¶
Automatically populates audit fields on entities extending AuditableEntity.
public sealed class AuditableEntityInterceptor : SaveChangesInterceptor
{
private readonly ICurrentUserAccessor _currentUserAccessor;
private readonly IDateTimeProvider _dateTimeProvider;
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(...)
{
var now = _dateTimeProvider.GetDateTime();
var userId = _currentUserAccessor.UserId;
foreach (var entry in context.ChangeTracker.Entries<AuditableEntity>())
{
if (entry.State == EntityState.Added)
{
entry.Entity.CreatedByUserId ??= userId;
if (entry.Entity.CreatedDate == default)
{
entry.Entity.CreatedDate = now;
}
}
if (entry.State == EntityState.Modified)
{
entry.Entity.UpdatedByUserId = userId;
entry.Entity.UpdatedDate = now;
}
}
return base.SavingChangesAsync(...);
}
}
Soft Delete Interceptor¶
Converts Delete operations to Update for soft-deletable entities.
public sealed class SoftDeleteInterceptor : SaveChangesInterceptor
{
private readonly IDateTimeProvider _dateTimeProvider;
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(...)
{
var now = _dateTimeProvider.GetDateTime();
foreach (var entry in context.ChangeTracker.Entries<ISoftDeletable>())
{
if (entry.State == EntityState.Deleted)
{
entry.State = EntityState.Modified;
entry.Entity.IsDeleted = true;
entry.Entity.DeletedDate = now;
}
}
return base.SavingChangesAsync(...);
}
}
Registration¶
services.AddDbContext<DataContext>((sp, options) =>
{
options.AddInterceptors(
sp.GetRequiredService<AuditableEntityInterceptor>(),
sp.GetRequiredService<SoftDeleteInterceptor>()
);
});
Execution Order¶
Interceptors execute in registration order:
AuditableEntityInterceptor— Sets timestamps and user IDsSoftDeleteInterceptor— Converts deletes
Global Query Filters¶
Combined with interceptors, query filters ensure deleted entities are excluded:
Override with IgnoreQueryFilters() when needed.
Related¶
- Soft Delete — Soft delete pattern
- Concurrency — Optimistic locking
- Data Model — Entity design