EF Core Interceptors: Supercharge Your Data Access with the Decorator Pattern

posted 3 min read

Learn how to hook into EF Core operations using interceptors, leverage the Decorator Pattern for clean cross-cutting concerns, and see real-world examples like SQL logging and auditing.

Table of Contents

  • A Brief History of EF Core Interceptors

  • What Are EF Core Interceptors?

  • EF Core Interceptors and the Decorator Pattern

  • Top Use Cases for EF Core Interceptors

  • Example 1: Logging SQL Queries

  • Example 2: Auditing SaveChanges

  • Final Thoughts

A Brief History of EF Core Interceptors

EF Core interceptors were first introduced in EF Core 3.0 (released in September 2019). This version marked a significant step forward in EF Core's extensibility, allowing developers to intercept low-level database operations like commands, connections, and transactions.

Since then, Microsoft has expanded interceptor capabilities in newer versions:

EF Core 5.0: Introduced SaveChanges interceptors for tracking changes at the context level.

EF Core 6.0+: Continued performance improvements and better support for asynchronous interception.

What Are EF Core Interceptors?

In Entity Framework Core (EF Core), interceptors allow developers to hook into the execution pipeline of EF Core's operations. Think of them as middleware for your database interactions. You can intercept events such as:

  1. SQL command execution

  2. Connection opening/closing

  3. Transactions

  4. SaveChanges lifecycle

This lets you inject custom logic like logging, auditing, validation, or performance metrics without modifying your application’s core logic.

EF Core Interceptors and the Decorator Pattern

EF Core interceptors are a real-world application of the Decorator Pattern, a classic design pattern where functionality is wrapped around existing behavior to extend or alter it.

The core EF operation is the base object.
Your interceptor is the decorator.
You add behavior before or after EF Core’s operation — without altering EF’s core.

This approach promotes clean code, separation of concerns, and makes your custom behavior reusable and testable.

Top Use Cases for EF Core Interceptors

– SQL Query Logging

– Track database queries for performance or debugging.

– Auditing – Automatically log user actions and timestamps.

– Soft Deletes – Intercept delete commands and convert them to updates.

– Command Modification – Change or enrich raw SQL before execution.

– Multi-Tenant Filtering – Inject filters dynamically for multi-tenant scenarios.

Example 1: Logging SQL Queries

Let’s log every SQL command executed by EF Core.

Step 1: Create a SQL Logging Interceptor

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

public class SqlLoggingInterceptor : DbCommandInterceptor
{
    public override InterceptionResult<DbDataReader> ReaderExecuting(
        DbCommand command, 
        CommandEventData eventData, 
        InterceptionResult<DbDataReader> result)
    {
        Debug.WriteLine($"SQL Executed: {command.CommandText}");
        return base.ReaderExecuting(command, eventData, result);
    }
}

Output:

SQL Executed: SELECT * FROM [Users] WHERE [IsActive] = 1

Step 2: Register the Interceptor

public class AppDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer("YourConnectionString")
            .AddInterceptors(new SqlLoggingInterceptor());
    }
}

Example 2: Auditing SaveChanges

Let’s implement automatic auditing of entity modifications.

Step 1: Define an Auditable Entity Base Class

public abstract class AuditableEntity
{
    public DateTime ModifiedAt { get; set; }
    public string ModifiedBy { get; set; }
}

Step 2: Implement a SaveChangesInterceptor

public class AuditInterceptor : SaveChangesInterceptor
{
    private readonly string _currentUser;

    public AuditInterceptor(string currentUser)
    {
        _currentUser = currentUser;
    }

    public override int SavingChanges(DbContextEventData eventData, int result)
    {
        var context = eventData.Context;
        if (context == null) return base.SavingChanges(eventData, result);

        var entries = context.ChangeTracker
            .Entries<AuditableEntity>()
            .Where(e => e.State == EntityState.Modified);

        foreach (var entry in entries)
        {
            entry.Entity.ModifiedAt = DateTime.UtcNow;
            entry.Entity.ModifiedBy = _currentUser;
        }

        return base.SavingChanges(eventData, result);
    }
}

Final Thoughts

EF Core interceptors are an underutilized gem in the .NET ecosystem. They let you cleanly implement cross-cutting concerns without muddying your core application code. And because they follow the Decorator Pattern, you get flexibility, maintainability, and testability.

Whether you need to log queries, audit changes, enforce soft deletes, or handle custom behavior, interceptors offer a powerful and elegant solution.

References
EF Core Documentation – Interceptors
https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors

EF Core 3.0 Release Notes (Interceptors introduced)
https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/whatsnew

EF Core SaveChanges Interceptor Docs
https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors#savechanges-interception

Decorator Pattern - Refactoring Guru (for pattern explanation)
https://refactoring.guru/design-patterns/decorator

EF Core GitHub Repository (for latest updates and issues)
https://github.com/dotnet/efcore

If you read this far, tweet to the author to show them you care. Tweet a Thanks

Great article! The Decorator Pattern connection was spot on. Do you think interceptors could slow down performance in large apps?

Thanks, I really appreciate that! Great question — interceptors can add some overhead, especially if they’re doing a lot of work or used too much. In big apps, it’s worth keeping an eye on, but in most cases, the impact is small compared to how useful they are.

If used wisely and kept simple, they usually work well even at scale.

In my next article, I’ll dive into the new batch update and batch delete features , they offer impressive speed improvements, but also come with their own set of pros and cons.

More Posts

EF Core: Lazy Loading, Eager Loading, and Loading Data on Demand

Spyros - Mar 6

EF Core Bulk Updates and Deletes: Performance vs Change Tracking

Spyros - Mar 24

EF Core Global Query Filters: A Complete Guide

Spyros - Mar 2

When NOT to Use AsSplitQuery() in EF.core

Spyros - Feb 26

Optimistic vs. Pessimistic Concurrency in EF Core (with Table Hints)

Spyros - Mar 31
chevron_left