Published on

Interceptors, what are they for?

Authors

What are they for?

Interceptors offer a powerful way to enhance functionality in ASP.NET Core applications. One reason to use them is to implement a simple audit trail for your entities. However, they can also facilitate many other types of functionality, such as logging, performance monitoring, and even transaction management.

Sample

The plan is to provide a simple sample where we use interceptors to update the CreatedDate and UpdatedDate properties on our models. All models inheriting from BaseEntity must automatically have the CreatedDate and UpdatedDate populated.

First, I will start by preparing some simple EF models to use as sample data.

public class Article : BaseEntity
{
    [Key] public Guid Id { get; set; }
    public string? Title { get; set; }
    public string? Content { get; set; }
}

public abstract class BaseEntity : IEntity
{
    public DateTime? CreatedDate { get; set; }
    public DateTime? UpdatedDate { get; set; }
}

public interface IEntity
{
    DateTime? CreatedDate { get; set; }
    DateTime? UpdatedDate { get; set; }
}

Now lets create the interceptor.

using EFCore.Samples.Interceptor.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;

namespace EFCore.Samples.Interceptor.Interceptor;

public class AuditInterceptor : SaveChangesInterceptor
{
    public override InterceptionResult<int> SavingChanges(
        DbContextEventData eventData,
        InterceptionResult<int> result)
    {
        BeforeSaveTriggers(eventData.Context);
        return result;
    }

    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        BeforeSaveTriggers(eventData.Context);
        return ValueTask.FromResult(result);
    }

    private static void BeforeSaveTriggers(DbContext? context)
    {
        // We get all entity of interface type IEntity where state is Added or Modified
        var entries = context?.ChangeTracker
            .Entries()
            .Where(e => e is { Entity: IEntity, State: EntityState.Added or EntityState.Modified }).ToList();

        if (entries == null || entries.Count == 0) return;

        foreach (var entityEntry in entries)
        {
            // We always set the UpdatedDate. Only when Entity is being added, we add a value to CreatedDate
            ((IEntity)entityEntry.Entity).UpdatedDate = DateTime.UtcNow;
            if (entityEntry.State == EntityState.Added)
            {
                ((IEntity)entityEntry.Entity).CreatedDate = DateTime.UtcNow;
            }
        }
    }
}

Now, let's register our interceptor and tell Entity Framework to use it in Program.cs. We register our audit interceptor and resolve it for EF using the AddInterceptors method.

builder.Services.AddSingleton<AuditInterceptor>();
builder.Services.AddDbContext<ApplicationDbContext>((services, optionsBuilder) =>
{
    var interceptor = services.GetService<AuditInterceptor>();
    if (interceptor != null)
    {
        optionsBuilder.UseSqlite(connectionString).AddInterceptors(interceptor);
    }
});

Testing our code

Now lets test it: I created a MVC project with Entity Framework based on the DotNet template for testing this. Now lets have a look at the results

Interceptor sample

As you can see, the interceptor is being called on save, and the data is being updated accordingly. There’s no need to set CreatedDate an UpdatedDate manually anymore.

Repo

The link to the sample can be found here