<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged interceptors</title>
<link>https://coderlegion.com/tag/interceptors</link>
<description>Powered by Question2Answer</description>
<item>
<title>Reliable Messaging in .NET: Domain Events and the Outbox Pattern with EF Core Interceptors</title>
<link>https://coderlegion.com/6363/reliable-messaging-in-net-domain-events-and-the-outbox-pattern-with-ef-core-interceptors</link>
<description>&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Distributed systems are powerful — but they come with one big challenge: &lt;strong&gt;reliability&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;What happens if your application updates a record in the database and then tries to publish a message to a message broker… but crashes in between?&lt;br&gt;
You’ve just lost an event.&lt;/p&gt;
&lt;p&gt;This problem is common in microservices, and it’s why the &lt;strong&gt;Outbox Pattern&lt;/strong&gt; exists — a simple, elegant approach that guarantees message delivery without introducing distributed transactions or external coordinators.&lt;/p&gt;
&lt;p&gt;In this article, we’ll implement a &lt;strong&gt;Domain Event + Outbox Pattern&lt;/strong&gt; in &lt;strong&gt;.NET 9&lt;/strong&gt;, using &lt;strong&gt;Entity Framework Core&lt;/strong&gt;, &lt;strong&gt;SQLite&lt;/strong&gt;, and an &lt;strong&gt;EF Core SaveChangesInterceptor&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-a-quick-look-at-entity-framework-core&quot;&gt;⚙️ A Quick Look at Entity Framework Core&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Entity Framework Core (EF Core)&lt;/strong&gt; is Microsoft’s modern &lt;strong&gt;Object-Relational Mapper (ORM)&lt;/strong&gt; for .NET.&lt;br&gt;
It allows developers to work with a database using &lt;strong&gt;C# classes&lt;/strong&gt; instead of raw SQL, automatically handling relationships, migrations, and change tracking.&lt;/p&gt;
&lt;p&gt;When you call &lt;code&gt;SaveChanges()&lt;/code&gt; or &lt;code&gt;SaveChangesAsync()&lt;/code&gt;, EF Core:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Tracks your entity changes.&lt;/li&gt;
&lt;li&gt;Generates SQL commands.&lt;/li&gt;
&lt;li&gt;Executes them in a transaction.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This makes EF Core the perfect place to integrate the &lt;strong&gt;Outbox Pattern&lt;/strong&gt;, since we can hook into its save pipeline and ensure domain events are written in the same transaction as the data itself.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-are-interceptors-&quot;&gt;What Are Interceptors?&lt;/h3&gt;
&lt;p&gt;In EF Core, &lt;strong&gt;interceptors&lt;/strong&gt; are powerful hooks that allow you to intercept and extend the framework’s internal operations — such as saving, querying, or connecting to the database.&lt;/p&gt;
&lt;p&gt;They work like &lt;strong&gt;middleware for EF Core&lt;/strong&gt;:&lt;br&gt;
you can observe, modify, or add logic before and after EF performs certain actions.&lt;/p&gt;
&lt;p&gt;There are several types:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SaveChangesInterceptor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CommandInterceptor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ConnectionInterceptor&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TransactionInterceptor&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this article, we’ll use &lt;strong&gt;SaveChangesInterceptor&lt;/strong&gt; to detect domain events before EF saves, serialize them, and insert them into our Outbox table.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;In traditional architectures, your service might do something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;await _db.SaveChangesAsync();
await _bus.PublishAsync(new OrderPlacedEvent(orderId));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If your app crashes after saving but before publishing, your event never reaches other services — resulting in inconsistent state across systems.&lt;/p&gt;
&lt;p&gt;We need a way to ensure &lt;strong&gt;atomicity&lt;/strong&gt;: either both the data and the event are persisted, or neither are.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-outbox-pattern-in-a-nutshell-&quot;&gt;The Outbox Pattern (In a Nutshell)&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;Outbox Pattern&lt;/strong&gt; solves this by introducing a dedicated &lt;strong&gt;Outbox table&lt;/strong&gt; inside your database.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When your aggregate raises a domain event, it’s &lt;strong&gt;captured and saved&lt;/strong&gt; into the Outbox table &lt;strong&gt;within the same transaction&lt;/strong&gt; as your entity.&lt;/li&gt;
&lt;li&gt;A background worker (or a separate service) &lt;strong&gt;polls&lt;/strong&gt; the Outbox table, &lt;strong&gt;publishes events&lt;/strong&gt;, and marks them as processed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This way, even if your app crashes, the events are safely stored and can be retried later.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-architecture&quot;&gt;The Architecture&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;┌──────────────────────────────┐
│         Application          │
│ ──────────────────────────── │
│  Order raises DomainEvent    │
│  → EF Interceptor writes     │
│    Outbox row                │
│  → Transaction commits       │
└──────────────┬───────────────┘
               │
               ▼
        Outbox Dispatcher
       ├─ Polls database
       ├─ Publishes messages
       └─ Marks them processed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern elegantly blends &lt;strong&gt;Domain-Driven Design&lt;/strong&gt; concepts with &lt;strong&gt;event-driven reliability&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;domain-events&quot;&gt;Domain Events&lt;/h3&gt;
