What Are EF Core Interceptors?
EF Core interceptors are hooks that let you run custom logic before or after EF executes database operations. Instead of sprinkling logic across services, you can enforce business rules centrally in your data access layer.
Purpose
- Centralize cross-cutting logic
- Enforce consistency across entities
- Extend EF Core behavior without cluttering application code
Pros and Cons
✅ Pros
- Keeps business rules in one place
- Reduces repetitive code
- Works for both SaveChanges and SQL commands
- Increases maintainability in large systems
❌ Cons
- Can make rules less visible to developers
- Debugging is harder if you forget about them
- Potential performance overhead if overused
Common Business Use Cases (Summary)
- Auto-Fill Properties → Automatically update fields like
UpdatedDate - Entity Validation → Enforce business rules globally (e.g., Age > 18)
- Audit Logging → Track inserts, updates, and deletes
- Soft Deletes → Mark rows as deleted instead of physically removing them
- SQL Command Logging & Profiling → Inspect and monitor generated SQL
Detailed Examples
1. Auto-Fill Properties (UpdatedDate)
Ensures audit fields like UpdatedDate are always correct, without manual handling.
public class UpdateDateInterceptor : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
var context = eventData.Context;
if (context == null) return base.SavingChangesAsync(eventData, result, cancellationToken);
foreach (var entry in context.ChangeTracker.Entries())
{
if (entry.State == EntityState.Modified &&
entry.Properties.Any(p => p.Metadata.Name == "UpdatedDate"))
{
entry.Property("UpdatedDate").CurrentValue = DateTime.UtcNow;
}
}
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
}
2. Entity Validation (Age > 18)
Enforces rules across all entities without extra service logic.
public class ValidationInterceptor : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
var context = eventData.Context;
if (context == null) return base.SavingChangesAsync(eventData, result, cancellationToken);
foreach (var entry in context.ChangeTracker.Entries<Person>())
{
if ((entry.State == EntityState.Added || entry.State == EntityState.Modified) &&
entry.Entity.Age < 18)
{
throw new InvalidOperationException("Age must be greater than 18.");
}
}
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
}
public class Person
{
public int Id { get; set; }
public int Age { get; set; }
}
3. Audit Logging
Tracks data changes for compliance and debugging.
public class AuditLogInterceptor : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
var context = eventData.Context;
if (context == null) return base.SavingChangesAsync(eventData, result, cancellationToken);
foreach (var entry in context.ChangeTracker.Entries())
{
if (entry.State == EntityState.Added ||
entry.State == EntityState.Modified ||
entry.State == EntityState.Deleted)
{
Console.WriteLine($"AUDIT: {entry.Entity.GetType().Name} - {entry.State}");
}
}
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
}
4. Soft Deletes
Prevents permanent data loss by flipping an IsDeleted flag.
public class SoftDeleteInterceptor : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
var context = eventData.Context;
if (context == null) return base.SavingChangesAsync(eventData, result, cancellationToken);
foreach (var entry in context.ChangeTracker.Entries<ISoftDeletable>())
{
if (entry.State == EntityState.Deleted)
{
entry.State = EntityState.Modified;
entry.Entity.IsDeleted = true;
}
}
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
}
public interface ISoftDeletable
{
bool IsDeleted { get; set; }
}
5. SQL Command Logging & Profiling
Captures raw SQL queries for performance monitoring.
public class CommandLoggingInterceptor : DbCommandInterceptor
{
public override InterceptionResult<int> NonQueryExecuting(
DbCommand command,
CommandEventData eventData,
InterceptionResult<int> result)
{
Console.WriteLine($"SQL: {command.CommandText}");
return base.NonQueryExecuting(command, eventData, result);
}
}
Registering Interceptors
Add them to your DbContext:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.AddInterceptors(new UpdateDateInterceptor())
.AddInterceptors(new ValidationInterceptor())
.AddInterceptors(new AuditLogInterceptor())
.AddInterceptors(new SoftDeleteInterceptor())
.AddInterceptors(new CommandLoggingInterceptor());
}
Conclusion
EF Core interceptors are powerful for:
- Auto-filling properties like timestamps
- Validating business rules
- Auditing changes
- Soft deletion strategies
- Query logging and profiling
They keep your application clean by centralizing cross-cutting logic, but should be used carefully to avoid hidden complexity.