Entity Framework Core (EF Core) is a powerful and flexible Object-Relational Mapper (ORM) for .NET developers. Among its features, interceptors provides a mechanism to tap into the EF Core pipeline, allowing developers to inspect, modify, or augment EF Core operations. This guide explains interceptors, how they work, and their practical applications with examples. We’ll also explore the benefits and pitfalls of using interceptors.
What Are Interceptors in EF Core?
Interceptors in EF Core are hooks into the internal processes of EF Core. They allow developers to execute custom logic during various EF Core operations such as database commands, queries, or save operations.
Interceptors work similarly to middleware. They intercept operations, giving you a chance to:
- Log data
- Validate operations
- Modify behavior
Unlike EF Core events (e.g., SaveChanges or Querying), interceptors are more granular and designed for low-level access to EF Core operations.
Key Types of Interceptors
EF Core provides severals built-in interfaces to define interceptors:
- IDbCommandInterceptor: For database command execution.
- ISaveChangesInterceptor: For intercepting SaveChanges operations.
- IQueryExpressionInterceptor: For intercepting LINQ query expressions.
- IConnectionInterceptor: For database connection operations.
- ITransactionInterceptor: For intercepting transaction-related operations.
Setting Up a Sample .NET Web API with EF Core
Let’s start by creating a clean .NET Web API project using EF Core and then integrate interceptors.
Step 1: Create a New Web API Project
Run the following commands to create a new .NET Web API project
mkdir EfCoreInterceptors
cd EfCoreInterceptors
dotnet new webapi
Step 2: Add EF Core and Database Provider
Install EF Core and a database provider, such as SQL Server:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
Step 3: Create the DbContext and Entity Classes
Define a Product entity and AppDbContext:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public bool IsDeleted { get; set; } // For soft delete functionality
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
}
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>().HasQueryFilter(p => !p.IsDeleted); // Soft delete query filter
}
}
Step 4: Configure the Database Connection
Update Program.cs to configure the database connection:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseAuthorization();
app.MapControllers();
app.Run();
Add a connection string to appsettings.json:
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=EfCoreDemo;Trusted_Connection=True;"
}
Adding Interceptors
Example 1: Logging SQL Queries with IDbCommandInterceptor
Let’s add a simple interceptor to log all SQL commands executed by EF Core.
using Microsoft.EntityFrameworkCore.Diagnostics;
using System.Data.Common;
public class SqlLoggingInterceptor : IDbCommandInterceptor
{
public override DbCommand CommandCreated(CommandEndEventData eventData, DbCommand result)
{
Console.WriteLine($"SQL Command Created: {result.CommandText}");
return result;
}
public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
Console.WriteLine($"Executing SQL Command: {command.CommandText}");
return result;
}
}
Registering the Interceptor
Register the interceptor in AppDbContext:
public class AppDbContext : DbContext
{
private readonly IDbCommandInterceptor _sqlLoggingInterceptor;
public AppDbContext(DbContextOptions<AppDbContext> options, IDbCommandInterceptor sqlLoggingInterceptor)
: base(options)
{
_sqlLoggingInterceptor = sqlLoggingInterceptor;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.AddInterceptors(_sqlLoggingInterceptor);
}
public DbSet<Product> Products { get; set; }
}
Update Program.cs to inject the interceptor:
builder.Services.AddSingleton<IDbCommandInterceptor, SqlLoggingInterceptor>();
Example 2: Detecting Slow Queries
This interceptor logs queries that take longer than a specified threshold and triggers an alert (e.g., sends an email or logs to an external monitoring service) for production monitoring.
using System.Diagnostics;
public class SlowQueryInterceptor : IDbCommandInterceptor
{
private readonly TimeSpan _threshold = TimeSpan.FromMilliseconds(500);
private readonly IMonitoringService _monitoringService;
public SlowQueryInterceptor(IMonitoringService monitoringService)
{
_monitoringService = monitoringService;
}
public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
var stopwatch = Stopwatch.StartNew();
try
{
return base.ReaderExecuting(command, eventData, result);
}
finally
{
stopwatch.Stop();
if (stopwatch.Elapsed > _threshold)
{
var message = $"Slow Query Detected: {command.CommandText} took {stopwatch.ElapsedMilliseconds} ms";
Console.WriteLine(message);
// Trigger alert to monitoring service
_monitoringService.LogSlowQuery(message);
}
}
}
}
Implementation Notes
IMonitoringService: Define a monitoring service interface to abstract the alerting mechanism. For example:
public interface IMonitoringService
{
void LogSlowQuery(string message);
}
Concrete Monitoring Service: Implement the interface using tools like email services or external monitoring platforms (e.g., Application Insights, or custom email notifications).
public class EmailMonitoringService : IMonitoringService
{
public void LogSlowQuery(string message)
{
// Implement email or external logging here
Console.WriteLine($"Alert sent: {message}");
}
}
Register Services: Register both the interceptor and the monitoring service in Program.cs:
builder.Services.AddSingleton<IMonitoringService, EmailMonitoringService>();
builder.Services.AddSingleton<IDbCommandInterceptor, SlowQueryInterceptor>();
This interceptor logs queries that take longer than a specified threshold.
public class SlowQueryInterceptor : IDbCommandInterceptor
{
private readonly TimeSpan _threshold = TimeSpan.FromMilliseconds(500);
public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
var stopwatch = Stopwatch.StartNew();
var readerResult = base.ReaderExecuting(command, eventData, result);
stopwatch.Stop();
if (stopwatch.Elapsed > _threshold)
{
Console.WriteLine($"Slow Query Detected: {command.CommandText} took {stopwatch.ElapsedMilliseconds} ms");
}
return readerResult;
}
}
Example 3: Implementing Soft Delete with ISaveChangesInterceptor
Soft delete logic can be particularly useful in multi-tenant systems or in cases where complex relationships require handling cascading deletes safely. This example shows how to intercept _SaveChanges _to implement soft delete functionality while considering these scenarios.
public class SoftDeleteInterceptor : ISaveChangesInterceptor
{
public override int SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
var context = eventData.Context;
if (context == null) return base.SavingChanges(eventData, result);
foreach (var entry in context.ChangeTracker.Entries())
{
// Handle entities with IsDeleted property
if (entry.Entity is ISoftDeletable softDeletableEntity && entry.State == EntityState.Deleted)
{
entry.State = EntityState.Modified;
softDeletableEntity.IsDeleted = true;
// Update related entities if required for complex relationships
if (entry.Entity is ITenantEntity tenantEntity)
{
HandleTenantSpecificLogic(context, tenantEntity);
}
}
}
return base.SavingChanges(eventData, result);
}
private void HandleTenantSpecificLogic(DbContext context, ITenantEntity tenantEntity)
{
// Example: Ensure related tenant-specific records are soft-deleted or flagged appropriately
var relatedEntities = context.ChangeTracker.Entries()
.Where(e => e.Entity is IRelatedEntity && ((IRelatedEntity)e.Entity).TenantId == tenantEntity.TenantId);
foreach (var relatedEntity in relatedEntities)
{
if (relatedEntity.State == EntityState.Deleted)
{
relatedEntity.State = EntityState.Modified;
((IRelatedEntity)relatedEntity.Entity).IsDeleted = true;
}
}
}
}
Considerations for Multi-Tenant Systems:
Tenant Isolation: Ensure that soft delete operations respect tenant boundaries to avoid accidental cross-tenant modifications.
** Query Filters:** Use global query filters to exclude soft-deleted entities based on the tenant context.
modelBuilder.Entity<Product>().HasQueryFilter(p => !p.IsDeleted && p.TenantId == currentTenantId);
Register the Interceptor
Register the interceptor in Program.cs as before:
builder.Services.AddSingleton<ISaveChangesInterceptor, SoftDeleteInterceptor>();
Example 4: Auditing Changes with ISaveChangesInterceptor
Track who created or updated entitiess.
public class AuditInterceptor : ISaveChangesInterceptor
{
public override int SavingChanges(DbContextEventData eventData, InterceptionResult<int> result)
{
var context = eventData.Context;
if (context == null) return base.SavingChanges(eventData, result);
foreach (var entry in context.ChangeTracker.Entries<Product>())
{
if (entry.State == EntityState.Added)
{
entry.CurrentValues["CreatedAt"] = DateTime.UtcNow;
}
if (entry.State == EntityState.Modified)
{
entry.CurrentValues["UpdatedAt"] = DateTime.UtcNow;
}
}
return base.SavingChanges(eventData, result);
}
}
Example 5: Data Masking for Sensitive Data
Mask sensitive data when it’s queried from the database.
public class DataMaskingInterceptor : IDbCommandInterceptor
{
public override InterceptionResult<DbDataReader> ReaderExecuted(DbCommand command, CommandExecutedEventData eventData, DbDataReader result)
{
// Example: Masking sensitive fields like SSN or Email
var maskedReader = new MaskedDbDataReader(result);
return InterceptionResult<DbDataReader>.Success(maskedReader);
}
}
public class MaskedDbDataReader : DbDataReader
{
private readonly DbDataReader _innerReader;
public MaskedDbDataReader(DbDataReader innerReader)
{
_innerReader = innerReader;
}
public override string GetString(int ordinal)
{
var columnName = _innerReader.GetName(ordinal);
var value = _innerReader.GetString(ordinal);
if (columnName == "SSN")
{
return "***-**-****"; // Mask SSN
}
if (columnName == "Email")
{
return "****@example.com"; // Mask email
}
return value;
}
// Delegate all other methods to _innerReader
public override bool Read() => _innerReader.Read();
// Implement other necessary methods...
}
Register the interceptor as before.
Example 6: Retry Logic for Transient Failures
Handle transient failures (e.g., database connection drops) with retry logic.
public class RetryInterceptor : IDbCommandInterceptor
{
private const int MaxRetryCount = 3;
public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
{
for (int retry = 0; retry < MaxRetryCount; retry++)
{
try
{
return base.ReaderExecuting(command, eventData, result);
}
catch (DbException ex)
{
if (retry == MaxRetryCount - 1) throw;
Console.WriteLine($"Retrying due to transient failure: {ex.Message}");
}
}
return result;
}
}
Advantages of Interceptors
- Centralized Logic: Write logic once and apply it across the DbContext.
- Enhanced Debugging: Log queries, commands, and EF Core operations for better diagnostics.
- Custom Behavior: Modify or validate EF Core operations without polluting business logic.
- Security: Enforce rules such as preventing sensitive data deletion or unauthorized queries.
Potential Pitfalls
- Complexity: Interceptors introduce additional complexity and may confuse developers unfamiliar with the EF Core pipeline.
- Performance Overhead: Excessive processing within interceptors can degrade application performance.
- Maintenance Challenges: Changes to EF Core or application requirements may necessitate updates to interceptors.
- Limited Scope: Some interceptors, like IDbCommandInterceptor, provide access only at a low level, requiring knowledge of database internals.
Best Practices
- Use Interceptors Sparingly: Only use them for cross-cutting concerns or operations that cannot be handled elsewhere.
- Test Extensively: Ensure interceptors do not introduce unintended side effect.
- Log Strategically: Avoid over-logging to maintain performances.
- Keep Code Readable: Use clear and concise logic in interceptor.
Thank you .
Top comments (0)