Skip to content

EF Core Interceptors

Eduardo Fonseca edited this page Jul 25, 2024 · 1 revision

Tutorial: Implementing a SaveChangesInterceptor in Entity Framework Core

This tutorial will guide you through the process of creating an interceptor in Entity Framework Core that customizes the behavior of saving changes to the database. We'll be creating a SaveChangesInterceptor that sets metadata on entities implementing a specific interface.

Step 1: Define the IOriginatorInfo Interface

Here is the IOriginatorInfo interface that will be implemented by the entities you want to intercept.

namespace FairPlayCombined.DataAccess.Interceptors.Interfaces
{
    public interface IOriginatorInfo
    {
        string SourceApplication { get; set; }
        string OriginatorIpaddress { get; set; }
        DateTimeOffset RowCreationDateTime { get; set; }
        string RowCreationUser { get; set; }
    }
}

Step 2: Define the IUserProviderService Interface

Here is the IUserProviderService interface that will provide the current user ID and other user-related information.

namespace FairPlayCombined.Interfaces
{
    public interface IUserProviderService
    {
        string? GetCurrentUserId();
        string? GetAccessToken();
        bool IsAuthenticatedWithGoogle();
    }
}

Step 3: Implement the UserProviderService Class

Here is the UserProviderService class that implements the IUserProviderService interface.

using FairPlayCombined.Interfaces;
using Microsoft.AspNetCore.Http;
using System.Linq;

namespace FairPlayCombined.Services.Common
{
    public class UserProviderService : IUserProviderService
    {
        private readonly IHttpContextAccessor _httpContextAccessor;

        public UserProviderService(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }

        public string? GetAccessToken()
        {
            throw new NotImplementedException();
        }

        public string? GetCurrentUserId()
        {
            string? result = default;
            if (_httpContextAccessor?.HttpContext?.User?.Identity?.IsAuthenticated == true)
            {
                var claimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier";
                result = _httpContextAccessor.HttpContext.User.Claims.Single(p => p.Type == claimType)?.Value;
            }
            return result;
        }

        public bool IsAuthenticatedWithGoogle()
        {
            var result = _httpContextAccessor.HttpContext.User
                .HasClaim("http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationmethod",
                "Google");
            return result;
        }
    }
}

Step 4: Implement the SaveChangesInterceptor Class

Here is the SaveChangesInterceptor class that implements ISaveChangesInterceptor.

using FairPlayCombined.Interfaces;
using FairPlayCombined.DataAccess.Interceptors.Interfaces;
using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace FairPlayCombined.DataAccess.Interceptors
{
    public class SaveChangesInterceptor : ISaveChangesInterceptor
    {
        private readonly IUserProviderService _userProviderService;

        public SaveChangesInterceptor(IUserProviderService userProviderService)
        {
            _userProviderService = userProviderService;
        }

        public async ValueTask<InterceptionResult<int>> SavingChangesAsync(
            DbContextEventData eventData,
            InterceptionResult<int> result,
            CancellationToken cancellationToken = default)
        {
            var changedEntities = eventData.Context.ChangeTracker.Entries();
            foreach (var entityEntry in changedEntities.Where(e => e.Entity is IOriginatorInfo))
            {
                if (entityEntry.Entity is IOriginatorInfo entity)
                {
                    var connectionString = eventData.Context.Database.GetConnectionString();
                    SqlConnectionStringBuilder sqlConnectionStringBuilder = new(connectionString);
                    var applicationName = sqlConnectionStringBuilder.ApplicationName ?? "Unknown App";
                    var userName = _userProviderService.GetCurrentUserId() ?? "Unauthenticated User";

                    if (entityEntry.State == EntityState.Added)
                    {
                        entity.SourceApplication = applicationName;
                        entity.RowCreationDateTime = DateTimeOffset.UtcNow;
                        entity.RowCreationUser = userName;
                        entity.OriginatorIpaddress = string.Join(",", await IpAddressProvider.GetCurrentHostIPv4AddressesAsync());
                    }
                }
            }

            return result;
        }

        public InterceptionResult<int> SavingChanges(
            DbContextEventData eventData,
            InterceptionResult<int> result)
        {
            return result;
        }
    }
}

Explanation of SaveChangesInterceptor

  1. Constructor: The interceptor class constructor takes an IUserProviderService object, which is used to obtain the current user information.

    public SaveChangesInterceptor(IUserProviderService userProviderService)
    {
        _userProviderService = userProviderService;
    }
  2. SavingChangesAsync Method: This method is called asynchronously when changes are being saved to the database. It processes each entity that implements the IOriginatorInfo interface and sets its metadata if it is in the Added state.

    public async ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        var changedEntities = eventData.Context.ChangeTracker.Entries();
        foreach (var entityEntry in changedEntities.Where(e => e.Entity is IOriginatorInfo))
        {
            if (entityEntry.Entity is IOriginatorInfo entity)
            {
                var connectionString = eventData.Context.Database.GetConnectionString();
                SqlConnectionStringBuilder sqlConnectionStringBuilder = new(connectionString);
                var applicationName = sqlConnectionStringBuilder.ApplicationName ?? "Unknown App";
                var userName = _userProviderService.GetCurrentUserId() ?? "Unauthenticated User";
    
                if (entityEntry.State == EntityState.Added)
                {
                    entity.SourceApplication = applicationName;
                    entity.RowCreationDateTime = DateTimeOffset.UtcNow;
                    entity.RowCreationUser = userName;
                    entity.OriginatorIpaddress = string.Join(",", await IpAddressProvider.GetCurrentHostIPv4AddressesAsync());
                }
            }
        }
    
        return result;
    }
  3. SavingChanges Method: This is a synchronous version of the SavingChangesAsync method. It currently doesn't perform any actions and simply returns the result.

    public InterceptionResult<int> SavingChanges(
        DbContextEventData eventData,
        InterceptionResult<int> result)
    {
        return result;
    }

By following these steps, you have created a SaveChangesInterceptor class that intercepts the process of saving changes to the database and sets metadata on entities implementing the IOriginatorInfo interface. This metadata can be useful for auditing and tracking purposes.

Clone this wiki locally