How to Log SQL Queries Generated by EF Core

How to Log SQL Queries Generated by EF Core

5 min read··

dotnetef-core

The quickest way to see EF Core's SQL is LogTo(Console.WriteLine, LogLevel.Information) in your DbContext options, or ToQueryString() on a query to read its SQL without executing it. For more than ad-hoc debugging, query tags label a call site, a DbCommandInterceptor logs durations and slow queries, and MiniProfiler shows every query in a request.

The LINQ expression is not what your database executes. Generated SQL reveals accidental joins, missing predicates, parameter values, and repeated queries that are invisible at the C# level. EF Core gives you lightweight inspection for development and structured diagnostics for production.

Why You Need to See the SQL

SQL logging in EF Core means routing the commands the provider sends to the database, and optionally their parameter values, into a log you can read.

EF Core generates SQL behind the scenes. Most of the time it's fine, but sometimes it generates queries that are inefficient, missing indexes, or drastically different from what you expected. If you're not looking at the SQL, you're flying blind.

Check generated SQL during development, especially for complex LINQ queries. The log output exposes N+1 problems, unnecessary subqueries, and missing WHERE clauses that are easy to miss in C#. Understanding what EF Core produces is essential for EF Core performance optimization.

Built-in Logging

The simplest approach - configure EF Core to log SQL to the console or your logging framework:

builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseNpgsql(connectionString)
           .LogTo(Console.WriteLine, LogLevel.Information)
           .EnableSensitiveDataLogging()
           .EnableDetailedErrors();
});
  • LogTo(Console.WriteLine): Sends all EF Core log messages to the console
  • EnableSensitiveDataLogging(): Shows parameter values in logs (disable in production!)
  • EnableDetailedErrors(): Includes more context in error messages

To filter only SQL queries:

options.LogTo(
    Console.WriteLine,
    new[] { DbLoggerCategory.Database.Command.Name },
    LogLevel.Information);

This outputs something like:

Executed DbCommand (5ms) [Parameters=[@__id_0='?' (DbType = Guid)],
CommandType='Text', CommandTimeout='30']
SELECT p."Id", p."Name", p."Price"
FROM "Products" AS p
WHERE p."Id" = @__id_0

Using ILoggerFactory

For better integration with ASP.NET Core's logging:

builder.Services.AddDbContext<AppDbContext>((sp, options) =>
{
    var loggerFactory = sp.GetRequiredService<ILoggerFactory>();

    options.UseNpgsql(connectionString)
           .UseLoggerFactory(loggerFactory);
});

Then configure the log level in appsettings.Development.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.EntityFrameworkCore.Database.Command": "Information",
      "Microsoft.EntityFrameworkCore.Infrastructure": "Warning"
    }
  }
}

This lets you control EF Core logging granularity through configuration.

ToQueryString() for Ad-Hoc Inspection

During debugging, you can convert any LINQ query to its SQL representation without executing it:

var query = dbContext.Products
    .Where(p => p.Price > 100)
    .OrderBy(p => p.Name)
    .Take(10);

var sql = query.ToQueryString();
Console.WriteLine(sql);

// Output:
// SELECT p."Id", p."Name", p."Price"
// FROM "Products" AS p
// WHERE p."Price" > 100.0
// ORDER BY p."Name"
// LIMIT 10

This is useful for checking query shape before execution and in provider-backed tests that verify LINQ-to-SQL translation.

Query Tags

Add descriptive tags to queries so you can identify them in logs and database monitoring tools:

var products = await dbContext.Products
    .TagWith("GetPopularProducts - called from DashboardController")
    .Where(p => p.OrderCount > 100)
    .OrderByDescending(p => p.OrderCount)
    .Take(20)
    .ToListAsync();

The generated SQL includes the tag as a comment:

-- GetPopularProducts - called from DashboardController

SELECT p."Id", p."Name", p."OrderCount"
FROM "Products" AS p
WHERE p."OrderCount" > 100
ORDER BY p."OrderCount" DESC
LIMIT 20

This makes it trivial to trace slow queries back to their source code. In pg_stat_activity, slow query logs, or your database monitoring tool, you'll see the comment right alongside the query.

Interceptors for Advanced Logging

Use interceptors for fine-grained control over query logging:

public class QueryLoggingInterceptor : DbCommandInterceptor
{
    private readonly ILogger<QueryLoggingInterceptor> _logger;

    public QueryLoggingInterceptor(ILogger<QueryLoggingInterceptor> logger)
    {
        _logger = logger;
    }

    public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
        DbCommand command,
        CommandEventData eventData,
        InterceptionResult<DbDataReader> result,
        CancellationToken cancellationToken = default)
    {
        _logger.LogDebug("Executing query:\n{Sql}", command.CommandText);
        return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
    }

    public override ValueTask<DbDataReader> ReaderExecutedAsync(
        DbCommand command,
        CommandExecutedEventData eventData,
        DbDataReader result,
        CancellationToken cancellationToken = default)
    {
        if (eventData.Duration.TotalMilliseconds > 500)
        {
            _logger.LogWarning(
                "Slow query detected ({Duration}ms):\n{Sql}",
                eventData.Duration.TotalMilliseconds,
                command.CommandText);
        }

        return base.ReaderExecutedAsync(command, eventData, result, cancellationToken);
    }
}

Register the interceptor:

builder.Services.AddSingleton<QueryLoggingInterceptor>();

