EF Core 6+ 推荐用 DbCommandInterceptor 实现 SQL 日志记录,需在 AddDbContext 配置中注册线程安全实例,重写 CommandExecuting/CommandExecuted;避免 LogTo 的语义日志局限,兼顾参数安全格式化、异步落盘与采样策略。

EF Core 中如何注册 DbCommandInterceptor 实现 SQL 日志记录
EF Core 6+ 推荐用 DbCommandInterceptor 拦截原始 SQL 执行,它比旧版的 IDbCommandTreeInterceptor 更轻量、更直接。注册必须在 DbContextOptionsBuilder 阶段完成,且拦截器实例需是线程安全的(不能带状态,或自行同步)。
常见错误:把拦截器当普通服务注入到构造函数里,或在 OnConfiguring 中重复 AddInterceptors —— 这会导致多次注册、日志重复甚至异常。
- 在
Startup.cs或Program.cs的services.AddDbContext配置中调用.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快速调试,但仅限开发环境().Select(p => $"{p.ParameterName} = {p.Value ?? "NULL"}").ToArray() - 生产环境建议用
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(底层已适配 async logger provider),或投递到.LogInformation Channel由后台任务批量刷盘 - 避免在拦截器里做耗时操作:如序列化大对象、查配置中心、调远程服务 —— 这些应提前缓存或降级
- 生产环境建议加采样开关,比如只记录慢 SQL(
durationMs > 500)或失败命令(result.Exception != null)
最复杂的不是怎么写拦截器,而是怎么让它既看得清 SQL,又不拖垮系统 —— 参数格式化、异步落盘、采样策略,这三块漏掉任一,上线后都可能变成隐性故障源。










