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 Name | Created By | Created Date | Modified By | Modified Date |
|---|---|---|---|---|
| Laptop | Admin | 01-Jun-2026 | Manager | 02-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";
}
}
}
}

Join the conversation! Your thoughts help the community grow.