EF Core 中如何注册 DbCommandInterceptor
实现 SQL 日志记录
EF Core 6+ 推荐用
DbCommandInterceptor拦截原始 SQL 执行,它比旧版的
IDbCommandTreeInterceptor更轻量、更直接。注册必须在
DbContextOptionsBuilder阶段完成,且拦截器实例需是线程安全的(不能带状态,或自行同步)。
常见错误:把拦截器当普通服务注入到构造函数里,或在
OnConfiguring中重复 AddInterceptors —— 这会导致多次注册、日志重复甚至异常。 在
Startup.cs或
Program.cs的
services.AddDbContext<mycontext></mycontext>配置中调用
.AddInterceptors(new SqlLoggerInterceptor())拦截器类需继承
DbCommandInterceptor,重写
CommandExecuting(同步执行前)或
CommandExecuted(执行后) 若只需看 SQL,
CommandExecuting足够;若还要查耗时或结果行数,用
CommandExecuted并检查
result是否为
DbDataReader或
int
CommandExecuting
中如何安全提取参数化 SQL 和参数值
EF Core 默认使用参数化查询,
DbCommand.CommandText是带 @param 的模板,
DbCommand.Parameters是
DbParameter集合。直接拼接易出错,尤其遇到
DBNull.Value、二进制数据或日期时区问题。
别手动遍历
Parameters拼字符串 —— 容易漏转义、类型不一致、日志格式混乱。推荐用现成工具辅助格式化。 用
command.Parameters.Cast<dbparameter>().Select(p => $"{p.ParameterName} = {p.Value ?? "NULL"}").ToArray()</dbparameter> 快速调试,但仅限开发环境
生产环境建议用 Microsoft.Data.SqlClient的
SqlClientFactory.CreateCommand()+
ToString()行为模拟,或引入轻量库如
EFCore.SqlServer.Interceptors(非官方)做安全展开 注意:
p.Value可能是
DateTimeOffset、
byte[]、
Guid,打印前先
ToString("O") 或 Convert.ToString()避免
ToString()返回空或乱码
为什么 LogTo
不够用,还得自己写拦截器
DbContextOptionsBuilder.LogTo确实能打日志,但它输出的是 EF Core 内部语义(如 “Executing DbCommand [Parameters=[@p0='xxx']]”),不包含真实 SQL 文本,也不方便对接 ELK、Seq 等结构化日志系统。
更重要的是:
LogTo不暴露命令执行耗时、影响行数、是否成功等上下文,也无法修改命令(比如统一加
SET STATISTICS IO ON调试)。
LogTo输出不可控,字段无固定 schema;拦截器中可自由构造 JSON 对象,含
commandText、
parameters、
durationMs、
timestamp、
isSuccess某些场景需改写 SQL:比如多租户系统中自动注入
WHERE TenantId = @tenantId,这只能在
CommandExecuting中通过修改
command.CommandText和追加参数实现
LogTo无法区分是迁移命令、查询还是 SaveChanges —— 拦截器可通过
command.CommandType(
Text/
StoredProcedure)和上下文栈判断
拦截器里写日志要注意线程安全和性能开销
每次数据库操作都会触发拦截器,高频写文件或同步 HTTP 请求会拖慢整个请求链路。日志本身不该成为瓶颈。
容易被忽略的点:拦截器方法运行在 EF Core 的同步/异步执行路径中,
Console.WriteLine或
File.AppendAllText这类同步 I/O 会阻塞线程池,尤其在高并发 Web API 下极易引发线程饥饿。 日志写入务必异步:用
ILogger<t>.LogInformation</t>(底层已适配 async logger provider),或投递到
Channel<logentry></logentry>由后台任务批量刷盘 避免在拦截器里做耗时操作:如序列化大对象、查配置中心、调远程服务 —— 这些应提前缓存或降级 生产环境建议加采样开关,比如只记录慢 SQL(
durationMs > 500)或失败命令(
result.Exception != null)
最复杂的不是怎么写拦截器,而是怎么让它既看得清 SQL,又不拖垮系统 —— 参数格式化、异步落盘、采样策略,这三块漏掉任一,上线后都可能变成隐性故障源。
