Entity Framework  

EF Core Auditing Using Interceptors: Step-by-Step Tutorial

Introduction

In modern applications, tracking data changes is an essential requirement. Businesses often need to know who created a record, when it was modified, and what changes were made over time. This process is known as auditing.

Imagine an e-commerce application where product prices suddenly change. Without auditing, it may be difficult to determine who updated the price and when the change occurred. Similarly, in banking, healthcare, and enterprise applications, maintaining an audit trail is critical for compliance, troubleshooting, and security.

In earlier versions of Entity Framework, developers commonly implemented auditing logic directly inside repositories or overridden SaveChanges() methods. While these approaches work, they can lead to duplicated code and maintenance challenges.

Entity Framework Core introduced Interceptors, which provide a cleaner and more centralized way to handle auditing. Interceptors allow developers to intercept database operations and automatically apply auditing rules without cluttering business logic.

In this article, you'll learn how to implement auditing in EF Core using Interceptors with practical examples and real-world scenarios.

What Is Auditing?

Auditing is the process of recording information about changes made to application data.

Common auditing fields include:

  • CreatedBy

  • CreatedDate

  • ModifiedBy

  • ModifiedDate

For example:

Product NameCreated ByCreated DateModified ByModified Date
LaptopAdmin01-Jun-2026Manager02-Jun-2026

This information helps organizations:

  • Track user activities

  • Investigate issues

  • Meet compliance requirements

  • Improve accountability

  • Maintain data integrity

What Are EF Core Interceptors?

Interceptors are components that allow developers to intercept and customize Entity Framework Core operations.

They act as middleware for EF Core.

Interceptors can monitor or modify:

  • Database commands

  • Queries

  • Save operations

  • Transactions

  • Connections

For auditing, we typically use SaveChangesInterceptor.

The execution flow looks like this:

Application
      ↓
DbContext.SaveChanges()
      ↓
SaveChangesInterceptor
      ↓
Audit Fields Updated
      ↓
Database

This allows auditing logic to remain separate from business logic.

Why Use Interceptors for Auditing?

Let's consider a traditional approach.

public async Task AddProduct(Product product)
{
    product.CreatedDate = DateTime.UtcNow;
    product.CreatedBy = "Admin";

    context.Products.Add(product);

    await context.SaveChangesAsync();
}

Now imagine hundreds of repositories performing similar operations.

Problems include:

  • Repeated code

  • Difficult maintenance

  • Inconsistent implementation

  • Higher risk of mistakes

Interceptors solve these issues by centralizing auditing logic in a single place.

Creating an Auditable Base Entity

A common approach is to create a base class containing audit properties.

public abstract class AuditableEntity
{
    public DateTime CreatedDate { get; set; }

    public string CreatedBy { get; set; }

    public DateTime? ModifiedDate { get; set; }

    public string? ModifiedBy { get; set; }
}

Now all entities can inherit from this base class.

Example:

public class Product : AuditableEntity
{
    public int Id { get; set; }

    public string Name { get; set; }

    public decimal Price { get; set; }
}

This ensures every entity supports auditing.

Creating the Audit Interceptor

Create a new interceptor class.

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;

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

        return base.SavingChanges(eventData, result);
    }

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

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

    private void UpdateAuditFields(DbContext? context)
    {
        if (context == null)
            return;

        var entries = context.ChangeTracker
            .Entries<AuditableEntity>();

        foreach (var entry in entries)
        {
            if (entry.State == EntityState.Added)
            {
                entry.Entity.CreatedDate = DateTime.UtcNow;
                entry.Entity.CreatedBy = "System";
            }

            if (entry.State == EntityState.Modified)
            {
                entry.Entity.ModifiedDate = DateTime.UtcNow;
                entry.Entity.ModifiedBy = "System";
            }
        }
    }
}

This interceptor automatically updates audit fields whenever data is inserted or modified.

Understanding the Interceptor Logic

The interceptor examines EF Core's Change Tracker.

var entries = context.ChangeTracker
                     .Entries<AuditableEntity>();

The Change Tracker contains information about entities that have changed.

Possible states include:

  • Added

  • Modified

  • Deleted

  • Unchanged

The interceptor uses these states to determine which audit values should be updated.

Registering the Interceptor

Register the interceptor when configuring DbContext.

builder.Services.AddSingleton<AuditInterceptor>();

builder.Services.AddDbContext<ApplicationDbContext>(
    (serviceProvider, options) =>
    {
        options.UseSqlServer(
            builder.Configuration.GetConnectionString("DefaultConnection"));

        options.AddInterceptors(
            serviceProvider.GetRequiredService<AuditInterceptor>());
    });

