Appearance
拦截器(Interceptors)
EF Core 拦截器是一种强大的横切关注点实现机制,允许你在 EF Core 操作执行前后注入自定义逻辑。本章将深入探讨拦截器的各种应用场景,包括审计日志、软删除、性能监控、多租户隔离等。
目录
1. 拦截器基础
1.1 什么是拦截器?
拦截器(Interceptor)是 EF Core 提供的一种扩展机制,允许你在数据库操作执行前后插入自定义逻辑,而无需修改业务代码。
拦截器 vs 其他扩展点:
csharp
// 对比各种扩展方式:
// 1. SaveChanges 重写 - 只能在保存时介入
public override int SaveChanges()
{
// ❌ 只能拦截保存操作
return base.SaveChanges();
}
// 2. 值转换器 - 只能转换特定类型
entity.Property(e => e.Status)
.HasConversion<string>(); // ❌ 只能转换值
// 3. 拦截器 - 可以在任何操作前后介入
public class MyInterceptor : SaveChangesInterceptor
{
public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(...)
{
// ✅ 可以拦截查询、保存、命令等所有操作
return await base.SavingChangesAsync(...);
}
}1.2 拦截器类型
EF Core 支持多种拦截器接口:
csharp
// 1. 保存变更拦截器
public interface ISaveChangesInterceptor : IInterceptor
{
ValueTask<InterceptionResult<int>> SavingChangesAsync(...);
ValueTask<int> SavedChangesAsync(...);
}
// 2. 命令拦截器
public interface IDbCommandInterceptor : IInterceptor
{
ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(...);
ValueTask<DbDataReader> ReaderExecutedAsync(...);
ValueTask<InterceptionResult<object>> ScalarExecutingAsync(...);
ValueTask<object> ScalarExecutedAsync(...);
ValueTask<InterceptionResult<int>> NonQueryExecutingAsync(...);
ValueTask<int> NonQueryExecutedAsync(...);
}
// 3. 连接拦截器
public interface IDbConnectionInterceptor : IInterceptor
{
Task ConnectionOpeningAsync(...);
Task ConnectionOpenedAsync(...);
Task ConnectionClosingAsync(...);
Task ConnectionClosedAsync(...);
}
// 4. 事务拦截器
public interface IDbTransactionInterceptor : IInterceptor
{
ValueTask<InterceptionResult<DbTransaction>> TransactionStartingAsync(...);
ValueTask<DbTransaction> TransactionStartedAsync(...);
ValueTask<InterceptionResult> TransactionCommittingAsync(...);
ValueTask TransactionCommittedAsync(...);
ValueTask<InterceptionResult> TransactionRollingBackAsync(...);
ValueTask TransactionRolledBackAsync(...);
}
// 5. 迁移拦截器
public interface IMigrationsInterceptor : IInterceptor
{
ValueTask<InterceptionResult> MigratingAsync(...);
ValueTask MigratedAsync(...);
}1.3 注册拦截器
.NET 8/9/10 Minimal API 风格:
csharp
var builder = WebApplication.CreateBuilder(args);
// 方法1: 直接注册
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("Default"));
options.AddInterceptors(new AuditInterceptor());
options.AddInterceptors(new SoftDeleteInterceptor());
});
// 方法2: 从依赖注入容器注册
builder.Services.AddSingleton<AuditInterceptor>();
builder.Services.AddSingleton<SoftDeleteInterceptor>();
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("Default"));
var auditInterceptor = sp.GetRequiredService<AuditInterceptor>();
var softDeleteInterceptor = sp.GetRequiredService<SoftDeleteInterceptor>();
options.AddInterceptors(auditInterceptor, softDeleteInterceptor);
});
// 方法3: 使用配置批量注册
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("Default"));
options.AddInterceptors(sp.GetServices<IInterceptor>());
});1.4 拦截器执行顺序
csharp
// 注册多个拦截器时,按注册顺序执行
options.AddInterceptors(
new LoggingInterceptor(), // 1. 首先执行
new AuditInterceptor(), // 2. 然后执行
new RetryInterceptor() // 3. 最后执行
);
// 对于异步操作,执行链如下:
// Before: Logging -> Audit -> Retry -> [实际DB操作]
// After: Retry -> Audit -> Logging <- 反向执行2. 审计日志拦截器
2.1 完整的审计日志实现
定义审计记录实体:
csharp
public class AuditRecord
{
public long Id { get; set; }
public string EntityName { get; set; }
public string EntityId { get; set; }
public string Action { get; set; } // Insert, Update, Delete
public string OldValues { get; set; } // JSON
public string NewValues { get; set; } // JSON
public string ChangedProperties { get; set; } // JSON
public string UserId { get; set; }
public string IpAddress { get; set; }
public string UserAgent { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
// 配置
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<AuditRecord>(entity =>
{
entity.HasKey(e => e.Id);
entity.Property(e => e.EntityName).IsRequired().HasMaxLength(200);
entity.Property(e => e.Action).IsRequired().HasMaxLength(50);
entity.Property(e => e.UserId).HasMaxLength(100);
// 添加索引以提高查询性能
entity.HasIndex(e => e.EntityName);
entity.HasIndex(e => e.CreatedAt);
entity.HasIndex(e => e.UserId);
});
}实现审计拦截器:
csharp
public class AuditInterceptor : SaveChangesInterceptor
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly List<AuditRecord> _auditRecords = new();
public AuditInterceptor(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
var context = eventData.Context;
if (context == null) return result;
// 遍历所有被跟踪的实体
var entries = context.ChangeTracker.Entries()
.Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted);
foreach (var entry in entries)
{
// 跳过审计记录本身,避免无限循环
if (entry.Entity is AuditRecord) continue;
var auditRecord = CreateAuditRecord(entry);
_auditRecords.Add(auditRecord);
}
return await base.SavingChangesAsync(eventData, result, cancellationToken);
}
public override async ValueTask<int> SavedChangesAsync(
SaveChangesCompletedEventData eventData,
int result,
CancellationToken cancellationToken = default)
{
var context = eventData.Context;
// 在保存成功后,将审计记录添加到数据库
if (context != null && _auditRecords.Any())
{
context.Set<AuditRecord>().AddRange(_auditRecords);
await context.SaveChangesAsync(cancellationToken);
_auditRecords.Clear();
}
return await base.SavedChangesAsync(eventData, result, cancellationToken);
}
private AuditRecord CreateAuditRecord(EntityEntry entry)
{
var audit = new AuditRecord
{
EntityName = entry.Entity.GetType().Name,
EntityId = GetEntityId(entry),
Action = entry.State.ToString(),
UserId = GetCurrentUserId(),
IpAddress = GetClientIpAddress(),
UserAgent = GetUserAgent(),
CreatedAt = DateTime.UtcNow
};
// 记录变更的属性
if (entry.State == EntityState.Modified)
{
var changedProps = entry.Properties
.Where(p => p.IsModified)
.Select(p => new
{
PropertyName = p.Metadata.Name,
OldValue = p.OriginalValue,
NewValue = p.CurrentValue
})
.ToList();
audit.ChangedProperties = JsonSerializer.Serialize(changedProps);
audit.OldValues = JsonSerializer.Serialize(
changedProps.ToDictionary(p => p.PropertyName, p => p.OldValue));
audit.NewValues = JsonSerializer.Serialize(
changedProps.ToDictionary(p => p.PropertyName, p => p.NewValue));
}
else if (entry.State == EntityState.Added)
{
audit.NewValues = JsonSerializer.Serialize(
entry.Properties.ToDictionary(p => p.Metadata.Name, p => p.CurrentValue));
}
else if (entry.State == EntityState.Deleted)
{
audit.OldValues = JsonSerializer.Serialize(
entry.Properties.ToDictionary(p => p.Metadata.Name, p => p.OriginalValue));
}
return audit;
}
private string GetEntityId(EntityEntry entry)
{
// 尝试获取主键值
var keyProperties = entry.Metadata.FindPrimaryKey()?.Properties;
if (keyProperties != null && keyProperties.Any())
{
var keyValues = keyProperties.Select(kp => entry.Property(kp.Name).CurrentValue);
return string.Join(",", keyValues);
}
return "Unknown";
}
private string GetCurrentUserId()
{
var user = _httpContextAccessor.HttpContext?.User;
return user?.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "System";
}
private string GetClientIpAddress()
{
return _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString()
?? "Unknown";
}
private string GetUserAgent()
{
return _httpContextAccessor.HttpContext?.Request.Headers["User-Agent"].FirstOrDefault()
?? "Unknown";
}
}使用审计日志:
csharp
// 查询审计历史
var auditHistory = await context.AuditRecords
.Where(a => a.EntityName == "Product" && a.EntityId == "123")
.OrderByDescending(a => a.CreatedAt)
.ToListAsync();
// 查看某个用户的操作
var userActions = await context.AuditRecords
.Where(a => a.UserId == "user123" && a.CreatedAt > DateTime.UtcNow.AddDays(-7))
.GroupBy(a => a.Action)
.Select(g => new { Action = g.Key, Count = g.Count() })
.ToListAsync();
// 恢复被删除的数据
var deletedRecord = await context.AuditRecords
.Where(a => a.EntityName == "Order" && a.Action == "Delete")
.OrderByDescending(a => a.CreatedAt)
.FirstOrDefaultAsync();
if (deletedRecord != null)
{
var oldData = JsonSerializer.Deserialize<Order>(deletedRecord.OldValues);
context.Orders.Add(oldData);
await context.SaveChangesAsync();
}2.2 轻量级审计 - 仅记录关键信息
如果不需要完整的审计记录,可以使用简化版本:
csharp
public class SimpleAuditInterceptor : SaveChangesInterceptor
{
private readonly ILogger<SimpleAuditInterceptor> _logger;
public SimpleAuditInterceptor(ILogger<SimpleAuditInterceptor> logger)
{
_logger = logger;
}
public override async ValueTask<int> SavedChangesAsync(
SaveChangesCompletedEventData eventData,
int result,
CancellationToken cancellationToken = default)
{
var context = eventData.Context;
if (context != null)
{
var changes = context.ChangeTracker.Entries()
.Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted)
.Select(e => new
{
Entity = e.Entity.GetType().Name,
State = e.State,
Timestamp = DateTime.UtcNow
})
.ToList();
if (changes.Any())
{
_logger.LogInformation("Changes saved: {@Changes}", changes);
}
}
return await base.SavedChangesAsync(eventData, result, cancellationToken);
}
}3. 软删除拦截器
3.1 软删除基础
定义软删除接口和基类:
csharp
// 标记可软删除的实体
public interface ISoftDeletable
{
bool IsDeleted { get; set; }
DateTime? DeletedAt { get; set; }
}
// 基类实现
public abstract class SoftDeletableEntity : ISoftDeletable
{
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
}
// 应用示例
public class Product : SoftDeletableEntity
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class Order : SoftDeletableEntity
{
public int Id { get; set; }
public DateTime OrderDate { get; set; }
}实现软删除拦截器:
csharp
public class SoftDeleteInterceptor : SaveChangesInterceptor
{
public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
var context = eventData.Context;
if (context == null) return result;
// 查找所有被标记为删除的实体
var deletedEntries = context.ChangeTracker.Entries()
.Where(e => e.State == EntityState.Deleted);
foreach (var entry in deletedEntries)
{
// 如果实体实现了 ISoftDeletable,转换为软删除
if (entry.Entity is ISoftDeletable softDeletable)
{
// 改为更新操作
entry.State = EntityState.Unchanged;
// 设置软删除字段
softDeletable.IsDeleted = true;
softDeletable.DeletedAt = DateTime.UtcNow;
// 标记属性为已修改,确保保存到数据库
entry.Property(nameof(ISoftDeletable.IsDeleted)).IsModified = true;
entry.Property(nameof(ISoftDeletable.DeletedAt)).IsModified = true;
}
}
return await base.SavingChangesAsync(eventData, result, cancellationToken);
}
}3.2 全局查询过滤器自动应用
在 DbContext 中配置全局过滤器:
csharp
public class AppDbContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 为所有实现 ISoftDeletable 的实体添加全局过滤器
var softDeletableTypes = modelBuilder.Model.GetEntityTypes()
.Where(t => typeof(ISoftDeletable).IsAssignableFrom(t.ClrType));
foreach (var entityType in softDeletableTypes)
{
// 动态创建过滤器: e => !e.IsDeleted
var parameter = Expression.Parameter(entityType.ClrType, "e");
var property = Expression.Property(parameter, nameof(ISoftDeletable.IsDeleted));
var notExpression = Expression.Not(property);
var lambda = Expression.Lambda(notExpression, parameter);
modelBuilder.Entity(entityType.ClrType).HasQueryFilter(lambda);
}
}
}使用效果:
csharp
// 自动过滤已删除的数据
var products = await context.Products.ToListAsync();
// SQL: SELECT * FROM Products WHERE IsDeleted = 0
// 显式包含已删除的数据
var allProducts = await context.Products
.IgnoreQueryFilters() // 忽略全局过滤器
.ToListAsync();
// SQL: SELECT * FROM Products
// 查询已删除的数据
var deletedProducts = await context.Products
.IgnoreQueryFilters()
.Where(p => p.IsDeleted)
.ToListAsync();3.3 级联软删除
当删除父实体时,自动软删除相关子实体:
csharp
public class CascadeSoftDeleteInterceptor : SaveChangesInterceptor
{
public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
var context = eventData.Context;
if (context == null) return result;
var deletedEntries = context.ChangeTracker.Entries()
.Where(e => e.State == EntityState.Deleted)
.ToList();
foreach (var entry in deletedEntries)
{
if (entry.Entity is ISoftDeletable softDeletable)
{
// 查找相关的子实体
var navigations = entry.Navigations
.Where(n => n.IsCollection && n.TargetEntityType.ClrType.GetInterface(nameof(ISoftDeletable)) != null);
foreach (var navigation in navigations)
{
var relatedEntities = navigation.CurrentValue as IEnumerable<ISoftDeletable>;
if (relatedEntities != null)
{
foreach (var relatedEntity in relatedEntities.Where(e => !e.IsDeleted))
{
// 软删除子实体
relatedEntity.IsDeleted = true;
relatedEntity.DeletedAt = DateTime.UtcNow;
}
}
}
// 转换父实体为软删除
entry.State = EntityState.Unchanged;
softDeletable.IsDeleted = true;
softDeletable.DeletedAt = DateTime.UtcNow;
}
}
return await base.SavingChangesAsync(eventData, result, cancellationToken);
}
}4. 性能监控拦截器
4.1 SQL 执行监控
csharp
public class PerformanceMonitoringInterceptor : DbCommandInterceptor
{
private readonly ILogger<PerformanceMonitoringInterceptor> _logger;
private readonly IMetricsCollector _metrics;
private readonly Stopwatch _stopwatch = new();
public PerformanceMonitoringInterceptor(
ILogger<PerformanceMonitoringInterceptor> logger,
IMetricsCollector metrics)
{
_logger = logger;
_metrics = metrics;
}
public override async ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<DbDataReader> result,
CancellationToken cancellationToken = default)
{
_stopwatch.Restart();
return await base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
}
public override async ValueTask<DbDataReader> ReaderExecutedAsync(
DbCommand command,
CommandExecutedEventData eventData,
DbDataReader result,
CancellationToken cancellationToken = default)
{
_stopwatch.Stop();
var duration = _stopwatch.ElapsedMilliseconds;
var sql = TruncateSql(command.CommandText, 500);
// 记录指标
_metrics.RecordMetric("ef_query_duration_ms", duration);
_metrics.RecordMetric("ef_query_count", 1);
// 慢查询告警
if (duration > 1000) // 超过1秒
{
_logger.LogWarning("Slow query detected ({Duration}ms):\n{SQL}",
duration, sql);
}
// 记录详细日志
_logger.LogDebug("Query executed in {Duration}ms:\n{SQL}\nParameters: {@Parameters}",
duration,
sql,
command.Parameters.Cast<DbParameter>()
.ToDictionary(p => p.ParameterName, p => p.Value));
return await base.ReaderExecutedAsync(command, eventData, result, cancellationToken);
}
public override async ValueTask<InterceptionResult<object>> ScalarExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<object> result,
CancellationToken cancellationToken = default)
{
_stopwatch.Restart();
return await base.ScalarExecutingAsync(command, eventData, result, cancellationToken);
}
public override async ValueTask<object> ScalarExecutedAsync(
DbCommand command,
CommandExecutedEventData eventData,
object result,
CancellationToken cancellationToken = default)
{
_stopwatch.Stop();
var duration = _stopwatch.ElapsedMilliseconds;
_metrics.RecordMetric("ef_scalar_query_duration_ms", duration);
if (duration > 1000)
{
_logger.LogWarning("Slow scalar query ({Duration}ms): {SQL}", duration, command.CommandText);
}
return await base.ScalarExecutedAsync(command, eventData, result, cancellationToken);
}
public override async ValueTask<InterceptionResult<int>> NonQueryExecutingAsync(
DbCommand command,
CommandEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
_stopwatch.Restart();
return await base.NonQueryExecutingAsync(command, eventData, result, cancellationToken);
}
public override async ValueTask<int> NonQueryExecutedAsync(
DbCommand command,
CommandExecutedEventData eventData,
int result,
CancellationToken cancellationToken = default)
{
_stopwatch.Stop();
var duration = _stopwatch.ElapsedMilliseconds;
_metrics.RecordMetric("ef_non_query_duration_ms", duration);
if (duration > 1000)
{
_logger.LogWarning("Slow non-query ({Duration}ms): {SQL}", duration, command.CommandText);
}
return await base.NonQueryExecutedAsync(command, eventData, result, cancellationToken);
}
private string TruncateSql(string sql, int maxLength)
{
if (string.IsNullOrEmpty(sql) || sql.Length <= maxLength)
return sql;
return sql.Substring(0, maxLength) + "...";
}
}4.2 Prometheus 集成
csharp
public class PrometheusMetricsCollector : IMetricsCollector
{
private static readonly Histogram QueryDurationHistogram = Metrics
.CreateHistogram("ef_core_query_duration_seconds", "EF Core query duration",
new HistogramConfiguration
{
LabelNames = new[] { "query_type" },
Buckets = Histogram.ExponentialBuckets(0.01, 2, 10)
});
private static readonly Counter QueryCountCounter = Metrics
.CreateCounter("ef_core_query_total", "Total number of EF Core queries");
public void RecordMetric(string name, double value)
{
switch (name)
{
case "ef_query_duration_ms":
QueryDurationHistogram.WithLabels("query").Observe(value / 1000.0);
break;
case "ef_query_count":
QueryCountCounter.Inc();
break;
}
}
}
// 在 Program.cs 中暴露指标端点
app.MapMetrics(); // Prometheus 指标端点5. 多租户隔离拦截器
5.1 基于租户ID的数据隔离
定义多租户接口:
csharp
public interface IMultiTenant
{
string TenantId { get; set; }
}
public class Product : IMultiTenant
{
public int Id { get; set; }
public string Name { get; set; }
public string TenantId { get; set; } // 租户ID
}实现租户隔离拦截器:
csharp
public class MultiTenantInterceptor : SaveChangesInterceptor
{
private readonly ITenantProvider _tenantProvider;
public MultiTenantInterceptor(ITenantProvider tenantProvider)
{
_tenantProvider = tenantProvider;
}
public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
var context = eventData.Context;
if (context == null) return result;
var currentTenantId = _tenantProvider.GetCurrentTenantId();
if (string.IsNullOrEmpty(currentTenantId))
{
throw new InvalidOperationException("Tenant ID is not set");
}
// 自动设置新实体的租户ID
var addedEntries = context.ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added);
foreach (var entry in addedEntries)
{
if (entry.Entity is IMultiTenant multiTenant)
{
multiTenant.TenantId = currentTenantId;
}
}
return await base.SavingChangesAsync(eventData, result, cancellationToken);
}
}
// 租户提供者
public interface ITenantProvider
{
string GetCurrentTenantId();
}
public class HttpContextTenantProvider : ITenantProvider
{
private readonly IHttpContextAccessor _httpContextAccessor;
public HttpContextTenantProvider(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public string GetCurrentTenantId()
{
// 从请求头获取租户ID
var tenantId = _httpContextAccessor.HttpContext?.Request.Headers["X-Tenant-Id"].FirstOrDefault();
if (string.IsNullOrEmpty(tenantId))
{
// 从子域名提取租户ID
var host = _httpContextAccessor.HttpContext?.Request.Host.Host;
if (!string.IsNullOrEmpty(host))
{
tenantId = host.Split('.')[0]; // tenant1.example.com -> tenant1
}
}
return tenantId ?? "default";
}
}配置全局查询过滤器:
csharp
public class MultiTenantDbContext : DbContext
{
private readonly ITenantProvider _tenantProvider;
public MultiTenantDbContext(DbContextOptions options, ITenantProvider tenantProvider)
: base(options)
{
_tenantProvider = tenantProvider;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 为所有实现 IMultiTenant 的实体添加过滤器
var multiTenantTypes = modelBuilder.Model.GetEntityTypes()
.Where(t => typeof(IMultiTenant).IsAssignableFrom(t.ClrType));
foreach (var entityType in multiTenantTypes)
{
var parameter = Expression.Parameter(entityType.ClrType, "e");
var property = Expression.Property(parameter, nameof(IMultiTenant.TenantId));
var tenantId = Expression.Constant(_tenantProvider.GetCurrentTenantId());
var equalExpression = Expression.Equal(property, tenantId);
var lambda = Expression.Lambda(equalExpression, parameter);
modelBuilder.Entity(entityType.ClrType).HasQueryFilter(lambda);
}
}
}使用效果:
csharp
// 自动过滤当前租户的数据
var products = await context.Products.ToListAsync();
// SQL: SELECT * FROM Products WHERE TenantId = 'tenant1'
// 自动设置新实体的租户ID
context.Products.Add(new Product { Name = "Product A" });
await context.SaveChangesAsync();
// 插入的 Product 自动设置 TenantId = 'tenant1'6. 领域事件发布拦截器
6.1 领域事件模式
定义领域事件:
csharp
// 标记接口
public interface IDomainEvent
{
DateTime OccurredOn { get; }
}
// 订单创建事件
public record OrderCreatedEvent : IDomainEvent
{
public int OrderId { get; }
public string CustomerId { get; }
public decimal TotalAmount { get; }
public DateTime OccurredOn { get; } = DateTime.UtcNow;
public OrderCreatedEvent(int orderId, string customerId, decimal totalAmount)
{
OrderId = orderId;
CustomerId = customerId;
TotalAmount = totalAmount;
}
}
// 库存扣减事件
public record InventoryDeductedEvent : IDomainEvent
{
public int ProductId { get; }
public int Quantity { get; }
public DateTime OccurredOn { get; } = DateTime.UtcNow;
public InventoryDeductedEvent(int productId, int quantity)
{
ProductId = productId;
Quantity = quantity;
}
}
// 实体中收集事件
public abstract class EntityBase
{
private readonly List<IDomainEvent> _domainEvents = new();
public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();
protected void AddDomainEvent(IDomainEvent domainEvent)
{
_domainEvents.Add(domainEvent);
}
public void ClearDomainEvents()
{
_domainEvents.Clear();
}
}
public class Order : EntityBase
{
public int Id { get; set; }
public string CustomerId { get; set; }
public decimal TotalAmount { get; set; }
public void CreateOrder(string customerId, List<OrderItem> items)
{
CustomerId = customerId;
TotalAmount = items.Sum(i => i.Quantity * i.UnitPrice);
// 添加领域事件
AddDomainEvent(new OrderCreatedEvent(Id, customerId, TotalAmount));
}
}实现事件发布拦截器:
csharp
public class DomainEventPublisherInterceptor : SaveChangesInterceptor
{
private readonly IMediator _mediator; // MediatR
public DomainEventPublisherInterceptor(IMediator mediator)
{
_mediator = mediator;
}
public override async ValueTask<int> SavedChangesAsync(
SaveChangesCompletedEventData eventData,
int result,
CancellationToken cancellationToken = default)
{
var context = eventData.Context;
if (context != null)
{
// 获取所有包含领域事件的实体
var entitiesWithEvents = context.ChangeTracker.Entries<EntityBase>()
.Select(e => e.Entity)
.Where(e => e.DomainEvents.Any())
.ToList();
// 发布所有领域事件
foreach (var entity in entitiesWithEvents)
{
var events = entity.DomainEvents.ToList();
entity.ClearDomainEvents(); // 清除事件,避免重复发布
foreach (var domainEvent in events)
{
// 发布到 MediatR
await _mediator.Publish(domainEvent, cancellationToken);
}
}
}
return await base.SavedChangesAsync(eventData, result, cancellationToken);
}
}事件处理器:
csharp
// 订单创建事件处理器
public class OrderCreatedEventHandler : INotificationHandler<OrderCreatedEvent>
{
private readonly IEmailService _emailService;
private readonly ILogger<OrderCreatedEventHandler> _logger;
public OrderCreatedEventHandler(IEmailService emailService, ILogger<OrderCreatedEventHandler> logger)
{
_emailService = emailService;
_logger = logger;
}
public async Task Handle(OrderCreatedEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation("Order created: {OrderId}", notification.OrderId);
// 发送确认邮件
await _emailService.SendOrderConfirmationAsync(notification.CustomerId, notification.OrderId);
// 触发其他业务逻辑
// - 更新客户统计
// - 发送短信通知
// - 推送到数据分析系统
}
}
// 库存扣减事件处理器
public class InventoryDeductedEventHandler : INotificationHandler<InventoryDeductedEvent>
{
private readonly ICacheService _cacheService;
public InventoryDeductedEventHandler(ICacheService cacheService)
{
_cacheService = cacheService;
}
public async Task Handle(InventoryDeductedEvent notification, CancellationToken cancellationToken)
{
// 清除产品缓存
await _cacheService.InvalidateAsync($"product:{notification.ProductId}");
// 更新库存预警
if (notification.Quantity < 10)
{
await SendLowStockAlert(notification.ProductId);
}
}
}7. 连接弹性与重试
7.1 transient 故障处理
csharp
public class ResilientConnectionInterceptor : DbConnectionInterceptor
{
private readonly ILogger<ResilientConnectionInterceptor> _logger;
private readonly IResiliencePipeline _resiliencePipeline;
public ResilientConnectionInterceptor(
ILogger<ResilientConnectionInterceptor> logger,
IResiliencePipeline resiliencePipeline)
{
_logger = logger;
_resiliencePipeline = resiliencePipeline;
}
public override async Task ConnectionOpeningAsync(
DbConnection connection,
ConnectionEventData eventData,
InterceptionResult eventResult,
CancellationToken cancellationToken = default)
{
try
{
// 使用重试策略打开连接
await _resiliencePipeline.ExecuteAsync(async ct =>
{
await connection.OpenAsync(ct);
}, cancellationToken);
_logger.LogInformation("Database connection opened successfully");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to open database connection after retries");
throw;
}
}
public override async Task ConnectionClosingAsync(
DbConnection connection,
ConnectionEventData eventData,
InterceptionResult eventResult,
CancellationToken cancellationToken = default)
{
try
{
await connection.CloseAsync();
_logger.LogDebug("Database connection closed");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error closing database connection");
}
}
}
// 配置 Polly 重试策略
builder.Services.AddResiliencePipeline("ef-core", pipeline =>
{
pipeline.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
BackoffType = DelayBackoffType.Exponential,
UseJitter = true,
OnRetry = args =>
{
_logger.LogWarning("Retry attempt {Attempt} due to {Reason}",
args.AttemptNumber, args.Outcome.Exception);
return default;
}
});
});8. 最佳实践与性能考虑
8.1 拦截器性能优化
✅ 应该做的:
保持拦截器轻量
csharp// ✅ 好: 异步非阻塞 public override async ValueTask<int> SavedChangesAsync(...) { await _eventBus.PublishAsync(events); return await base.SavedChangesAsync(...); } // ❌ 坏: 同步阻塞 public override int SavedChangesAsync(...) { _eventBus.Publish(events).Wait(); // 阻塞线程 return base.SavedChangesAsync(...); }批量处理减少开销
csharp// ✅ 好: 批量写入 context.AuditRecords.AddRange(_auditRecords); await context.SaveChangesAsync(); // ❌ 坏: 逐条写入 foreach (var record in _auditRecords) { context.AuditRecords.Add(record); await context.SaveChangesAsync(); // N次数据库调用 }避免在拦截器中查询数据库
csharp// ❌ 危险: 可能导致递归调用 public override async ValueTask<InterceptionResult<int>> SavingChangesAsync(...) { var existingRecord = await context.AuditRecords.FindAsync(id); // 可能触发新的拦截 return await base.SavingChangesAsync(...); }使用条件编译调试
csharp#if DEBUG _logger.LogDebug("Interceptor executed: {Details}", details); #endif
8.2 拦截器组合与优先级
csharp
// 推荐的服务注册顺序
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
{
options.UseSqlServer(connectionString);
// 1. 连接层
options.AddInterceptors(sp.GetRequiredService<ResilientConnectionInterceptor>());
// 2. 命令层
options.AddInterceptors(sp.GetRequiredService<PerformanceMonitoringInterceptor>());
// 3. 保存层
options.AddInterceptors(
sp.GetRequiredService<SoftDeleteInterceptor>(),
sp.GetRequiredService<AuditInterceptor>(),
sp.GetRequiredService<MultiTenantInterceptor>(),
sp.GetRequiredService<DomainEventPublisherInterceptor>()
);
});8.3 测试拦截器
csharp
public class AuditInterceptorTests
{
[Fact]
public async Task Should_Create_Audit_Record_On_Insert()
{
// Arrange
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase("TestDb")
.AddInterceptors(new AuditInterceptor(mockHttpContext.Object))
.Options;
using var context = new AppDbContext(options);
// Act
context.Products.Add(new Product { Name = "Test" });
await context.SaveChangesAsync();
// Assert
var audit = await context.AuditRecords.FirstOrDefaultAsync();
Assert.NotNull(audit);
Assert.Equal("Product", audit.EntityName);
Assert.Equal("Added", audit.Action);
}
}总结
拦截器是 EF Core 最强大的扩展机制之一:
核心优势
✅ 横切关注点分离 - 业务逻辑与基础设施解耦
✅ 零侵入式设计 - 无需修改现有代码
✅ 高度可复用 - 一次编写,多处使用
✅ 灵活组合 - 多个拦截器协同工作
典型应用场景
- 审计日志 - 自动追踪所有数据变更
- 软删除 - 透明转换硬删除为软删除
- 性能监控 - 捕获慢查询并告警
- 多租户 - 自动数据隔离
- 领域事件 - 解耦业务流程
- 连接弹性 - 自动重试 transient 故障
注意事项
⚠️ 避免在拦截器中执行重型操作
⚠️ 注意拦截器执行顺序
⚠️ 防止递归调用和无限循环
⚠️ 做好异常处理和日志记录
掌握拦截器技术,你可以构建出更加优雅和可维护的企业级应用!