1. EF Core拦截器实战概述
在数据访问层开发中,拦截器(Interceptor)是一种强大的AOP(面向切面编程)实现方式。EF Core提供了完善的拦截器机制,允许我们在数据库操作的关键节点插入自定义逻辑。SaveChangesInterceptor和CommandInterceptor是其中最常用的两种拦截器类型,它们分别对应着不同的执行时机和功能场景。
SaveChangesInterceptor主要拦截DbContext.SaveChanges()方法的执行过程,适合实现审计日志、数据校验等全局性操作。而CommandInterceptor则作用于更底层的数据库命令层面,可以拦截所有发送到数据库的SQL命令,适用于SQL改写、性能监控等场景。
实际项目中,我曾遇到一个典型需求:需要记录所有关键业务表的增删改操作,包括操作人、时间、修改前后的数据差异。通过组合使用这两种拦截器,最终实现了完整的审计追踪功能,且对业务代码零侵入。
2. 核心拦截器详解
2.1 SaveChangesInterceptor深度解析
SaveChangesInterceptor的核心方法是:
public virtual InterceptionResult<int> SavingChanges( DbContextEventData eventData, InterceptionResult<int> result) public virtual int SavedChanges( DbContextEventData eventData, int result)典型实现流程:
- 在SavingChanges中获取变更实体列表:
var entries = eventData.Context.ChangeTracker .Entries() .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified || e.State == EntityState.Deleted);- 遍历实体收集审计信息:
foreach (var entry in entries) { var auditEntry = new AuditEntry(entry); auditEntry.TableName = entry.Metadata.GetTableName(); auditEntry.Action = entry.State.ToString(); // 记录修改前后的值差异... }- 在SavedChanges中确保事务提交后写入审计日志:
// 使用独立上下文保存审计记录 using (var auditContext = new AuditDbContext()) { auditContext.AuditLogs.AddRange(auditEntries); await auditContext.SaveChangesAsync(); }2.2 CommandInterceptor实战技巧
CommandInterceptor的核心方法是:
public virtual ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync( DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result, CancellationToken cancellationToken = default)典型应用场景包括:
- SQL注入防护:
if (command.CommandText.Contains("DROP TABLE")) { throw new InvalidOperationException("危险操作被拦截!"); }- 查询重写(自动添加租户过滤):
if (command.CommandText.StartsWith("SELECT") && !command.CommandText.Contains("TenantId")) { command.CommandText += " WHERE TenantId = @tenantId"; command.Parameters.Add(new SqlParameter("@tenantId", _currentTenant)); }- 性能监控:
var stopwatch = Stopwatch.StartNew(); var result = await next.ReaderExecutingAsync( command, eventData, result, cancellationToken); _logger.LogInformation($"执行耗时:{stopwatch.ElapsedMilliseconds}ms");3. 审计系统完整实现
3.1 审计模型设计
完整的审计模型应包含:
public class AuditLog { public Guid Id { get; set; } public string UserId { get; set; } public string Action { get; set; } // CREATE/UPDATE/DELETE public string TableName { get; set; } public DateTime Timestamp { get; set; } public string OldValues { get; set; } // JSON格式 public string NewValues { get; set; } public string ChangedColumns { get; set; } }3.2 拦截器注册方式
在DbContext配置中注册拦截器:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder .UseSqlServer(connectionString) .AddInterceptors(new AuditSaveChangesInterceptor(_currentUserService)) .AddInterceptors(new QueryRewriteInterceptor(_tenantProvider)); }3.3 性能优化实践
- 批量插入审计记录:
// 使用EF Core的AddRangeAsync批量插入 await auditContext.AuditLogs.AddRangeAsync(auditEntries);- 异步日志写入:
// 使用Channel实现生产者-消费者模式 await _auditChannel.Writer.WriteAsync(auditEntry);- 关键字段索引优化:
CREATE INDEX IX_AuditLog_TableName ON AuditLog(TableName); CREATE INDEX IX_AuditLog_Timestamp ON AuditLog(Timestamp);4. 高级应用场景
4.1 多租户数据隔离
结合CommandInterceptor实现自动租户过滤:
public override InterceptionResult<DbDataReader> ReaderExecuting( DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result) { if (command.CommandText.StartsWith("SELECT")) { command.CommandText = ApplyTenantFilter(command.CommandText); } return result; }4.2 软删除自动转换
通过拦截器将删除操作转换为更新操作:
public override InterceptionResult<int> SavingChanges( DbContextEventData eventData, InterceptionResult<int> result) { foreach (var entry in eventData.Context.ChangeTracker .Entries() .Where(e => e.State == EntityState.Deleted && e.Entity is ISoftDelete)) { entry.State = EntityState.Modified; entry.CurrentValues["IsDeleted"] = true; entry.CurrentValues["DeletedAt"] = DateTime.UtcNow; } return result; }4.3 并发控制增强
在SaveChangesInterceptor中实现乐观并发检查:
public override async Task<int> SavedChangesAsync( DbContextEventData eventData, int result, CancellationToken cancellationToken = default) { var concurrentEntries = eventData.Context.ChangeTracker .Entries() .Where(e => e.State == EntityState.Modified && e.Entity is IConcurrencyControl); foreach (var entry in concurrentEntries) { var originalVersion = entry.OriginalValues["Version"]; var currentVersion = entry.CurrentValues["Version"]; if (originalVersion != currentVersion) { throw new DbUpdateConcurrencyException( $"并发冲突:实体{entry.Metadata.Name}已被其他用户修改"); } } return await base.SaveChangesAsync(eventData, result, cancellationToken); }5. 生产环境注意事项
- 拦截器性能影响评估:
- 每个DbContext实例都会创建新的拦截器实例
- 避免在拦截器中执行耗时操作(如网络请求)
- 对高频操作考虑使用缓存策略
- 异常处理最佳实践:
public override InterceptionResult<int> SavingChanges( DbContextEventData eventData, InterceptionResult<int> result) { try { // 审计逻辑... } catch (Exception ex) { _logger.LogError(ex, "审计记录失败"); // 审计失败不应中断主业务流程 return result; } }- 测试策略建议:
- 单元测试:模拟DbContextEventData验证拦截逻辑
- 集成测试:使用真实数据库验证端到端流程
- 性能测试:评估拦截器对系统吞吐量的影响
- 常见问题排查:
- 拦截器未生效:检查是否正确注册到DbContextOptions
- 审计记录丢失:确认是否在正确的拦截点执行保存
- SQL改写错误:使用MiniProfiler等工具检查最终SQL
在最近的一个电商项目中,我们通过组合使用多种拦截器,实现了完整的审计追踪、多租户隔离和查询优化功能。特别是在高并发场景下,通过异步日志写入和批量处理,审计模块的性能损耗控制在5%以内。一个实用的技巧是:对于复杂的审计需求,可以考虑将审计日志写入专门的数据库,既减轻主库压力,又便于后续分析查询。