在.NET项目开发中,不管是做Web应用还是后台服务,只要用到EF Core操作数据库,大概率都会遇到过查询变慢的问题——前端页面加载卡顿、接口响应超时,排查半天找不到原因,这时候慢查询日志就是最好的帮手。

一、为什么要关注EF Core的慢查询

很多开发者刚开始用EF Core时,会觉得“它会自动生成最优SQL”,但实际项目中,因为实体关系映射、条件拼接、索引缺失等问题,EF Core生成的SQL很可能是低效的,比如全表扫描、笛卡尔积查询,最终导致接口响应慢。

1.1 应用场景

最常见的场景是:电商商品列表接口,原本响应时间200ms,上线后突然变成3秒,用户反馈卡顿;或者后台报表查询,本应秒出,却要等5秒以上,影响运营效率。这时候如果没有慢查询日志,只能去数据库抓SQL,步骤繁琐,耽误时间。

1.2 技术优缺点

用EF Core日志拦截器的优点很明显:不需要额外部署监控工具,不用手动抓SQL,所有查询都会被统一拦截,成本低;缺点是如果拦截逻辑写得太复杂,可能会轻微影响查询性能,所以要保持逻辑轻量。

1.3 注意事项

别把慢查询阈值设得太严,比如所有查询都设100ms,会生成大量无用日志;另外,别在拦截器里执行耗时操作,比如调用第三方API,会导致查询本身变慢。

二、EF Core日志拦截器的核心原理

EF Core提供了一套拦截器机制,基于“钩子”设计,在SQL执行的各个阶段(执行前、执行后、异常)都能插入自定义逻辑。我们只需要继承DbCommandInterceptor类,重写对应方法,就能拿到SQL语句、执行时间等关键信息,进而判断是否为慢查询。

2.1 拦截器的作用

它相当于EF Core和数据库之间的“中间人”,能监听所有DbCommand的执行,不管是增删改查还是存储过程调用,都逃不过它的监听。

2.2 如何注册拦截器

在DbContext的OnConfiguring方法里,通过AddInterceptors把我们写好的拦截器注册进去,这样EF Core就会自动调用它的逻辑。

三、完整示例:实现慢查询日志拦截器

我们用.NET 6+EF Core 6作为技术栈,一步步实现可以直接复用的慢查询拦截器,代码带详细注释,方便理解。

using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using System.Data.Common;
using System.Diagnostics;
using Microsoft.Extensions.Logging;

// 慢查询拦截器:记录超过自定义阈值的SQL执行
public class SlowQueryInterceptor : DbCommandInterceptor
{
    // 慢查询阈值:超过500毫秒记为慢查询,可根据业务调整
    private const int SlowQueryThreshold = 500;
    // 依赖日志服务,用来输出慢查询日志
    private readonly ILogger<SlowQueryInterceptor> _logger;

    // 构造函数,从DI获取日志实例,符合依赖注入原则
    public SlowQueryInterceptor(ILogger<SlowQueryInterceptor> logger)
    {
        _logger = logger;
    }

    // SQL执行前,启动计时器,记录执行开始时间
    public override InterceptionResult<DbDataReader> ReaderExecuting(
        DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
    {
        // 用反射给DbCommand加个私有属性存计时器(因为官方没提供公开属性)
        command.GetType().GetProperty("ExecutionTimer", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)
            ?.SetValue(command, Stopwatch.StartNew());
        return base.ReaderExecuting(command, eventData, result);
    }

    // SQL执行后,停止计时器,判断是否慢查询
    public override DbDataReader ReaderExecuted(
        DbCommand command, CommandExecutedEventData eventData, DbDataReader result)
    {
        // 取出之前存的计时器,计算执行时间
        var stopwatch = (Stopwatch)command.GetType()
            .GetProperty("ExecutionTimer", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)?
            .GetValue(command);
        
        if (stopwatch != null)
        {
            stopwatch.Stop();
            // 超过阈值就输出警告日志,包含SQL语句、执行时间、参数(简化参数防止泄露)
            if (stopwatch.ElapsedMilliseconds > SlowQueryThreshold)
            {
                var sql = command.CommandText;
                var parameters = string.Join(",", command.Parameters.Cast<DbParameter>().Select(p => $"{p.ParameterName}={p.Value}"));
                _logger.LogWarning($"【慢查询预警】执行时间:{stopwatch.ElapsedMilliseconds}ms | SQL:{sql} | 参数:{parameters}");
            }
        }
        return base.ReaderExecuted(command, eventData, result);
    }
}

接下来是DbContext的配置,把拦截器注册进去:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

// 自定义DbContext,对应数据库操作
public class AppDbContext : DbContext
{
    private readonly IServiceProvider _serviceProvider;

    // 构造函数注入DbContextOptions和服务提供者,方便注册拦截器
    public AppDbContext(DbContextOptions<AppDbContext> options, IServiceProvider serviceProvider) : base(options)
    {
        _serviceProvider = serviceProvider;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        // 配置SQL Server连接字符串(替换成自己的数据库地址)
        var connectionString = "Server=.;Database=ShopDB;Integrated Security=True;TrustServerCertificate=True;";
        // 注册慢查询拦截器,注意要注入日志实例
        optionsBuilder.UseSqlServer(connectionString)
                      .AddInterceptors(new SlowQueryInterceptor(_serviceProvider.GetRequiredService<ILogger<SlowQueryInterceptor>>()));
    }

    // 示例实体:商品表
    public DbSet<Product> Products { get; set; }
}

// 商品实体类
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int CategoryId { get; set; }
}

最后写个测试程序,模拟慢查询触发拦截器:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

class Program
{
    static async Task Main(string[] args)
    {
        // 初始化服务容器,注册DbContext和日志
        var services = new ServiceCollection();
        services.AddDbContext<AppDbContext>();
        // 添加控制台日志,级别设为Warning,只会输出慢查询日志
        services.AddLogging(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Warning));
        
        var serviceProvider = services.BuildServiceProvider();
        
        // 用作用域获取DbContext,避免资源泄漏
        using var scope = serviceProvider.CreateScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        // 模拟慢查询1:复杂条件查询,实际项目中可能因为没索引变慢
        var expensiveProducts = await dbContext.Products
            .Where(p => p.Price > 10000 && p.CategoryId == 5)
            .ToListAsync();

        // 模拟慢查询2:故意加延迟,触发日志(真实场景中不需要这行,是模拟用的)
        await Task.Delay(600);
        var allProducts = await dbContext.Products.ToListAsync();
    }
}

四、拦截器的优化和注意事项

刚才的示例用了反射,其实可以换更优雅的方式,比如自定义DbCommand子类,避免反射;另外,参数部分最好脱敏,比如用户ID、手机号这些敏感信息,不要直接记录;还有,阈值要分场景设置,比如用户交互的接口设300ms,后台定时任务设2秒,因为定时任务本来就慢,不用太苛刻。

4.1 避免反射的优化

如果不想用反射存计时器,可以重写CreateDbCommand方法,在创建命令时就附加计时器,不需要后续反射取值,代码更安全。

4.2 敏感数据处理

记录参数时,要过滤PasswordPhoneEmail这类字段,避免日志泄露敏感信息,比如把参数里的敏感值替换成***

五、总结

用EF Core日志拦截器诊断慢查询,是一种轻量、低成本的性能排查方式,不需要额外工具,适合所有用EF Core的.NET项目。它能帮开发者快速定位低效的SQL,优化索引和查询逻辑,提升应用响应速度。整个实现过程代码少、易集成,哪怕是新手也能跟着示例快速上手,解决实际开发中的性能问题。