&lt;p&gt;Domain events capture something meaningful that happened in your domain model.&lt;br&gt;
For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public sealed record OrderPlacedDomainEvent(Guid OrderId, decimal Total)
    : DomainEvent(DateTime.UtcNow);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And your aggregate raises them naturally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;public sealed class Order : IHasDomainEvents
{
    private readonly List&amp;lt;DomainEvent&amp;gt; _domainEvents = new();
    public Guid Id { get; private set; } = Guid.NewGuid();
    public decimal Total { get; private set; }

    public IReadOnlyCollection&amp;lt;DomainEvent&amp;gt; DomainEvents =&amp;gt; _domainEvents;

    public Order(decimal total)
    {
        Total = total;
        AddEvent(new OrderPlacedDomainEvent(Id, Total));
    }

    private void AddEvent(DomainEvent @event) =&amp;gt; _domainEvents.Add(@event);
    public void ClearDomainEvents() =&amp;gt; _domainEvents.Clear();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-capturing-events-with-an-ef-core-interceptor&quot;&gt;⚙️ Capturing Events with an EF Core Interceptor&lt;/h3&gt;
&lt;p&gt;Instead of overriding &lt;code&gt;SaveChangesAsync()&lt;/code&gt; directly inside &lt;code&gt;AppDbContext&lt;/code&gt;, we can use a &lt;strong&gt;SaveChangesInterceptor&lt;/strong&gt; — a clean, composable way to plug custom logic into EF’s lifecycle.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;
public sealed class OutboxSaveChangesInterceptor(IEventSerializer serializer) : SaveChangesInterceptor
{
    /// &amp;lt;summary&amp;gt;
    /// 
    /// &amp;lt;/summary&amp;gt;
    /// &amp;lt;param name=&quot;eventData&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;result&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;param name=&quot;cancellationToken&quot;&amp;gt;&amp;lt;/param&amp;gt;
    /// &amp;lt;returns&amp;gt;&amp;lt;/returns&amp;gt;
    public override async ValueTask&amp;lt;InterceptionResult&amp;lt;int&amp;gt;&amp;gt; SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult&amp;lt;int&amp;gt; result,
        CancellationToken cancellationToken = default)
    {
        var context = eventData.Context;
        if (context is null) 
            return await base.SavingChangesAsync(eventData, result, cancellationToken);

        var aggregates = context.ChangeTracker
                    .Entries()
                    .Where(e =&amp;gt; e.Entity is IHasDomainEvents hasEvents &amp;amp;&amp;amp; hasEvents.DomainEvents.Count &amp;gt; 0)
                    .Select(e =&amp;gt; (IHasDomainEvents)e.Entity)
                    .ToList();

        if (aggregates.Count == 0)
            return await base.SavingChangesAsync(eventData, result, cancellationToken);

        var outbox = context.Set&amp;lt;OutboxMessage&amp;gt;();

        foreach (var aggregate in aggregates)
        {
            foreach (var @event in aggregate.DomainEvents)
            {
                outbox.Add(new OutboxMessage
                {
                    OccurredOnUtc = @event.OccurredOnUtc,
                    Type = @event.GetType().AssemblyQualifiedName!,
                    Payload = serializer.Serialize(@event)
                });
            }
        }

        return await base.SavingChangesAsync(eventData, result, cancellationToken);
    }

}

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now every time you call &lt;code&gt;SaveChangesAsync()&lt;/code&gt;, domain events are written to your &lt;strong&gt;OutboxMessages&lt;/strong&gt; table automatically — in the same transaction.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-outbox-dispatcher&quot;&gt;The Outbox Dispatcher&lt;/h3&gt;
&lt;p&gt;Once the transaction is committed, a background worker can safely process these messages.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-csharp&quot;&gt;
public sealed class OutboxDispatcher(IServiceProvider sp, 
    ILogger&amp;lt;OutboxDispatcher&amp;gt; logger) : BackgroundService
{
    private readonly IServiceProvider _sp = sp;
    private readonly ILogger&amp;lt;OutboxDispatcher&amp;gt; _logger = logger;
    private readonly TimeSpan _pollInterval = TimeSpan.FromSeconds(2);
    private const int BatchSize = 50;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation(&quot; Outbox Dispatcher started...&quot;);

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                using var scope = _sp.CreateScope();
                var db = scope.ServiceProvider.GetRequiredService&amp;lt;AppDbContext&amp;gt;();

                var jsonSerializer = scope.ServiceProvider.GetRequiredService&amp;lt;IEventSerializer&amp;gt;();

                var pending = await db.OutboxMessages
                    .Where(x =&amp;gt; x.ProcessedOnUtc == null)
                    .OrderBy(x =&amp;gt; x.Id)
                    .Take(BatchSize)
                    .ToListAsync(stoppingToken);

                foreach (var msg in pending)
                {
                    try
                    {
                        var evt = jsonSerializer.Deserialize(msg.Payload, msg.Type);
                        await SimulatedBusPublish(evt, stoppingToken);
                        msg.ProcessedOnUtc = DateTime.UtcNow;
                        msg.Error = null;
                    }
                    catch (Exception ex)
                    {
                        msg.Error = ex.Message;
                        _logger.LogError(ex, &quot;❌ Error dispatching OutboxMessage {Id}&quot;, msg.Id);
                    }
                }

                if (pending.Count &amp;gt; 0)
                    await db.SaveChangesAsync(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, &quot;Outbox dispatcher loop error&quot;);
            }

            await Task.Delay(_pollInterval, stoppingToken);
        }
    }

    private Task SimulatedBusPublish(DomainEvent evt, CancellationToken ct)
        =&amp;gt; evt switch
        {
            OrderPlacedDomainEvent e =&amp;gt; HandleOrderPlaced(e),
            _ =&amp;gt; Task.CompletedTask
        };

    private Task HandleOrderPlaced(OrderPlacedDomainEvent e)
    {
        Console.WriteLine($&quot;[BUS] ✅ OrderPlaced published =&amp;gt; OrderId={e.OrderId}, Total={e.Total}&quot;);

        return Task.CompletedTask;
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-use-an-interceptor-&quot;&gt;Why Use an Interceptor?&lt;/h3&gt;
&lt;p&gt;Using an &lt;strong&gt;interceptor&lt;/strong&gt; instead of overriding &lt;code&gt;SaveChangesAsync()&lt;/code&gt; has several advantages:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Approach                      &lt;/th&gt;&lt;th&gt;Pros                          &lt;/th&gt;&lt;th&gt;Cons&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Override SaveChangesAsync&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Simple to start               &lt;/td&gt;&lt;td&gt;Harder to reuse across contexts&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Use Interceptor&lt;/strong&gt;           &lt;/td&gt;&lt;td&gt;Decoupled, reusable, testable &lt;/td&gt;&lt;td&gt;Slightly more setup&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Interceptors follow the &lt;strong&gt;Open/Closed Principle&lt;/strong&gt; — your DbContext remains focused on persistence, while cross-cutting logic (like event logging, auditing, or outbox handling) is easily attachable.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;benefits-of-the-outbox-pattern&quot;&gt;Benefits of the Outbox Pattern&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Transactional Safety&lt;/strong&gt; — data + events saved atomically&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Guaranteed Delivery&lt;/strong&gt; — retries handled by dispatcher&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;DDD Alignment&lt;/strong&gt; — aggregates raise domain events naturally&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Resilience&lt;/strong&gt; — works even if message broker is offline&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Extensibility&lt;/strong&gt; — can integrate with RabbitMQ, Kafka, or Azure Service Bus later&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;references&quot;&gt;References&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors&quot;&gt;Microsoft Docs – EF Core Interceptors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://martinfowler.com/articles/patterns-of-distributed-systems/transactional-outbox.html&quot;&gt;Martin Fowler – Transactional Outbox Pattern&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://udidahan.com/2008/05/14/when-to-use-sagas/&quot;&gt;Udi Dahan – Reliable Messaging without Distributed Transactions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/dotnet/core/extensions/workers&quot;&gt;Microsoft Docs – BackgroundService in .NET&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.jimmybogard.com/domain-events-outbox-pattern/&quot;&gt;Jimmy Bogard – Domain Events and the Outbox Pattern in .NET&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/stevsharp/OutboxPattern&quot;&gt;Source Code &lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-closing-thoughts&quot;&gt;✨ Closing Thoughts&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;Domain Events + Outbox Pattern&lt;/strong&gt; is one of the most practical ways to achieve &lt;strong&gt;reliable, eventually consistent event-driven systems&lt;/strong&gt; in .NET.&lt;/p&gt;
&lt;p&gt;By combining &lt;strong&gt;Entity Framework Core&lt;/strong&gt;, &lt;strong&gt;SaveChangesInterceptor&lt;/strong&gt;, and a simple &lt;strong&gt;background dispatcher&lt;/strong&gt;, you can ensure every event is persisted and delivered — even in the face of failure.&lt;/p&gt;
&lt;p&gt;Small pattern. Big reliability. &lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;author&quot;&gt;Author&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Spyros Ponaris&lt;/strong&gt;&lt;br&gt;
 &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/spyros-ponaris-913a6937/&quot;&gt;LinkedIn&lt;/a&gt;&lt;br&gt;
 Senior Software Engineer &amp;amp; .NET Enthusiast&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/6363/reliable-messaging-in-net-domain-events-and-the-outbox-pattern-with-ef-core-interceptors</guid>
<pubDate>Mon, 13 Oct 2025 11:27:53 +0000</pubDate>
</item>
<item>
<title>EF Core Interceptors: Supercharge Your Data Access with the Decorator Pattern</title>
<link>https://coderlegion.com/1602/ef-core-interceptors-supercharge-your-data-access-with-the-decorator-pattern</link>
<description>&lt;p&gt;Learn how to hook into EF Core operations using interceptors, leverage the &lt;strong&gt;Decorator Pattern&lt;/strong&gt; for clean cross-cutting concerns, and see real-world examples like SQL logging and auditing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Table of Contents&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A Brief History of EF Core Interceptors&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What Are EF Core Interceptors?&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EF Core Interceptors and the Decorator Pattern&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Top Use Cases for EF Core Interceptors&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example 1: Logging SQL Queries&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Example 2: Auditing SaveChanges&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Final Thoughts&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;a-brief-history-of-ef-core-interceptors&quot;&gt;A Brief History of EF Core Interceptors&lt;/h3&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;Since then, Microsoft has expanded interceptor capabilities in newer versions:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EF Core 5.0&lt;/strong&gt;: Introduced SaveChanges interceptors for tracking changes at the context level.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EF Core 6.0+&lt;/strong&gt;: Continued performance improvements and better support for asynchronous interception.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What Are EF Core Interceptors?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;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:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;SQL command execution&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Connection opening/closing&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Transactions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SaveChanges lifecycle&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This lets you inject custom logic like &lt;strong&gt;logging&lt;/strong&gt;, &lt;strong&gt;auditing&lt;/strong&gt;, &lt;strong&gt;validation&lt;/strong&gt;, or performance metrics without modifying your application’s core logic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EF Core Interceptors and the Decorator Pattern&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The core EF operation is the base object.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Your interceptor is the decorator.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;You add behavior before or after EF Core’s operation — without altering EF’s core.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This approach promotes clean code, separation of concerns, and makes your custom behavior reusable and testable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Top Use Cases for EF Core Interceptors&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;– SQL Query Logging &lt;/p&gt;
&lt;p&gt;– Track database queries for performance or debugging. &lt;/p&gt;
&lt;p&gt;– Auditing – Automatically log user actions and timestamps. &lt;/p&gt;
&lt;p&gt;– Soft Deletes – Intercept delete commands and convert them to updates. &lt;/p&gt;
&lt;p&gt;– Command Modification – Change or enrich raw SQL before execution. &lt;/p&gt;
&lt;p&gt;– Multi-Tenant Filtering – Inject filters dynamically for multi-tenant scenarios.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example 1: Logging SQL Queries&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Let’s log every SQL command executed by EF Core.&lt;/p&gt;
&lt;p&gt;Step 1: Create a SQL Logging Interceptor&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;using Microsoft.EntityFrameworkCore.Diagnostics;
using System.Diagnostics;

public class SqlLoggingInterceptor : DbCommandInterceptor
{
    public override InterceptionResult&amp;lt;DbDataReader&amp;gt; ReaderExecuting(
        DbCommand command, 
        CommandEventData eventData, 
        InterceptionResult&amp;lt;DbDataReader&amp;gt; result)
    {
        Debug.WriteLine($&quot;SQL Executed: {command.CommandText}&quot;);
        return base.ReaderExecuting(command, eventData, result);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SQL Executed: SELECT * FROM [Users] WHERE [IsActive] = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Register the Interceptor&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public class AppDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer(&quot;YourConnectionString&quot;)
            .AddInterceptors(new SqlLoggingInterceptor());
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Example 2: Auditing SaveChanges&lt;/p&gt;
&lt;p&gt;Let’s implement automatic auditing of entity modifications.&lt;/p&gt;
&lt;p&gt;Step 1: Define an Auditable Entity Base Class&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public abstract class AuditableEntity
{
    public DateTime ModifiedAt { get; set; }
    public string ModifiedBy { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Step 2: Implement a SaveChangesInterceptor&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;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&amp;lt;AuditableEntity&amp;gt;()
            .Where(e =&amp;gt; e.State == EntityState.Modified);

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

        return base.SavingChanges(eventData, result);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Final Thoughts&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;Whether you need to log queries, audit changes, enforce soft deletes, or handle custom behavior, interceptors offer a powerful and elegant solution.&lt;/p&gt;
&lt;p&gt;References&lt;br&gt;
EF Core Documentation – Interceptors&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors&quot;&gt;https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;EF Core 3.0 Release Notes (Interceptors introduced)&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/whatsnew&quot;&gt;https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-3.0/whatsnew&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;EF Core SaveChanges Interceptor Docs&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors#savechanges-interception&quot;&gt;https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors#savechanges-interception&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Decorator Pattern - Refactoring Guru (for pattern explanation)&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://refactoring.guru/design-patterns/decorator&quot;&gt;https://refactoring.guru/design-patterns/decorator&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;EF Core GitHub Repository (for latest updates and issues)&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/dotnet/efcore&quot;&gt;https://github.com/dotnet/efcore&lt;/a&gt;&lt;/p&gt;
</description>
<category>C#</category>
<guid isPermaLink="true">https://coderlegion.com/1602/ef-core-interceptors-supercharge-your-data-access-with-the-decorator-pattern</guid>
<pubDate>Thu, 20 Mar 2025 08:25:11 +0000</pubDate>
</item>
</channel>
</rss>