# How to Log SQL Queries Generated by EF Core

> Generated SQL exposes inefficient joins, repeated round trips, and missing predicates hidden by LINQ. Inspect it with EF Core logging, query tags, interceptors, and profiling tools.

Published: 2026-08-19. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/log-sql-queries-ef-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**](https://milanjovanovic.tech/blog/n-plus-one-query-ef-core), 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**](https://milanjovanovic.tech/blog/ef-core-performance-guide).

## Built-in Logging

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

```csharp
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:

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

This outputs something like:

```text
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:

```csharp
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`:

```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:

```csharp
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:

```csharp
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:

```sql
-- 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**](https://milanjovanovic.tech/blog/how-to-use-ef-core-interceptors) for fine-grained control over query logging:

```csharp
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:

```csharp
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**](https://milanjovanovic.tech/blog/ef-core-query-performance-mistakes). An interceptor can detect them (register it as scoped so the counter is per request):

```csharp
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:

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

Configure it:

```csharp
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:

```csharp
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:

```csharp
[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.
