EF Core Interceptors and Events: Deep Dive for Production Apps
Master EF Core interceptors and events to audit changes, handle concurrency, and optimize queries.
20+ years shipping production .NET services in enterprise systems. Lessons pulled from things that broke in production.
- ✓Basic knowledge of C# and .NET
- ✓Familiarity with Entity Framework Core (DbContext, migrations)
- ✓Understanding of dependency injection in ASP.NET Core
- Interceptors allow you to hook into EF Core operations (SaveChanges, query execution) to add cross-cutting concerns like auditing, logging, or caching.
- Events like ChangeTracker.Tracked and StateChanged let you react to entity state changes in real-time.
- Use interceptors for global behavior; use events for per-context reactions.
- Always dispose of resources properly and avoid blocking async operations inside interceptors.
- Production debugging: check interceptor registration order and exception handling.
Think of EF Core interceptors like security cameras in a store. Every time a customer (query) enters or a purchase (SaveChanges) happens, the camera records it. You can review footage later for audits. Events are like doorbells that ring when someone enters or leaves, letting you react immediately. Both help you monitor and control what happens in your database interactions.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
In production applications, you often need to add cross-cutting concerns like auditing, logging, caching, or soft-delete logic to your database operations. Doing this manually in every repository or service leads to code duplication and maintenance nightmares. EF Core provides two powerful mechanisms: interceptors and events. Interceptors allow you to intercept low-level operations (like command execution, SaveChanges, or connection opening) to add custom behavior. Events like ChangeTracker.Tracked and StateChanged let you react to entity state changes as they happen. This tutorial dives deep into both, with production-ready C# examples, a real-world incident story, and debugging strategies. You'll learn how to implement an audit trail interceptor, handle concurrency conflicts, and avoid common pitfalls. By the end, you'll be able to add robust, maintainable cross-cutting logic to any EF Core application.
Understanding EF Core Interceptors
EF Core interceptors are classes that implement one or more interfaces from the Microsoft.EntityFrameworkCore.Diagnostics namespace. They allow you to intercept database operations such as command execution, SaveChanges, connection opening, and more. Interceptors are registered per DbContext instance via the options builder. There are three main categories: SaveChanges interceptors (ISaveChangesInterceptor), command interceptors (ICommandInterceptor), and connection interceptors (IConnectionInterceptor). Each provides before/after events (synchronous and asynchronous). For example, ISaveChangesInterceptor has SavingChangesAsync and SavedChangesAsync methods. You can use them to log changes, modify entities, or abort the operation. Interceptors are powerful but must be used carefully to avoid side effects like infinite recursion or performance bottlenecks.
Registering Interceptors with Dependency Injection
Interceptors are registered via the DbContext options builder. In ASP.NET Core, you typically configure this in the ConfigureServices method (or Program.cs). You can add interceptors using AddInterceptors() on the options builder. Interceptors can be registered as transient, scoped, or singleton. However, because interceptors are often stateless or have singleton dependencies, it's recommended to register them as singletons to avoid creating multiple instances per DbContext. If your interceptor has scoped dependencies (e.g., a scoped audit logger), you need to use a scoped interceptor or resolve the dependency from the DbContext's service provider. The example below shows registration with a singleton interceptor that uses a scoped service via the context's service provider.
Using ChangeTracker Events
EF Core's ChangeTracker exposes events that fire when entities are tracked or their state changes. The two main events are: Tracked (fires when an entity is first tracked) and StateChanged (fires when an entity's state changes after being tracked). These events are useful for scenarios like caching, validation, or updating denormalized data. Unlike interceptors, events are per-context and are not global. You subscribe to them on the ChangeTracker instance of your DbContext. The example below shows how to use StateChanged to automatically update a LastModified property on entities.
Command Interceptors for Query Caching
Command interceptors allow you to intercept SQL command execution. You can use them to cache query results, log slow queries, or modify commands. The ICommandInterceptor interface provides methods like CommandExecuting and CommandExecuted. For caching, you can check if a query result is already cached before executing the command. The example below shows a simple query cache interceptor that caches results based on the command text. Note: This is a simplified example; production caching should consider parameters, normalization, and expiration.
Soft Delete with Interceptors
Soft delete is a common pattern where records are marked as deleted instead of physically removed. With an interceptor, you can automatically convert delete operations to updates on a 'IsDeleted' column. This ensures that all deletes in your application go through the soft delete logic without requiring changes to business code. The interceptor overrides the SavingChanges method to change the state of entities that implement an ISoftDelete interface from Deleted to Modified, and sets the IsDeleted flag.
Concurrency Conflict Handling with Interceptors
EF Core supports optimistic concurrency using a concurrency token (e.g., a row version column). When a DbUpdateConcurrencyException occurs, you can use an interceptor to handle it gracefully, e.g., by retrying or logging. The IConcurrencyInterceptor interface (available in EF Core 6+) provides methods for handling concurrency conflicts. Alternatively, you can catch the exception in the interceptor's SaveChangesFailed method. The example below shows a simple retry interceptor that retries the operation up to three times with exponential backoff.
The Phantom Audit Logs: How an Interceptor Caused a Production Outage
- Always register interceptors as singletons unless they have scoped dependencies.
- Avoid calling SaveChanges inside SaveChanges interceptors; use a separate context or queue.
- Use a re-entrancy guard (e.g., AsyncLocal flag) to prevent infinite loops.
- Test interceptors with integration tests that simulate real-world load.
- Monitor interceptor performance in production with telemetry.
Check if _isSaving is true before proceeding.Log entry and exit of interceptor methods.| File | Command / Code | Purpose |
|---|---|---|
| AuditInterceptor.cs | using Microsoft.EntityFrameworkCore.Diagnostics; | Understanding EF Core Interceptors |
| Program.cs | using Microsoft.EntityFrameworkCore; | Registering Interceptors with Dependency Injection |
| AppDbContext.cs | using Microsoft.EntityFrameworkCore; | Using ChangeTracker Events |
| QueryCacheInterceptor.cs | using Microsoft.EntityFrameworkCore.Diagnostics; | Command Interceptors for Query Caching |
| SoftDeleteInterceptor.cs | using Microsoft.EntityFrameworkCore; | Soft Delete with Interceptors |
| ConcurrencyRetryInterceptor.cs | using Microsoft.EntityFrameworkCore.Diagnostics; | Concurrency Conflict Handling with Interceptors |
Key takeaways
Common mistakes to avoid
3 patternsCalling SaveChanges inside a SaveChanges interceptor
Registering interceptors as transient or scoped without considering lifetime
Using synchronous methods in async interceptors (e.g., .Result)
Interview Questions on This Topic
Explain how you would implement an audit trail using EF Core interceptors.
Frequently Asked Questions
20+ years shipping production .NET services in enterprise systems. Lessons pulled from things that broke in production.
That's ASP.NET. Mark it forged?
3 min read · try the examples if you haven't