Entity Framework Core: View Generated SQL Statements

A brief post on how to view SQL statements generated by LINQ when using the Fluent API.

If you don’t need the example code, and you just want to learn how to see the SQL in your own code, you can skip directly to Examining the Generated SQL Queries

Tables Schema for the Examples

For this example, I’ll be using my sample Customers and CustomerInvoices tables again. These examples are identical to the ones used in my post on joining tables with LINQ .

Here’s a screenshot of the CustomerInvoices table structure:

Customer Invoices Table in SSMS

Customer Invoices Table

And here’s the SQL DDL for the table:


USE [appdb]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[CustomerInvoices](
	[InvoiceId] [bigint] IDENTITY(1,1) NOT NULL,
	[CustomerId] [int] NOT NULL,
	[InvoiceAmount] [decimal](18, 2) NOT NULL,
	[InvoiceMessage] [varchar](1000) NULL,
 CONSTRAINT [PK_CustomerInvoices] PRIMARY KEY CLUSTERED 
(
	[InvoiceId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

ALTER TABLE [dbo].[CustomerInvoices]  WITH CHECK ADD  CONSTRAINT [FK_CustomerInvoices_Customers] FOREIGN KEY([CustomerId])
REFERENCES [dbo].[Customers] ([Id])
GO

ALTER TABLE [dbo].[CustomerInvoices] CHECK CONSTRAINT [FK_CustomerInvoices_Customers]
GO

Here’s a screenshot of the Customers table structure:

Customers Table in SSMS

Customers Table

And here’s the SQL DDL for the Customers table:


USE [appdb]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Customers](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[Name] [varchar](50) NULL,
	[Address] [varchar](250) NULL,
	[City] [varchar](50) NULL,
	[Region] [varchar](50) NULL,
	[PostalCode] [varchar](50) NULL,
	[Country] [varchar](3) NULL,
	[PhoneNumber] [varchar](20) NULL,
 CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO

Model Classes for the Examples

And here are the model classes for Customers and CustomerInvoice:


public class Customer
{
    public int Id { get; set; }
    public string? Name { get; set; }
    public string? Address { get; set; }
    public string? City { get; set; }
    public string? Region { get; set; }
    public string? PostalCode { get; set; }
    public string? Country { get; set; }
    public string? PhoneNumber { get; set; }
}

public class CustomerInvoice
{
    public long InvoiceId { get; set; }
    public int CustomerId { get; set; }
    public decimal InvoiceAmount { get; set; }
    public string? InvoiceMessage { get; set; }
    public Customer? Customer {get; set;}
}

For these examples, I’ve also created a map for the CustomerInvoice class:


using LinqExamples.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

public class CustomerInvoiceMap : IEntityTypeConfiguration<CustomerInvoice>
{
    public void Configure(EntityTypeBuilder<CustomerInvoice> builder)
    {
        builder.ToTable("CustomerInvoices");
        builder.HasKey(c => c.InvoiceId);
        builder.Property(p => p.InvoiceAmount)
               .HasPrecision(18, 2);
    }
}

The map tells Entity Framework Core what the primary key of the table is, as “InvoiceId” does not follow the standard EF Core convention of using just Id or <TypeName>Id as the primary key name.

The map also lets EF Core know the precision of the InvoiceAmount column.

For this example, we’ll also need the code for the AppDbContext, which tells the .NET application how the database context is configured.

using ViewLinqSql.Models;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.ConstrainedExecution;
using System.Text;
using System.Threading.Tasks;

namespace ViewLinqSql
{
    public class AppDbContext : DbContext
    {
        private readonly string? _connectionString;
        public DbSet<Customer> Customers { get; set; }
        public DbSet<CustomerInvoice> CustomerInvoices { get; set; }
        public DbSet<UserCustomer> Usercustomers { get; set; }

        public AppDbContext(DbContextOptions<AppDbContext> options)
                : base(options)
        {
        }

        public AppDbContext(string connectionString)
        {
            _connectionString = connectionString;
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            if (!optionsBuilder.IsConfigured && _connectionString != null)
            {
                optionsBuilder.UseSqlServer(_connectionString);
            }
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<CustomerInvoice>()
            .HasKey(c => c.InvoiceId);

            modelBuilder.ApplyConfiguration(new CustomerInvoiceMap());
        }

    }
}

Inner Join Example

We’ll use one of the join examples from the previous joining tables with LINQ post. I did move this to a new project with a new namespace, but it’s still the same logic as the first example in the previous post. Here is the console Program.cs file containing the join example:


using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using ViewLinqSql;


var builder = Host.CreateApplicationBuilder(args);

builder.Configuration.AddJsonFile("appsettings.json");

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
if (connectionString == null)
{
    Console.WriteLine("Connection string cannot be null.");
    return;
}

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString));

var app = builder.Build();

using var scope = app.Services.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();

var query = context.CustomerInvoices.Join(context.Customers,
    invoice => invoice.CustomerId, customer => customer.Id,
    (invoice, customer) => new
    {
        customer.Id,
        customer.Name,
        customer.Address,
        invoice.InvoiceId,
        invoice.InvoiceAmount,
        invoice.InvoiceMessage
    });

var customerInvoices = query.Where(x => x.Id == 1).ToList();

Console.WriteLine(customerInvoices[0]);
Console.WriteLine();
Console.ReadKey();

var query2 = context.CustomerInvoices.Join(context.Customers,
    invoice => invoice.CustomerId, customer => customer.Id,
    (invoice, customer) => new
    {
        CustomerId = customer.Id,
        CustomerName = customer.Name,
        CustomerAddress = customer.Address,
        invoice.InvoiceId,
        invoice.InvoiceAmount,
        invoice.InvoiceMessage
    });

var customerInvoices2 = query2.Where(x => x.CustomerId == 2).ToList();

Console.WriteLine(customerInvoices2[0]);
Console.ReadKey();

I’ll run the program, stepping through to the second ReadKey, and the following output is displayed:

Output of Program Showing Results of Join

Output of Program Showing Results of Join

Now we know the program is working as expected. When working with LINQ that gets translated to SQL, it can often be very helpful to inspect the actual SQL being generated by LINQ. Let’s look at a couple of ways to do this.

Examining the Generated SQL Queries

Now that we’ve established our example, we can look at three different ways to see the generated SQL queries.

Examining the SQL Query Using the Debugger

The easiest way to examine the query is using the debugger. I’ll put a breakpoint on the first ReadKey() and let the program run to the breakpoint.

Once at the breakpoint, I can hover over the query variable, an instance of an object that implements the IQueryable interface, to inspect it.

Hovering Over the ```query``` Variable

Hovering Over the Query Variable

On the left side of the information that displays when hovering over the variable is a small arrow. Hovering over this arrow expands additional information available from the debugger.

Expanded ```query``` Variable Information

Expanded query Variable Information

Finally, hovering over the arrow next to Debug View displays two additional properties of the query variable, an Expression property and Query property. The Query property contains the SQL statement that was generated from the LINQ code.

The ```Query``` property of the ```query``` Variable

The Query property of the query Variable

From here, we can right click on the Query property and click “Copy Value” from the context menu.

The Copy Value Item From the Context Menu

The Copy Value Item From the Context Menu

If we click Copy Value and then paste the copied value into a text editor, we can see the SQL statement that was generated from the LINQ code:

SELECT [c0].[Id], [c0].[Name], [c0].[Address], [c].[InvoiceId], [c].[InvoiceAmount], [c].[InvoiceMessage]
FROM [CustomerInvoices] AS [c]
INNER JOIN [Customers] AS [c0] ON [c].[CustomerId] = [c0].[Id]

While hovering is the fastest and most convenient way to view information about a variable in the debugger, another way to get to this same information is to add a watch on the query variable. To do this, right click on the variable and click “Add Watch” from the context menu.

The Add Watch Item From the Context Menu

The Add Watch Item From the Context Menu

This will add a watch in the Watch panel of Visual Studio. The variables properties can then be expanded and inspected in the same way as the hover method.

The Variable Information in the Watch Panel

The Variable Information in the Watch Panel

Write the SQL to a Variable Using ToQueryString.

Another way to view the SQL generated by LINQ in EF Core is to use the ToQueryString() method on the IQueryable variable.

To do this, I’ll add the following line of code just before the lines that write to the console.

var queryString = query.ToQueryString();

If I run the code to the same breakpoint added earlier, I can inspect the queryString variable, which now contains the generated SQL statement.

The SQL Statement Written to a Variable From ```ToQueryString```

The SQL Statement Written to a Variable From ToQueryString

Configure the Log Output to Capture SQL

EF Core integrates with .NET’s built in logging. For a simple example, I will add the following line to the OnConfiguring method of the AppDbContext class:

    optionsBuilder.LogTo(Console.WriteLine);

Here is the entire updated OnConfiguring method.

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    if (!optionsBuilder.IsConfigured && _connectionString != null)
    {
        optionsBuilder.UseSqlServer(_connectionString);
    }

    optionsBuilder.LogTo(Console.WriteLine);
}

Running the program to the same breakpoint again shows that EF Core is now writing log output to the console. The output is verbose, but with a little bit of scrolling, I can find the SQL statement that was generated.

The SQL Statement in the Log Output

The SQL Statement in the Log Output

You’ll notice a minor difference between the SQL in the log output and the SQL copied earlier. In the log output, we’re seeing the WHERE c0.Id == 1 added to the query. This additional WHERE clause is coming from the query.Where(x => x.Id == 1) in the code. If we to wanted capture the effect of the call to .Where() in code, we could call ToQueryString() after the Where. The example code would look like this var queryStringWithWhere = query.Where(x => x.Id == 1).ToQueryString();.

Logging to the console is great for debugging, but is generally not recommended in production due the potential exposure of sensitive information. If you do need to write SQL output to the logs, write the logs to a secure location, such as a file protected by file system security, a database table, or some other log ingestion system that provides adequate security mechanisms.

For more information on writing to a log, see Simple Logging from the Entity Framework Core documentation.

Final Thoughts

In this post, we looked at a few different methods of viewing SQL statements generated by LINQ code. Specifically in a .NET Core console application written in C#. Whether you’re trying to resolve a bug or investigating database performance issues, examining the SQL that gets generated from LINQ can be very helpful.


The postings on this site are my own and do not necessarily reflect the views of my employer.

The content on this blog is for informational and educational purposes only and represents my personal opinions and experience. While I strive to provide accurate and up-to-date information, I make no guarantees regarding the completeness, reliability, or accuracy of the information provided.

By using this website, you acknowledge that any actions you take based on the information provided here are at your own risk. I am not liable for any losses, damages, or issues arising from the use or misuse of the content on this blog.

Please consult a qualified professional or conduct your own research before implementing any solutions or advice mentioned here.