Logo

dev-resources.site

for different kinds of informations.

How To Use EF Core Interceptors

Published at
11/20/2023
Categories
dotnet
efcore
entityframework
database
Author
milanjovanovictech
Author
18 person written this
milanjovanovictech
open
How To Use EF Core Interceptors

EF Core is my favorite ORM for .NET applications. Yet, its many fantastic features sometimes go unnoticed. For example, query splitting, query filters, 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 interceptorsallow 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.

public interface IInterceptor
{
}
Enter fullscreen mode Exit fullscreen mode

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.

public interface IAuditable
{
    DateTime CreatedOnUtc { get; }

    DateTime? ModifiedOnUtc { get; }
}
Enter fullscreen mode Exit fullscreen mode

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 UpdateAuditableInterceptorare also part of the current database transaction.

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

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Publish Domain Events With EF Interceptors

Another use case for EF interceptors is publishing domain events. 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.

using MediatR;

public interface IDomainEvent : INotification
{
}
Enter fullscreen mode Exit fullscreen mode

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.

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Store Outbox Messages With EF Interceptors

Instead of publishing domain events 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 with EF Core.

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

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

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

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:

Thanks for reading, and stay awesome!


P.S. Whenever you're ready, there are 2 ways I can help you:

  1. Pragmatic Clean Architecture: This comprehensive course will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture. Join 1,200+ students here.

  2. Patreon Community: Think like a senior software engineer with access to the source code I use in my YouTube videos and exclusive discounts for my courses. Join 930+ engineers here.

entityframework Article's
30 articles in total
Favicon
Entity Framework Core Code First
Favicon
Code First Approach with Entity Framework.
Favicon
Custom NET8 Entity Framework Core Generic Repository
Favicon
Link Many To Many entities with shadow join-table using Entity Framework Core
Favicon
Running Entity Framework Core Migrations with Optimizely CMS 12
Favicon
Check Pagination in .NET: With and Without Entity Framework
Favicon
EF Core 6 - correct types halving the execution time!
Favicon
EF Core 6 - This SqlTransaction has completed; it is no longer usable.
Favicon
Entity Framework Core Tutorial:Introduction to Entity Framework Core
Favicon
ReadOnly DbContext with Entity Framework
Favicon
[KOSD] Multiple Parallel Operations in Entity Framework Core (.NET 8)
Favicon
Entity Framework in .net core 6.0 - Code first and Database first approach
Favicon
5 EF Core Features You Need To Know
Favicon
C# | Best Practices for Pagination using EF Core 8
Favicon
C# | Using Entity Framework with PostgreSQL Database
Favicon
C# | Entity Framework Generic Repository with SOLID Design Pattern
Favicon
C# | Entity Framework Issues and Troubleshooting
Favicon
Entity Framework Core with Scalar Functions
Favicon
Prefer Empty Objects over Compiler tricks
Favicon
The Differences Between EntityFramework .Add and .AddAsync
Favicon
Entity FrameWork
Favicon
Load Appointments on Demand in Blazor Scheduler using Entity Framework Core
Favicon
Finding the Right Balance: Clean Architecture and Entity Framework in Practice
Favicon
Compilation steps in EF Core
Favicon
Learning is another full-time job.
Favicon
How To Use EF Core Interceptors
Favicon
Using Entity Framework Core 8 Owned Types HasData()
Favicon
Simple Event-Sourcing with EF Core and SQL Server
Favicon
Delete in EF 8 !
Favicon
Optimizing Database Access with Entity Framework - Lazy Loading vs. Eager Loading

Featured ones: