When I first started using Entity Framework Core, I had this feeling that everything was almost magical. I write some LINQ, hit Run, and boom, data comes back. At the beginning, that felt amazing. But later, when projects became bigger and business logic became more complicated, that magic sometimes turned into confusion.
I remember one project where a simple screen was loading very slowly. The API was not doing anything crazy, or at least that was what I thought. I looked at my C# code and everything looked clean. Nice LINQ. Very readable. But the users were complaining, and in production the query was taking seconds instead of milliseconds. That was the moment I really understood why logging and debugging EF Core SQL queries is not optional. It is a survival skill.
In this post, I want to share how I usually log and debug EF Core SQL queries, based on real situations I faced. This is not a textbook explanation. This is more like this is what worked for me, this is what I broke, and this is how I fixed it.
All example code in this blog post is not taken from my actual projects. The snippets are simplified and created only to demonstrate the concepts clearly.
Why You Should Care About the SQL Behind EF Core
EF Core is great, but at the end of the day, your database does not understand LINQ. It understands SQL. If you do not know what SQL EF Core is generating, you are basically blind.
In one of my projects, we had a report screen that joined several tables. On the C# side, the query looked fine. But EF Core was generating a SQL query with multiple joins, subqueries, and columns we did not even need. That extra data was killing performance.
So the first lesson I learned is very simple. If something is slow, confusing, or behaving weirdly, look at the SQL.
Enabling Basic SQL Logging
Let us start with the simplest thing. Logging SQL queries to the console or logs.
Logging Using ILogger
In most ASP.NET Core projects, EF Core already integrates with ILogger. You just need to configure it properly. Here is a simple example in Program.cs.
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
.LogTo(Console.WriteLine);
});The first time I did this, I was shocked. I ran one API endpoint and saw many SQL statements printed. That was a wake up call. I thought I was executing one query, but EF Core was doing more behind the scenes.
Later, I improved this by filtering only database commands.
options.LogTo(
Console.WriteLine,
new[] { DbLoggerCategory.Database.Command.Name }
);This way, I only see SQL queries, not everything else.
Adding Log Level Control
In real projects, logging everything in production is not a good idea. I usually control this with log levels.
options.LogTo(
Console.WriteLine,
new[] { DbLoggerCategory.Database.Command.Name },
LogLevel.Information
);In development, I set it to Information. In production, I usually turn this off or log only warnings and errors.
Understanding a Simple Query First
Before debugging complex queries, I always start with something very simple.
var customers = dbContext.Customers
.Where(c => c.IsActive)
.ToList();Looks harmless, right? When you enable logging, you will see something like this
SELECT [c].[Id], [c].[Name], [c].[IsActive]
FROM [Customers] AS [c]
WHERE [c].[IsActive] = 1This is good. Simple and clean. At this stage, logging helps confirm that EF Core is doing what you expect. This is especially helpful when you are new to EF Core or when onboarding junior developers.
Filtering and Business Rules That Grow Over Time
Now let us make it more realistics. In one of my projects, filtering started simple. Then business rules kept coming.
var orders = dbContext.Orders
.Where(o => o.Status == OrderStatus.Completed)
.Where(o => o.TotalAmount > 1000)
.Where(o => o.CreatedAt >= startDate && o.CreatedAt <= endDate)
.ToList();At first glance, this still looks okay. But when I checked the SQL, I noticed something important. EF Core combined all those Where calls into one SQL statement. That is good.
SELECT ...
FROM [Orders] AS [o]
WHERE [o].[Status] = 2
AND [o].[TotalAmount] > 1000
AND [o].[CreatedAt] BETWEEN @startDate AND @endDateLogging here gave me confidence that chaining conditions was no the problem.
Joins and Navigation Properties
This is where things usually start to get tricky.
Using Navigation Properties
var orders = dbContext.Orders
.Where(o => o.Customer.IsVip)
.Select(o => new
{
o.Id,
o.TotalAmount,
CustomerName = o.Customer.Name
})
.ToList();When I first used queries like this, I assumed EF Core would do something inefficient. But when I checked the SQL, it was actually fine.
SELECT [o].[Id], [o].[TotalAmount], [c].[Name] AS [CustomerName]
FROM [Orders] AS [o]
INNER JOIN [Customers] AS [c] ON [o].[CustomerId] = [c].[Id]
WHERE [c].[IsVip] = 1Logging taught me something important here. EF Core is smarter than we sometimes think. But you still need to verify.
The N Plus One Problem and How Logging Saved Me
I clearly remember struggling with this. I had code like this.
var orders = dbContext.Orders.ToList();
foreach (var order in orders)
{
var items = dbContext.OrderItems
.Where(i => i.OrderId == order.Id)
.ToList();
}From a C# point of view, it worked. From a database point of view, it was a disaster. When I enabled SQL logging, I saw one query for oders and then one query per order for items. That was painful to watch. This is where logging really helped me understand the problem, not just read about it.
The fix was simple
var orders = dbContext.Orders
.Include(o => o.Items)
.ToList();After that, the SQL logs showed a single query with joins instead of dozens of queries.
Projections to Control What SQL Returns
Another lesson I learned the hard way is this. If you do not control what you select, EF Core will select everything.
var orders = dbContext.Orders
.Include(o => o.Customer)
.ToList();This pulls all columns from both tables. In real projects, especially with reporting or dashboards, that is not ideal. I started using projections more intentionally.
var orders = dbContext.Orders
.Select(o => new
{
o.Id,
o.TotalAmount,
CustomerName = o.Customer.Name
})
.ToList();When I checked the SQL, it was much cleaner. Only the needed columns were selected.
Logging made this visible and measurable.
Debugging Queries Without Executing Them
Sometimes, I do not even want to execute the query. I just want to see the SQL.
EF Core has a very useful method.
var query = dbContext.Orders
.Where(o => o.TotalAmount > 500);
var sql = query.ToQueryString();This saved me many times during debugging. I could copy the SQL, paste it into SQL Server Management Studio, and analyze execution plans.
In one project, this helped me identify missing indexes. Without seeing the SQL, I would never know where to start.
Tracking Versus No Tracking
Another performance issue I faced was related to tracking. By default, EF Core tracks entities.
var customers = dbContext.Customers.ToList();For read only scenarios, this tracking is unnecessary.
var customers = dbContext.Customers
.AsNoTracking()
.ToList();When logging SQL, the query itself looks the same. But memory usage and peformance improve. Logging plus profiling helped me understand when tracking was hurting us.
Logging Parameters and Sensitive Data
One thing to be careful about sensitive data. You can enable parameter logging.
options.EnableSensitiveDataLogging();This is useful in development. But never enable this in production if you deal with sensitive data. I learned this rule early, especially in payment related systems. Logs can easily become a security issue of you are not careful.
How Logging Helped Me Talk to Non Developers
This is a small but important point. Sometimes, I had to explain performance issues to product owners or managers. Showing them SQL logs and execution time helped make the problem concrete. Instead of saying EF Core is slow, I could say this query scans a million rows.
Logging gave me evidence, not just opinions.
Final Thoughts
EF Core is a powerful tool, but it does not remove the need to understand the SQL. Logging and debugging queries helped me grow as a developer. It helped me fix performance issues, avoid bad patterns, and build better solutions for real business problems.
If you take only one thing from this post, let it be this: “Do not guess what EF Core is doing. Log it. Read it. Learn from it”.
Sources
Here are some references I often go back to and that you can explore further.






