SQL Server Execution Plans: What EF Core Developers Should Actually Look At

September 26, 2025 · 8 min

You’re looking at a SQL Server execution plan that looks like a spider web of boxes and arrows. Your EF Core query takes 800ms, but you have no idea where to start optimizing.

Execution plans contain overwhelming detail, but EF Core developers only need to focus on specific elements. Here’s what actually matters for optimizing your LINQ queries.

Getting Execution Plans from EF Core

First, you need to capture the SQL that EF Core generates:

// Enable query logging to see generated SQL
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseSqlServer(connectionString)
        .LogTo(Console.WriteLine, LogLevel.Information)
        .EnableSensitiveDataLogging(); // Shows parameter values
}

Run your EF Core query and copy the logged SQL:

var expensiveOrders = await context.Orders
    .Include(o => o.Customer)
    .Include(o => o.OrderItems)
        .ThenInclude(oi => oi.Product)
    .Where(o => o.OrderDate >= DateTime.Now.AddDays(-30))
    .ToListAsync();

EF Core generates SQL like this:

SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate], [o].[Total],
       [c].[Id], [c].[Name], [c].[Email],
       [o0].[Id], [o0].[OrderId], [o0].[ProductId], [o0].[Quantity],
       [p].[Id], [p].[Name], [p].[Price]
FROM [Orders] AS [o]
INNER JOIN [Customers] AS [c] ON [o].[CustomerId] = [c].[Id]
LEFT JOIN [OrderItems] AS [o0] ON [o].[Id] = [o0].[OrderId]
LEFT JOIN [Products] AS [p] ON [o0].[ProductId] = [p].[Id]
WHERE [o …

Read more

RowVersion and Timestamps in EF Core: Practical Guide for SQL Server

September 22, 2025 · 8 min

You add a RowVersion property to your entity, mark it with [Timestamp], and think you’re done with concurrency control. Then production hits: concurrency exceptions aren’t caught properly, API clients can’t handle the binary data, and you’re not sure if your timestamps are working.

SQL Server’s rowversion is powerful but has specific quirks that can trip up EF Core developers. Here’s how to implement it correctly.

Understanding SQL Server RowVersion

SQL Server’s rowversion is not a timestamp despite the old name. It’s an 8-byte binary value that automatically increments whenever any column in the row changes:

CREATE TABLE [Products] (
    [Id] int IDENTITY(1,1) NOT NULL,
    [Name] nvarchar(100) NOT NULL,
    [Price] decimal(18,2) NOT NULL,
    [RowVersion] rowversion NOT NULL,
    CONSTRAINT [PK_Products] PRIMARY KEY ([Id])
);

Every time you UPDATE this row, SQL Server automatically changes the RowVersion value. You never set it manually - the database engine handles everything.

EF Core RowVersion Implementation

The correct way to implement rowversion in EF Core:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    
    [Timestamp] // This maps to SQL Server rowversion
    public byte[] RowVersion { get; set; }
}

The [Timestamp] attribute tells EF Core this is a concurrency token that’s automatically generated by the database. EF Core …

Read more