DbCommandInterceptor 通过 CommandExecuting 检查 command.CommandText 是否以 "SELECT" 开头(忽略大小写),仅对 SQL Server 查询语句开头插入 "WITH (NOLOCK)";SaveChangesInterceptor 必须同步/异步均实现以覆盖所有调用路径;拦截器中禁止查库或调 API,避免阻塞;审计日志应使用 Entry().State = Added 防止递归;原始值需在 SavingChanges 阶段提取;拦截器为单例,上下文信息须构造注入。

DbCommandInterceptor 怎么拦截 SELECT 并加 NOLOCK?
只对 SQL Server 有效,且必须判断命令类型,不能无差别加。关键在 CommandExecuting 方法里检查 command.CommandText 是否以 "SELECT" 开头(忽略大小写),再修改 command.CommandText:
- 用
StringComparison.OrdinalIgnoreCase判断,避免大小写敏感导致漏匹配 - 只改查询语句开头,比如插入
"SELECT NOLOCK ",别动后续 WHERE 或 JOIN 部分 - 注意:
NOLOCK可能读到脏数据,禁止用于资金、库存等强一致性场景 - MySQL/PostgreSQL 不支持
NOLOCK,加了会直接报错SqlException
SaveChangesInterceptor 为什么必须同步/异步都实现?
EF Core 的 SaveChanges 和 SaveChangesAsync 是两个独立入口,拦截器若只实现 SavingChanges,那么所有 await context.SaveChangesAsync() 调用都会跳过审计逻辑。
-
SavingChanges对应同步调用,SavingChangesAsync对应异步调用 - 混合使用(比如 Service 层有时同步、有时异步)时,漏实现任一方法 = 漏审计
- 两者逻辑应完全一致,建议抽成私有方法复用,避免不一致
- 别在
SavingChangesAsync里用.Wait()或.Result,会引发死锁
为什么不能在拦截器里查数据库或调外部 API?
拦截器运行在所有 EF Core 写操作的必经路径上,任何耗时操作都会拖慢整个请求链路,尤其在高并发下会迅速成为瓶颈。
-
IHttpContextAccessor在后台服务中不可用,DbContext实例本身也不该被跨作用域复用 - 用户 ID、租户 ID 等上下文信息,应在注册拦截器时通过构造函数注入(如
new AuditInterceptor(userId)),而非运行时去查 - 真需要异步日志落盘,必须确保日志写入本身也是异步的,并且不阻塞主线程(例如用
Task.Run+ 队列缓冲) - 拦截器默认是单例,所有实例共享状态;任何字段缓存、静态变量都可能引发线程安全问题
审计日志实体为什么不能用 Add() 而要用 Entry().State = Added?
在 SavingChanges 中直接调用 context.Logs.Add(log),会再次触发 EF Core 的变更跟踪机制,导致递归调用 SavingChanges,最终栈溢出或无限循环。
- 正确做法是调用
context.Entry(log).State = EntityState.Added,绕过变更检测流程 -
OriginalValues只在SaveChanges执行前有效;一旦保存完成,它会被自动同步为CurrentValues,所以别在SaveChangesAsync的await后再去读 - 如果审计需要记录原始值(比如修改前的 Email),必须在
SavingChanges阶段就提取并暂存,不能延迟
DbContextEventData 或构造参数带进来,而不是靠闭包或静态字段存。


















