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:
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:
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.
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.