Entity Framework Core Mastery

Interceptors (Logging & Auditing Data Changes)

1 Views Updated 5/4/2026

Interceptors (Logging & Auditing Data Changes)

In enterprise applications, every time a record is saved or a SQL query fires, you often need to execute global logic. For example: Setting "LastModified" dates, logging slow SQL queries, or writing Audit Trails for compliance frameworks. EF Core Interceptors allow you to hijack the DbContext pipeline right before, or right after, a database operation occurs.

1. SavingChanges Interceptor (Auditing CUD)

The most common interceptor hooks into the SaveChanges pipeline. We can analyze the Change Tracker and manipulate objects right before they are translated into SQL.

public class AuditingInterceptor : SaveChangesInterceptor
{
    // This executes just milliseconds before the SQL INSERT/UPDATE fires
    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData, 
        InterceptionResult<int> result, 
        CancellationToken cancellationToken)
    {
        var context = eventData.Context;
        
        // Scan the ChangeTracker for any object implementing 'IAuditable'
        foreach (var entry in context.ChangeTracker.Entries<IAuditable>())
        {
            if (entry.State == EntityState.Added)
            {
                entry.Entity.CreatedDate = DateTime.UtcNow;
                entry.Entity.CreatedBy = "SystemUser";
            }
            if (entry.State == EntityState.Modified)
            {
                entry.Entity.ModifiedDate = DateTime.UtcNow;
                entry.Entity.ModifiedBy = "SystemUser";
            }
        }

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

2. Command Interceptor (Logging Slow Queries)

What if you want to inspect every single raw SQL query that fires out of your API, and log an alert in Application Insights if a query takes longer than 2 seconds?

public class SlowQueryInterceptor : DbCommandInterceptor
{
    // Executes EXACTLY when the SQL query finishes returning from SQL Server
    public override async ValueTask<DbDataReader> ReaderExecutedAsync(
        DbCommand command, 
        CommandExecutedEventData eventData, 
        DbDataReader result, 
        CancellationToken cancellationToken)
    {
        if (eventData.Duration > TimeSpan.FromSeconds(2))
        {
            // Log the Warning! We can print out the exact SQL Command that was slow.
            Console.WriteLine($"SLOW QUERY ALERT: {eventData.Duration.TotalMilliseconds}ms\n{command.CommandText}");
        }

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

3. Registering the Interceptors

Interceptors must be formally attached to the DbContext during Dependency Injection registration.

builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
    options.UseSqlServer("...")
           .AddInterceptors(new AuditingInterceptor(), new SlowQueryInterceptor());
});

4. Interview Mastery

Q: "We wrote a `SaveChangesInterceptor` to write a historical log entry into an `AuditLogs` table every time a User is updated. However, the `ApplicationDbContext` cannot be injected into the Interceptor via Dependency Injection, resulting in a Null Reference Exception. How do we cleanly save new Audit Logs?"

Architect Answer: "You absolutely shouldn't manually inject the `ApplicationDbContext` into your Interceptor class constructor because it creates a circular dependency; the DbContext configures the Interceptor, and the Interceptor configures the DbContext. Instead, the `DbContextEventData eventData` parameter passed natively into the Interceptor execution method physically contains the runtime instance of the DbContext! You simply extract it via `var context = eventData.Context;`, instantiate a new `AuditLog` object, and push it directly into `context.Add(newLog);`. The framework natively folds this new injection directly into the exact same SQL Transaction that is currently saving the user."

Entity Framework Core Mastery
1. Foundations & Architecture
Introduction to Object Relational Mapping (ORM) Entity Framework Core Architecture & Providers Setup and DbContext Integration Code-First vs Database-First Approaches Reverse Engineering Existing Databases (Scaffolding)
2. Code-First Modeling
Entity Conventions & Data Annotations The Fluent API Deep Dive (OnModelCreating) Primary Keys, Composite Keys, & Guids Required Properties & Database Defaults Value Conversions (Enums & Strongly Typed IDs)
3. Relational Architecture
One-to-Many Relationships & Foreign Keys One-to-One Relationships (Dependent Entities) Many-to-Many Relationships & Navigation Properties Owned Entity Types (Value Objects) Table-per-Hierarchy (TPH) Inheritance
4. Data Querying & LINQ
Basic LINQ Queries & IQueryable Execution Tracking vs No-Tracking Queries (Performance) Eager Loading vs Explicit Loading (Include) Lazy Loading Pitfalls & Proxies Client vs Server Evaluation Parsing
5. Manipulating Data (CUD)
Adding, Updating, and Removing Entities The ChangeTracker and Entity States Disconnected Entities in Web APIs Batch Updates and Deletes (.NET 7+)
6. Advanced Performance & Scale
Concurrency Tokens and Optimistic Locking Raw SQL Queries and Views (FromSqlRaw) Compiled Queries for High Throughput Interceptors (Logging & Auditing Data Changes) DbContext Pooling Mechanisms Managing Complex EF Core Migrations (CI/CD)