builder.Services.AddDbContext<AppDbContext>((sp, options) =>
{
    var interceptor = sp.GetRequiredService<QueryLoggingInterceptor>();

    options.UseNpgsql(connectionString)
           .AddInterceptors(interceptor);
});

Detecting N+1 Queries

N+1 queries are the most common of the EF Core query performance mistakes. An interceptor can detect them (register it as scoped so the counter is per request):

public class NPlus1DetectorInterceptor : DbCommandInterceptor
{
    private readonly ILogger<NPlus1DetectorInterceptor> _logger;
    private int _queryCount;
    private string? _currentEndpoint;

    public NPlus1DetectorInterceptor(
        ILogger<NPlus1DetectorInterceptor> logger)
    {
        _logger = logger;
    }

    public void ResetForRequest(string endpoint)
    {
        _queryCount = 0;
        _currentEndpoint = endpoint;
    }

    public override ValueTask<DbDataReader> ReaderExecutedAsync(
        DbCommand command,
        CommandExecutedEventData eventData,
        DbDataReader result,
        CancellationToken cancellationToken = default)
    {
        _queryCount++;

        if (_queryCount > 10)
        {
            _logger.LogWarning(
                "Potential N+1 detected: {Count} queries for {Endpoint}.\n" +
                "Latest query:\n{Sql}",
                _queryCount, _currentEndpoint, command.CommandText);
        }

        return base.ReaderExecutedAsync(command, eventData, result, cancellationToken);
    }
}

Using MiniProfiler

MiniProfiler gives you a visual SQL profiler in your browser:

dotnet add package MiniProfiler.AspNetCore.Mvc
dotnet add package MiniProfiler.EntityFrameworkCore

Configure it:

builder.Services.AddMiniProfiler(options =>
{
    options.RouteBasePath = "/profiler";
}).AddEntityFramework();

app.UseMiniProfiler();

Navigate to /profiler/results-index to see:

  • Every SQL query executed per request
  • Query duration
  • Duplicate query detection
  • Parameter values

This is my go-to tool during development for spotting performance issues.

Structured Logging for Production

In production, log query metrics without the full SQL:

public class ProductionQueryInterceptor : DbCommandInterceptor
{
    private readonly ILogger<ProductionQueryInterceptor> _logger;

    public ProductionQueryInterceptor(
        ILogger<ProductionQueryInterceptor> logger)
    {
        _logger = logger;
    }

    public override ValueTask<DbDataReader> ReaderExecutedAsync(
        DbCommand command,
        CommandExecutedEventData eventData,
        DbDataReader result,
        CancellationToken cancellationToken = default)
    {
        _logger.LogInformation(
            "Query executed. Duration: {DurationMs}ms, " +
            "CommandType: {CommandType}, " +
            "HasParameters: {HasParameters}",
            eventData.Duration.TotalMilliseconds,
            command.CommandType,
            command.Parameters.Count > 0);

        return base.ReaderExecutedAsync(
            command, eventData, result, cancellationToken);
    }
}

Never log full SQL with parameter values in production - it may contain sensitive data. Log metrics only.

Debug View in Tests

For integration tests, capture generated SQL:

[Fact]
public async Task GetProducts_GeneratesExpectedSql()
{
    var queries = new List<string>();

    var options = new DbContextOptionsBuilder<AppDbContext>()
        .UseNpgsql(_connectionString)
        .LogTo(sql =>
        {
            if (sql.Contains("SELECT") || sql.Contains("INSERT"))
            {
                queries.Add(sql);
            }
        }, LogLevel.Information)
        .Options;

    await using var context = new AppDbContext(options);

    var products = await context.Products
        .Where(p => p.IsActive)
        .ToListAsync();

    queries.Should().HaveCount(1);
    queries[0].Should().Contain("\"IsActive\"");
}

Summary

Use ToQueryString and development logging to understand query shape before performance becomes an incident. Use query tags and interceptors to connect slow commands to application operations. Keep sensitive-data logging out of production and prefer duration, command metadata, and sanitized diagnostics over parameter values.

Frequently Asked Questions

How do I see the SQL generated by EF Core?

The quickest options are LogTo(Console.WriteLine) in your DbContext options, or calling ToQueryString() on any LINQ query to get its SQL without executing it.

What does EnableSensitiveDataLogging do?

It includes parameter values in EF Core log output instead of masking them. It is invaluable in development and dangerous in production, where parameters often contain personal or secret data.

How can I find slow EF Core queries?

Use a DbCommandInterceptor and check eventData.Duration after execution, logging a warning above a threshold. During development, MiniProfiler also shows per-request query timings and duplicate queries.

What are query tags in EF Core?

TagWith adds a comment above the generated SQL, so you can trace a query seen in database monitoring tools back to the exact call site in your code.

Should EF Core SQL logging be enabled in production?

Log query durations and metadata, not full SQL text with parameters. Full statements can leak sensitive data into logs and add noticeable overhead on hot paths.

Loading comments...

Whenever you're ready, there are 4 ways I can help you:

  1. Pragmatic Clean Architecture: Join 5,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  2. Modular Monolith Architecture: Join 2,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  3. Pragmatic REST APIs: Join 1,900+ students in this course that will teach you how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API.
  4. Patreon Community: Join a community of 5,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

The .NET Weekly

Become a Better .NET Software Engineer

Join 66,000+ engineers who are improving their skills every Saturday morning.