# How To Use EF Core Interceptors

> EF Core is my favorite ORM for .NET applications, but many of its fantastic features go unnoticed. Interceptors let you hook into materialization, handle optimistic concurrency errors, or add query hints. The most practical use case is adding behavior when saving changes to the database.

Published: 2023-11-18. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/how-to-use-ef-core-interceptors

**EF Core interceptors** let you intercept, change, or suppress EF Core operations.
Every interceptor implements the `IInterceptor` interface.
The most popular one is the `SaveChangesInterceptor`, which adds behavior before or after saving changes to the database.

EF Core is my favorite ORM for .NET applications.
Yet, its many fantastic features sometimes go unnoticed.
For example, [**query splitting**](https://milanjovanovic.tech/blog/how-to-improve-performance-with-ef-core-query-splitting),
[**query filters**](https://milanjovanovic.tech/blog/how-to-use-global-query-filters-in-ef-core),
and interceptors.

EF interceptors are interesting because you can do powerful things with them.
For example, you can hook into materialization, handle optimistic concurrency errors, or add query hints.

The most practical use case is adding behavior when saving changes to the database.

Today I want to show you three unique use cases for EF Core interceptors:

- Audit logging
- Publishing domain events
- Persisting Outbox messages

## What are EF Interceptors?

[EF Core interceptors](https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors)
allow you to intercept, change, or suppress EF Core operations.
Every interceptor implements the `IInterceptor` interface.
A few common derived interfaces include `IDbCommandInterceptor`, `IDbConnectionInterceptor`, and `IDbTransactionInterceptor`.

The most popular one is the `ISaveChangesInterceptor`. It allows you to add behavior before or after saving changes.

Interceptors are registered for each `DbContext` instance when configuring the context.

```csharp
public interface IInterceptor
{
}
```

You don't have to implement these interfaces directly.
It's better to use concrete implementations and override the needed methods.

For example, I'll show you how to use the `SaveChangesInterceptor`.

## Audit Logging With EF Interceptors

An audit log of entity changes is a valuable feature in some applications.
You write additional audit information every time an entity is created or modified.
The audit log could also contain the complete before/after values, depending on your requirements.

However, let's use a simple example to make it easy to understand.

I have an `IAuditable` interface with two properties representing when an entity was created or modified.

```csharp
public interface IAuditable
{
    DateTime CreatedOnUtc { get; }

    DateTime? ModifiedOnUtc { get; }
}
```

Next, I'll implement an `UpdateAuditableInterceptor` interceptor to write the audit values.
It uses the `ChangeTracker` to find all `IAuditable` instances and sets the respective property value.

I want to highlight that I'm overriding the `SavingChangesAsync` method here.
`SavingChangesAsync` runs before the changes are saved in the database and any updates applied inside the `UpdateAuditableInterceptor`
are also part of the current database transaction.

This implementation can be easily extended to include the information about the current user.

```csharp
internal sealed class UpdateAuditableInterceptor : SaveChangesInterceptor
{
    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        if (eventData.Context is not null)
        {
            UpdateAuditableEntities(eventData.Context);
        }

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

    private static void UpdateAuditableEntities(DbContext context)
    {
        DateTime utcNow = DateTime.UtcNow;
        var entities = context.ChangeTracker.Entries<IAuditable>().ToList();

        foreach (EntityEntry<IAuditable> entry in entities)
        {
            if (entry.State == EntityState.Added)
            {
                SetCurrentPropertyValue(
                    entry, nameof(IAuditable.CreatedOnUtc), utcNow);
            }

            if (entry.State == EntityState.Modified)
            {
                SetCurrentPropertyValue(
                    entry, nameof(IAuditable.ModifiedOnUtc), utcNow);
            }
        }

        static void SetCurrentPropertyValue(
            EntityEntry entry,
            string propertyName,
            DateTime utcNow) =>
            entry.Property(propertyName).CurrentValue = utcNow;
    }
}
```

## Publish Domain Events With EF Interceptors

Another use case for EF interceptors is [**publishing domain events.**](https://milanjovanovic.tech/blog/how-to-use-domain-events-to-build-loosely-coupled-systems)
Domain events are a DDD tactical pattern to create loosely coupled systems.

Domain events allow you to express side effects explicitly and provide a better separation of concerns in the domain.

You can create an `IDomainEvent` interface, which derives from `MediatR.INotification`.
This allows you to use the `IPublisher` to publish domain events and handle them asynchronously.

```csharp
using MediatR;

public interface IDomainEvent : INotification
{
}
```

Then, I'll create a `PublishDomainEventsInterceptor` that also inherits from `SaveChangesInterceptor`.
However, this time, we're using the `SavedChangesAsync` to publish the domain events _after_ saving changes in the database.

This has two important implications:

1. The entire workflow is now eventually consistent. Domain event handlers will save changes to the database after the original transaction.
2. If any domain event handlers fail, we risk failing the request even though the initial transaction was completed successfully.

You can make this process more reliable by using an [**Outbox.**](https://milanjovanovic.tech/blog/outbox-pattern-for-reliable-microservices-messaging)

```csharp
internal sealed class PublishDomainEventsInterceptor : SaveChangesInterceptor
{
    private readonly IPublisher _publisher;

    public PublishDomainEventsInterceptor(IPublisher publisher)
    {
        _publisher = publisher;
    }

    public override async ValueTask<int> SavedChangesAsync(
        SaveChangesCompletedEventData eventData,
        int result,
        CancellationToken cancellationToken = default)
    {
        if (eventData.Context is not null)
        {
            await PublishDomainEventsAsync(eventData.Context);
        }

        return result;
    }

    private async Task PublishDomainEventsAsync(DbContext context)
    {
        var domainEvents = context
            .ChangeTracker
            .Entries<Entity>()
            .Select(entry => entry.Entity)
            .SelectMany(entity =>
            {
                List<IDomainEvent> domainEvents = entity.DomainEvents;

                entity.ClearDomainEvents();

                return domainEvents;
            })
            .ToList();

        foreach (IDomainEvent domainEvent in domainEvents)
        {
            await _publisher.Publish(domainEvent);
        }
    }
}
```

## Store Outbox Messages With EF Interceptors

Instead of [**publishing domain events**](https://milanjovanovic.tech/blog/how-to-use-domain-events-to-build-loosely-coupled-systems) as part of the EF transaction, you can convert them to Outbox messages.

Here's an `InsertOutboxMessagesInterceptor` that does precisely this.

It overrides the `SavingChangesAsync` method.
Which means it runs inside the current EF transaction before saving changes.

The `InsertOutboxMessagesInterceptor` converts any domain events into an `OutboxMessage` and adds it to the respective `DbSet<OutboxMessage>`.
This means they will be saved to the database with any existing changes inside the same transaction.

This is an atomic operation.

Either everything succeeds or everything fails.

There's no in-between state like in the `PublishDomainEventsInterceptor`.

You can then create a background worker that will process the Outbox messages.

And this is how you implement the [**Outbox pattern**](https://milanjovanovic.tech/blog/outbox-pattern-for-reliable-microservices-messaging) with EF Core.

```csharp
using Newtonsoft.Json;

public sealed class InsertOutboxMessagesInterceptor : SaveChangesInterceptor
{
    private static readonly JsonSerializerSettings Serializer = new()
    {
        TypeNameHandling = TypeNameHandling.All
    };

    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        if (eventData.Context is not null)
        {
            InsertOutboxMessages(eventData.Context);
        }

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

    private static void InsertOutboxMessages(DbContext context)
    {
        context
            .ChangeTracker
            .Entries<Entity>()
            .Select(entry => entry.Entity)
            .SelectMany(entity =>
            {
                List<IDomainEvent> domainEvents = entity.DomainEvents;

                entity.ClearDomainEvents();

                return domainEvents;
            })
            .Select(domainEvent => new OutboxMessage
            {
                Id = domainEvent.Id,
                OccurredOnUtc = domainEvent.OccurredOnUtc,
                Type = domainEvent.GetType().Name,
                Content = JsonConvert.SerializeObject(domainEvent, Serializer)
            })
            .ToList();

        context.Set<OutboxMessage>().AddRange(outboxMessages);
    }
}
```

## Configuring EF Interceptors Using Dependency Injection

EF interceptors should be lightweight and stateless.
You can add them to the `DbContext` by calling `AddInterceptors` and passing in the interceptor instances.

I like to configure the interceptors with Dependency Injection for two reasons:

- It allows me also to use DI in the interceptors (be mindful that they are singletons)
- To simplify adding the interceptors to the `DbContext` using `AddDbContext`

Here's how you can configure the `UpdateAuditableInterceptor` and `InsertOutboxMessagesInterceptor` with the `ApplicationDbContext`:

```csharp
services.AddSingleton<UpdateAuditableInterceptor>();
services.AddSingleton<InsertOutboxMessagesInterceptor>();

services.AddDbContext<IApplicationDbContext, ApplicationDbContext>(
    (sp, options) => options
        .UseSqlServer(connectionString)
        .AddInterceptors(
            sp.GetRequiredService<UpdateAuditableInterceptor>(),
            sp.GetRequiredService<InsertOutboxMessagesInterceptor>()));
```

## Closing Thoughts

Interceptors allow you to do almost anything with an EF Core operation.
But with great power comes great responsibility.
You should be mindful that interceptors have an impact on performance.
Calls to external services or handling events will slow down the operation.

Remember, you don't necessarily have to use EF interceptors.
You can achieve the same behavior by overriding the `SaveChangesAsync` method on the `DbContext` and adding your custom logic there.

I showed you a few practical use cases for EF interceptors in this week's issue.

But, if you want to see more examples, I have a few videos about:

- [**Auditing logging**](https://youtu.be/mAlO3OuoQvo)
- [**Publishing domain events**](https://youtu.be/AHzWJ_SMqLo)
- [**Implementing the Outbox pattern**](https://youtu.be/XALvnX7MPeo)

Thanks for reading, and stay awesome!

---

## Frequently asked questions

### What are EF Core interceptors?

EF Core interceptors let you intercept, change, or suppress EF Core operations. Every interceptor implements the IInterceptor interface, with derived interfaces like IDbCommandInterceptor and IDbTransactionInterceptor. The most popular one is the SaveChangesInterceptor, which adds behavior before or after saving changes.

### What is the difference between SavingChangesAsync and SavedChangesAsync in EF Core?

SavingChangesAsync runs before changes are saved, so any updates you make join the current database transaction. SavedChangesAsync runs after the save, so work done there is eventually consistent and can fail even though the original transaction already committed.

### How do you implement audit logging with EF Core?

Create a SaveChangesInterceptor that overrides SavingChangesAsync, use the ChangeTracker to find entities implementing an IAuditable interface, and set CreatedOnUtc or ModifiedOnUtc based on the entity state. The audit values are saved in the same transaction as the changes.

### How do you implement the outbox pattern with EF Core?

Use a SaveChangesInterceptor that converts domain events into OutboxMessage rows inside SavingChangesAsync. The messages are saved atomically in the same transaction as the business changes, and a background worker processes them later. Either everything succeeds or everything fails.

### How do you register EF Core interceptors with dependency injection?

Register the interceptors as singletons, then use the AddDbContext overload that exposes the service provider and pass them to options.AddInterceptors via GetRequiredService. This lets the interceptors use dependency injection themselves, keeping in mind they are singletons.

### Do EF Core interceptors affect performance?

Yes. Calls to external services or handling events inside an interceptor slow down the operation, so interceptors should stay lightweight and stateless. As an alternative, you can override SaveChangesAsync on the DbContext and add the custom logic there.
