DEV Community

Cover image for Mastering EF Core Interceptors: A Practical Guide
Ali Shahriari (MasterPars)
Ali Shahriari (MasterPars)

Posted on

Mastering EF Core Interceptors: A Practical Guide

Entity Framework Core Interceptors are one of the most powerful yet underused features in EF Core. They allow you to hook into EF's pipeline and execute custom logic whenever EF performs certain operations — such as executing queries, saving data, or connecting to the database.

In this article, we'll explore:

  • ✅ What EF Core Interceptors are
  • ✅ When you should use them
  • ✅ Real production-ready examples
  • ✅ Audit & Soft Delete implementations
  • ✅ Logging SQL queries Let's dive in and see how EF Interceptors level up your application!

🔥 Why Use EF Core Interceptors?
EF Core Interceptors help you:

  • Automatically modify database operations
  • Track changes without touching business logic
  • Enforce global rules
  • Log SQL queries
  • Apply cross‑cutting concerns (audit, soft delete, multi‑tenancy)

They're like ASP.NET middleware — but for EF Core.


🧠 Types of Interceptors in EF Core

IDbCommandInterceptor => Log or modify SQL commands
ISaveChangesInterceptor => Audit, Soft Delete, Validation
IConnectionInterceptor => Track DB connectivity events


✅ SQL Logging Interceptor Example
Logs SQL commands executed by EF Core (useful for debugging & production observability)

using Microsoft.EntityFrameworkCore.Diagnostics;
using System.Data.Common;
using Microsoft.Extensions.Logging;


public class SqlLoggingInterceptor : DbCommandInterceptor
{
    private readonly ILogger<SqlLoggingInterceptor> _logger;


    public SqlLoggingInterceptor(ILogger<SqlLoggingInterceptor> logger)
    {
     _logger = logger;
    }


    public override InterceptionResult<DbDataReader> ReaderExecuting(
    DbCommand command,
    CommandEventData eventData,
    InterceptionResult<DbDataReader> result)
    {
     _logger.LogInformation("EF SQL: {Sql}", command.CommandText);
     return base.ReaderExecuting(command, eventData, result);
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Register in Program.cs

builder.Services.AddDbContext<AppDbContext>((sp, options) =>
 {
options.UseSqlServer(builder.Configuration.GetConnectionString("Default"));
options.AddInterceptors(sp.GetRequiredService<SqlLoggingInterceptor>());
});


builder.Services.AddScoped<SqlLoggingInterceptor>();
Enter fullscreen mode Exit fullscreen mode

✅ Soft Delete Interceptor
Instead of removing row → mark as deleted.

Soft‑Delete Entity Base Class

public abstract class SoftDeletableEntity
{
    public bool IsDeleted { get; set; }
    public DateTime? DeletedAt { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

User Provider
We need a service to get the current authenticated user:

public interface ICurrentUserService
{
string? GetUserId();
}


public class HttpCurrentUserService : ICurrentUserService
{
private readonly IHttpContextAccessor _context;
public HttpCurrentUserService(IHttpContextAccessor context) => _context = context;


public string? GetUserId()
=> _context.HttpContext?.User?.FindFirst("sub")?.Value // JWT `sub`
?? _context.HttpContext?.User?.Identity?.Name;
}
Enter fullscreen mode Exit fullscreen mode

Interceptor (Audit + User)

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;


public class SoftDeleteInterceptor : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
var context = eventData.Context;


foreach (var entry in context.ChangeTracker.Entries<SoftDeletableEntity>())
{
if (entry.State == EntityState.Deleted)
{
entry.State = EntityState.Modified;
entry.Entity.IsDeleted = true;
entry.Entity.DeletedAt = DateTime.UtcNow;
}
}


return base.SavingChangesAsync(eventData, result, cancellationToken);
}
}
Enter fullscreen mode Exit fullscreen mode

Global Query Filter

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>().HasQueryFilter(x => !x.IsDeleted);
}
Enter fullscreen mode Exit fullscreen mode

✅ Advanced Audit Interceptor (CreatedAt, UpdatedAt, UserId)
Tracking who created and updated records is essential in enterprise systems. With EF Core Interceptors, you can automatically populate:

  • CreatedAt
  • UpdatedAt
  • UserId (current authenticated user)

Automatically fill audit fields.

Base Entity (Auditable)
We extend the base entity to include UserId.

public abstract class AuditableEntity
{
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public string UserId { get; set; } // Creator / last modifier
}
{
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Interceptor

public class AuditInterceptor : SaveChangesInterceptor
{
private readonly ICurrentUserService _currentUser;
public AuditInterceptor(ICurrentUserService currentUser) => _currentUser = currentUser;


public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
var context = eventData.Context;
var utcNow = DateTime.UtcNow;
var userId = _currentUser.GetUserId() ?? "system";


foreach (var entry in context.ChangeTracker.Entries<AuditableEntity>())
{
if (entry.State == EntityState.Added)
{
entry.Entity.CreatedAt = utcNow;
entry.Entity.UpdatedAt = utcNow;
entry.Entity.UserId = userId;
}
else if (entry.State == EntityState.Modified)
{
entry.Property(x => x.CreatedAt).IsModified = false;
entry.Property(x => x.UserId).IsModified = false;
entry.Entity.UpdatedAt = utcNow;
entry.Entity.UserId = userId;
}
}


return base.SavingChangesAsync(eventData, result, cancellationToken);
}
} : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
var context = eventData.Context;
var utcNow = DateTime.UtcNow;


foreach (var entry in context.ChangeTracker.Entries<AuditableEntity>())
{
if (entry.State == EntityState.Added)
{
entry.Entity.CreatedAt = utcNow;
entry.Entity.UpdatedAt = utcNow;
}
else if (entry.State == EntityState.Modified)
{
entry.Property(x => x.CreatedAt).IsModified = false;
entry.Entity.UpdatedAt = utcNow;
}
}


return base.SavingChangesAsync(eventData, result, cancellationToken);
}
}
Enter fullscreen mode Exit fullscreen mode

Register Interceptors

builder.Services.AddScoped<SoftDeleteInterceptor>();
builder.Services.AddScoped<AuditInterceptor>();


builder.Services.AddDbContext<AppDbContext>((sp, options) =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("Default"));
options.AddInterceptors(
sp.GetRequiredService<SoftDeleteInterceptor>(),
sp.GetRequiredService<AuditInterceptor>()
);
});
Enter fullscreen mode Exit fullscreen mode

🎯 Final Thoughts
EF Core Interceptors let you apply enterprise‑grade data rules without scattering logic across your application.
Use them for:

  • Logging SQL
  • Audit metadata
  • Soft delete
  • Multi‑tenancy
  • Security rules If you're building serious systems — start using interceptors today.

Top comments (0)