EF Core Interceptors and Diagnostics: Hooking Into the Pipeline
EF Core interceptors let you hook into database operations — before a command executes, after it completes, when a connection opens, or when a transaction commits. They're the right tool for cross-cutting concerns like audit logging, query hints, soft deletes, and performance monitoring.
Types of Interceptors
EF Core provides several interceptor base classes:
DbCommandInterceptor— intercepts SQL command executionDbConnectionInterceptor— intercepts connection open/closeDbTransactionInterceptor— intercepts transaction begin/commit/rollbackSaveChangesInterceptor— interceptsSaveChangescalls
Each provides virtual methods for before/after (synchronous and asynchronous) variants.
A Simple Command Interceptor
Log slow queries by intercepting command execution:
public class SlowQueryInterceptor : DbCommandInterceptor
{
private readonly ILogger<SlowQueryInterceptor> _logger;
private readonly TimeSpan _threshold = TimeSpan.FromMilliseconds(500);
public SlowQueryInterceptor(ILogger<SlowQueryInterceptor> logger)
{
_logger = logger;
}
public override DbDataReader ReaderExecuted(
DbCommand command,
CommandExecutedEventData eventData,
DbDataReader result)
{
if (eventData.Duration > _threshold)
{
_logger.LogWarning(
"Slow query ({Duration}ms): {CommandText}",
eventData.Duration.TotalMilliseconds,
command.CommandText);
}
return result;
}
public override ValueTask<DbDataReader> ReaderExecutedAsync(
DbCommand command,
CommandExecutedEventData eventData,
DbDataReader result,
CancellationToken cancellationToken = default)
{
if (eventData.Duration > _threshold)
{
_logger.LogWarning(
"Slow query ({Duration}ms): {CommandText}",
eventData.Duration.TotalMilliseconds,
command.CommandText);
}
return ValueTask.FromResult(result);
}
}
Registering Interceptors
Add interceptors when configuring your DbContext:
services.AddDbContext<AppDbContext>((serviceProvider, options) =>
options.UseSqlServer(connectionString)
.AddInterceptors(
serviceProvider.GetRequiredService<SlowQueryInterceptor>()));
services.AddSingleton<SlowQueryInterceptor>();
Or directly in OnConfiguring:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.AddInterceptors(new SlowQueryInterceptor(logger));
}
Modifying Commands Before Execution
Interceptors can modify SQL before it reaches the database. For example, adding a query hint:
public class NoLockInterceptor : DbCommandInterceptor
{
public override InterceptionResult<DbDataReader> ReaderExecuting(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result)
{
command.CommandText = command.CommandText
.Replace("FROM [Products]", "FROM [Products] WITH (NOLOCK)");
return result;
}
}
Use this with caution — modifying SQL text is fragile. Prefer EF Core's built-in mechanisms where possible.
SaveChanges Interceptor for Auditing
A SaveChangesInterceptor is ideal for automatically populating audit fields:
public class AuditInterceptor : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
if (eventData.Context is null) return ValueTask.FromResult(result);
var now = DateTime.UtcNow;
foreach (var entry in eventData.Context.ChangeTracker.Entries<IAuditable>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedAt = now;
entry.Entity.UpdatedAt = now;
break;
case EntityState.Modified:
entry.Entity.UpdatedAt = now;
break;
}
}
return ValueTask.FromResult(result);
}
}
public interface IAuditable
{
DateTime CreatedAt { get; set; }
DateTime UpdatedAt { get; set; }
}
Soft Delete with SaveChanges Interceptor
Automatically convert hard deletes to soft deletes:
public class SoftDeleteInterceptor : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
if (eventData.Context is null) return ValueTask.FromResult(result);
foreach (var entry in eventData.Context.ChangeTracker.Entries<ISoftDeletable>())
{
if (entry.State == EntityState.Deleted)
{
entry.State = EntityState.Modified;
entry.Entity.IsDeleted = true;
entry.Entity.DeletedAt = DateTime.UtcNow;
}
}
return ValueTask.FromResult(result);
}
}
Connection Interceptors
Monitor connection lifecycle:
public class ConnectionMonitorInterceptor : DbConnectionInterceptor
{
private readonly ILogger<ConnectionMonitorInterceptor> _logger;
public ConnectionMonitorInterceptor(ILogger<ConnectionMonitorInterceptor> logger)
{
_logger = logger;
}
public override void ConnectionOpened(
DbConnection connection,
ConnectionEndEventData eventData)
{
_logger.LogDebug(
"Connection opened to {Database} ({Duration}ms)",
connection.Database,
eventData.Duration.TotalMilliseconds);
}
}
Diagnostic Listeners
For lower-level diagnostics, EF Core emits events through DiagnosticSource. This integrates with .NET's standard diagnostics infrastructure:
public class EfCoreDiagnosticListener : IObserver<DiagnosticListener>
{
public void OnNext(DiagnosticListener listener)
{
if (listener.Name == DbLoggerCategory.Name)
{
listener.Subscribe(new EfCoreEventObserver());
}
}
public void OnError(Exception error) { }
public void OnCompleted() { }
}
For most applications, interceptors are simpler and more practical than diagnostic listeners. Use diagnostic listeners when you need integration with distributed tracing systems like OpenTelemetry.
Practical Tips
- Keep interceptors lightweight. They run on every operation — expensive logic will slow down all database access.
- Override both sync and async methods. EF Core may call either variant depending on how your code invokes the operation.
- Register interceptors as singletons when they don't hold per-request state.
- Combine interceptors — you can register multiple interceptors of the same type; they execute in registration order.
Interceptors provide a clean, testable way to add cross-cutting behaviour to your data access layer without scattering it across your application code.