Once registered, the interceptor runs automatically.

No additional code is required inside repositories or services.

Testing the Auditing Process

Create a product.

var product = new Product
{
    Name = "Laptop",
    Price = 50000
};

context.Products.Add(product);

await context.SaveChangesAsync();

Generated values:

CreatedDate = 2026-06-03 10:00 AM
CreatedBy = System

Now update the product.

product.Price = 55000;

await context.SaveChangesAsync();

Generated values:

ModifiedDate = 2026-06-03 11:00 AM
ModifiedBy = System

Everything happens automatically.

Capturing the Logged-In User

In real applications, "System" is not enough.

Businesses usually want the actual user name.

Create a service:

public interface ICurrentUserService
{
    string UserName { get; }
}

Implementation:

public class CurrentUserService : ICurrentUserService
{
    private readonly IHttpContextAccessor _httpContextAccessor;

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

    public string UserName =>
        _httpContextAccessor.HttpContext?.User?.Identity?.Name
        ?? "Anonymous";
}

Inject this service into the interceptor.

private readonly ICurrentUserService _currentUserService;

Now audit records store the actual logged-in user.

Example:

CreatedBy = JohnSmith
ModifiedBy = SarahJones

This provides much more useful auditing information.

Auditing Delete Operations

Many organizations need to track deleted records.

You can detect deleted entities.

if (entry.State == EntityState.Deleted)
{
    Console.WriteLine(
        $"Entity deleted at {DateTime.UtcNow}");
}

Alternatively, implement soft deletes.

Implementing Soft Delete

Instead of physically removing data, mark it as deleted.

Add properties:

public bool IsDeleted { get; set; }

public DateTime? DeletedDate { get; set; }

Inside the interceptor:

if (entry.State == EntityState.Deleted)
{
    entry.State = EntityState.Modified;

    entry.Entity.IsDeleted = true;

    entry.Entity.DeletedDate = DateTime.UtcNow;
}

Benefits include:

  • Data recovery

  • Better auditing

  • Historical tracking

Many enterprise systems use this approach.

Real-World Example

Consider an online banking system.

Whenever account information changes:

  • CreatedDate records account creation.

  • CreatedBy records the employee who created it.

  • ModifiedDate tracks updates.

  • ModifiedBy records the person making changes.

Without auditing:

  • Troubleshooting becomes difficult.

  • Compliance requirements may fail.

  • Unauthorized changes become harder to detect.

With auditing:

  • Every change is traceable.

  • Regulatory compliance improves.

  • Investigations become easier.

Advantages of Using Interceptors for Auditing

Interceptors provide several benefits.

  • Centralized auditing logic

  • Cleaner repositories

  • Less duplicate code

  • Easier maintenance

  • Better scalability

  • Automatic auditing

  • Improved consistency

  • Better separation of concerns

These advantages become more valuable as applications grow.

Potential Limitations

While powerful, interceptors are not perfect.

Consider these limitations:

  • Additional processing overhead

  • Requires understanding of EF Core internals

  • Complex auditing may require extra customization

  • Improper implementation can affect performance

Fortunately, the overhead is usually minimal.

Best Practices

When implementing auditing:

  • Use a shared base entity.

  • Keep interceptor logic simple.

  • Capture actual logged-in users.

  • Use UTC dates.

  • Implement soft deletes when appropriate.

  • Avoid placing business logic inside interceptors.

  • Test auditing thoroughly.

Following these practices ensures a reliable auditing solution.

Before and After Scenario

Before Using Interceptors

Developers manually update audit fields.

product.ModifiedDate = DateTime.UtcNow;
product.ModifiedBy = userName;

This code appears everywhere.

Problems:

  • Repetition

  • Missed updates

  • Inconsistent behavior

After Using Interceptors

await context.SaveChangesAsync();

Audit information is automatically managed.

Benefits:

  • Cleaner code

  • Better consistency

  • Easier maintenance

Conclusion

Auditing is a critical feature in modern applications, helping organizations track changes, improve accountability, and meet compliance requirements.

While traditional approaches often involve overriding SaveChanges() or manually updating audit fields, EF Core Interceptors provide a cleaner and more maintainable solution. By centralizing auditing logic inside a SaveChangesInterceptor, developers can automatically populate audit fields without cluttering business code.

Whether you're building enterprise applications, e-commerce platforms, healthcare systems, or banking software, implementing auditing with EF Core Interceptors can significantly improve maintainability, consistency, and data traceability.

As applications continue to grow in complexity, EF Core Interceptors have become one of the most effective ways to implement robust and scalable auditing solutions.