Entity Framework  

EF Core 11 Query Translation Changes Under Real Workloads

Entity Framework Core allows developers to write strongly typed LINQ while EF Core translates those expressions into database-specific SQL.

That abstraction is productive, but it creates an important performance boundary:

C# LINQ
   ↓
Expression Tree
   ↓
EF Core Translation
   ↓
Generated SQL
   ↓
Database Query Optimizer
   ↓
Execution Plan
   ↓
Results

A LINQ query that looks efficient in C# is not necessarily efficient SQL.

This becomes especially important when upgrading EF Core because query translation behavior can change even when application code remains unchanged.

.NET 11 Preview 6 includes LINQ query translation improvements in Entity Framework Core, along with support for keys and indexes traversing complex-type properties.

That makes the useful question less about "What is new in EF Core 11?" and more about:

How does EF Core 11 translate real application queries, and does the generated SQL actually improve execution?

This article presents a practical methodology for evaluating EF Core 11 query translation against an existing implementation.

Why Query Translation Matters

Consider this LINQ query:

var customers = await db.Customers
    .Where(c => c.IsActive)
    .OrderBy(c => c.Name)
    .Select(c => new
    {
        c.Id,
        c.Name
    })
    .ToListAsync();

The C# code is compact.

The database does not execute C#.

EF Core has to translate it into SQL similar to:

SELECT
    [c].[Id],
    [c].[Name]
FROM [Customers] AS [c]
WHERE [c].[IsActive] = 1
ORDER BY [c].[Name];

The important performance characteristics are therefore determined by several factors:

  • Generated SQL

  • Available indexes

  • Cardinality

  • Query plan

  • Number of rows scanned

  • Columns returned

  • Database provider

  • Parameter values

  • Database statistics

A change in EF Core's translator can alter one or more of these characteristics.

Start With a Baseline

Before upgrading EF Core, capture representative queries from the existing application.

Do not start with artificial examples.

Choose queries that are:

  • Executed frequently

  • Data-intensive

  • Known to be slow

  • Used in critical API endpoints

  • Involved in reporting

  • Sensitive to indexes

  • Containing complex projections or joins

Create a baseline table:

QueryCurrent EF CoreMean TimeRowsSQL Shape
Customer searchExistingMeasureMeasureRecord
Order historyExistingMeasureMeasureRecord
Product filteringExistingMeasureMeasureRecord
Reporting queryExistingMeasureMeasureRecord

The measurements should come from the target database and workload.

Do not substitute theoretical benchmark numbers.

Keep the Database Constant

A valid EF Core comparison should isolate the ORM version as much as possible.

Ideally:

Same application query
Same database
Same schema
Same data
Same indexes
Same provider
Same configuration

        ↓

EF Core version A

versus

EF Core version B

If the database schema and EF Core version change simultaneously, it becomes difficult to determine whether a performance difference came from translation, indexing, or another change.

For a production migration, additional changes may eventually be necessary, but the first experiment should be controlled.

Inspect Generated SQL

One of the simplest ways to investigate translation is ToQueryString().

For example:

var query = db.Orders
    .Where(o => o.CustomerId == customerId)
    .OrderByDescending(o => o.CreatedAt)
    .Select(o => new
    {
        o.Id,
        o.CreatedAt,
        o.Total
    });

var sql = query.ToQueryString();

Print or capture the SQL:

Console.WriteLine(sql);

This allows you to compare SQL generated by two EF Core versions.

However, SQL text alone is not enough.

Two SQL statements can be logically equivalent while producing very different execution plans.

Compare Query Shapes

Suppose the previous EF Core version generated:

SELECT
    [o].[Id],
    [o].[CreatedAt],
    [o].[Total]
FROM [Orders] AS [o]
WHERE [o].[CustomerId] = @customerId
ORDER BY [o].[CreatedAt] DESC;

The newer translator may produce a different but semantically equivalent SQL shape.

The correct evaluation is:

LINQ
 ↓
SQL
 ↓
Execution Plan
 ↓
Logical Reads
 ↓
CPU
 ↓
Elapsed Time

Do not declare an improvement merely because the generated SQL looks cleaner.

Use Realistic Data Volumes

Query translation differences can remain invisible on a small development database.

Consider:

Development
10,000 orders

Production-like test
10,000,000 orders

A query that executes quickly against 10,000 rows may behave very differently when indexes, cardinality, and selectivity become meaningful.

For each benchmark, record:

Rows in table
Rows matched
Rows returned
Relevant indexes
Database engine
Database version

This allows the result to be reproduced and interpreted correctly.

Benchmark Query Execution

A simple benchmark can execute a query repeatedly.

For example:

[MemoryDiagnoser]
public class EfQueryBenchmark
{
    private readonly AppDbContext _db;

    [Benchmark]
    public async Task<List<OrderDto>> QueryOrders()
    {
        return await _db.Orders
            .Where(o => o.IsActive)
            .OrderByDescending(o => o.CreatedAt)
            .Select(o => new OrderDto
            {
                Id = o.Id,
                Total = o.Total
            })
            .ToListAsync();
    }
}

For database benchmarks, BenchmarkDotNet should be used carefully.

The database itself introduces variability through:

  • Buffer cache

  • Concurrent workloads

  • Network latency

  • Query-plan caching

  • Background activity

  • Database statistics

For this reason, database-level load tests and query-plan analysis should complement microbenchmarks.

Measure More Than Elapsed Time

A query that takes 50 ms instead of 60 ms is interesting.

But you also want to know why.

Measure where possible:

MetricPurpose
Elapsed timeOverall query performance
CPU timeDatabase computation
Logical readsData access efficiency
Physical readsStorage impact
Rows examinedSelectivity
Rows returnedResult size
Execution planAccess strategy
AllocationsApplication-side cost

The available metrics depend on the database provider.

Query Translation and Complex Types

One of the notable EF Core 11 Preview 6 changes is that keys and indexes can traverse complex-type properties.

This matters when a domain model groups related values into a complex type.

For example:

public class Customer
{
    public int Id { get; set; }

    public required CustomerAddress Address { get; set; }
}

public class CustomerAddress
{
    public required string City { get; set; }

    public required string PostalCode { get; set; }
}

A query might be:

var customers = await db.Customers
    .Where(c => c.Address.PostalCode == postalCode)
    .ToListAsync();

The important question is not whether the LINQ expression compiles.

The important questions are:

What SQL is generated?
Is the predicate translated to the expected column?
Can the database use the relevant index?
What does the execution plan show?

That is the production-oriented way to evaluate the feature.

Test Indexed and Non-Indexed Queries

Use two scenarios.

Without a Relevant Index

Query
 ↓
Table Scan
 ↓
Many rows examined

With a Relevant Index

Query
 ↓
Index Seek / Appropriate Access Path
 ↓
Fewer rows examined

The exact execution strategy depends on the database engine and data distribution.

EF Core can generate valid SQL, but it does not replace database indexing strategy.

This distinction is important:

EF Core controls query generation; the database optimizer determines the physical execution strategy.

Capture Execution Plans

For SQL Server, inspect the actual execution plan.

For PostgreSQL, use tools such as:

EXPLAIN (ANALYZE, BUFFERS)

The exact command depends on the database.

The goal is to compare:

Old EF Core
    ↓
Generated SQL
    ↓
Execution Plan

New EF Core
    ↓
Generated SQL
    ↓
Execution Plan

Look for meaningful differences such as:

  • Table scan versus index access

  • Join strategy

  • Sort operations

  • Estimated versus actual rows

  • Excessive reads

  • Expensive operators

  • Additional subqueries

Do not judge an execution plan from one operator alone.

Test Projection Changes

One common optimization is projecting only required columns.

Instead of:

var orders = await db.Orders
    .ToListAsync();

use:

var orders = await db.Orders
    .Select(o => new OrderSummary
    {
        Id = o.Id,
        Total = o.Total,
        CreatedAt = o.CreatedAt
    })
    .ToListAsync();

This can reduce the amount of data transferred and materialized.

When comparing EF Core versions, verify whether translation changes alter the generated projection.

The benchmark should measure both:

Entity materialization

and:

DTO projection

Test Joins With Real Cardinality

Joins are another area where generated SQL should be evaluated against real data.

For example:

var orders = await db.Orders
    .Where(o => o.Customer.IsActive)
    .Select(o => new
    {
        OrderId = o.Id,
        CustomerName = o.Customer.Name,
        Total = o.Total
    })
    .ToListAsync();

Inspect:

Generated JOIN
Join predicate
Indexes
Rows before join
Rows after join
Execution plan

A LINQ query can look simple while generating a costly join.

Test Grouping and Aggregation

Aggregation queries deserve separate testing.

For example:

var totals = await db.Orders
    .GroupBy(o => o.CustomerId)
    .Select(g => new
    {
        CustomerId = g.Key,
        Total = g.Sum(o => o.Total),
        Count = g.Count()
    })
    .ToListAsync();

Measure:

  • SQL generated

  • Database CPU

  • Reads

  • Execution time

  • Rows returned

Compare the execution plans between EF Core versions.

Do not assume that a shorter SQL statement is necessarily more efficient.

Test Conditional Predicates

Real applications often construct queries dynamically.

For example:

IQueryable<Order> query =
    db.Orders;

if (customerId.HasValue)
{
    query = query.Where(
        o => o.CustomerId == customerId.Value);
}

if (fromDate.HasValue)
{
    query = query.Where(
        o => o.CreatedAt >= fromDate.Value);
}

if (toDate.HasValue)
{
    query = query.Where(
        o => o.CreatedAt <= toDate.Value);
}

The resulting SQL can vary considerably depending on which filters are active.

Benchmark several combinations:

No filters
Customer only
Date only
Customer + date
Customer + date + status

This is much closer to real API behavior than benchmarking one fixed query.

Test Parameter Selectivity

Database performance can change depending on parameter values.

For example:

Customer A → 2 matching rows
Customer B → 2,000,000 matching rows

The same LINQ query may have very different execution characteristics.

Therefore, use representative parameter distributions.

Measure:

Highly selective
Moderately selective
Low selectivity

This can reveal performance problems that a single benchmark parameter hides.

Watch for Client-Side Evaluation

EF Core's query pipeline is designed to translate supported LINQ expressions to SQL. Since EF Core 3.0, unsupported expressions in most query positions cause translation failures rather than silently pulling the relevant rows into memory for client-side filtering.

For example:

var results = await db.Customers
    .Where(c => IsPreferredCustomer(c))
    .ToListAsync();

If IsPreferredCustomer cannot be translated, EF Core may reject the query rather than execute the predicate against every returned row.

That behavior is valuable from a performance perspective because accidental client-side evaluation can be extremely expensive.

During an EF Core upgrade, test queries that previously relied on custom expressions carefully.

Test Translation Failures Explicitly

A regression is not always "the query became slower."

A query can also change from:

Translated successfully

to:

Translation failure

or the reverse.

Create tests that verify important queries can still translate:

var query = db.Customers
    .Where(c => c.IsActive)
    .Select(c => new
    {
        c.Id,
        c.Name
    });

var sql = query.ToQueryString();

Assert.NotEmpty(sql);

For more complex queries, execute them against a controlled database as well.

The test should verify behavior rather than merely checking that ToQueryString() returns text.

Test Query Translation Across Providers

EF Core is provider-based.

The SQL generated for:

SQL Server
PostgreSQL
SQLite

does not have to be identical.

A query translation improvement in EF Core may therefore have different practical effects depending on the database provider.

If your application supports multiple providers, test each one separately.

Do not generalize a SQL Server result to PostgreSQL without evidence.

Avoid the In-Memory Provider for Translation Benchmarks

The EF Core InMemory provider is useful for some application tests, but it does not reproduce relational SQL generation and database execution behavior.

For query-translation performance testing, use the actual relational provider.

For example:

SQL Server workload
→ SQL Server provider

PostgreSQL workload
→ PostgreSQL provider

The database engine and provider are part of the workload being evaluated.

Compare Compiled Queries Carefully

EF Core caches query compilation internally, so repeated execution is not equivalent to compiling a brand-new LINQ expression every time.

If your application uses compiled queries, benchmark them separately.

For example:

private static readonly Func<
    AppDbContext,
    int,
    IAsyncEnumerable<Order>> OrdersByCustomer =
    EF.CompileAsyncQuery(
        (AppDbContext db, int customerId) =>
            db.Orders
                .Where(o => o.CustomerId == customerId));

Then:

await foreach (
    var order in OrdersByCustomer(
        db,
        customerId))
{
    Process(order);
}

The benchmark should distinguish:

Query construction
Query compilation
Query execution
Materialization

Otherwise, results can be misleading.

Test Tracking Versus No Tracking

EF Core's change tracker introduces application-side work.

Compare:

var orders = await db.Orders
    .ToListAsync();

with:

var orders = await db.Orders
    .AsNoTracking()
    .ToListAsync();

The generated SQL may be similar, but materialization and memory behavior can differ.

When evaluating query translation changes, isolate database execution from object materialization where possible.

Otherwise, you may attribute a materialization difference to the SQL translator.

Benchmark Cold and Warm Queries

A query can have different characteristics on its first execution.

Test:

Cold
First execution

Warm
Repeated execution

For example:

await query.ToListAsync();
await query.ToListAsync();
await query.ToListAsync();

Do not mix first-execution and steady-state numbers without identifying them.

EF Core caches several pieces of infrastructure, including query-related metadata and compiled query information, so warm execution can behave differently from cold execution.

Build a Query Regression Suite

For a production application, create a small set of critical queries.

For example:

Customer search
Order history
Product catalog
Inventory lookup
Reporting query
Dashboard aggregation
Audit search

For each query, store:

LINQ definition
Expected result
Generated SQL
Execution plan
Baseline measurements
Relevant indexes

After an EF Core upgrade:

Run suite
   ↓
Compare SQL
   ↓
Compare execution plans
   ↓
Compare performance
   ↓
Investigate meaningful differences

This turns ORM upgrades into a measurable engineering process.

Common Mistakes

Comparing Only LINQ Source Code

The LINQ expression may remain unchanged while generated SQL changes.

Always inspect the generated SQL.

Comparing Only SQL Text

SQL text is not the execution plan.

Run the generated SQL against the target database and inspect the actual execution behavior.

Using Tiny Test Databases

Small datasets hide cardinality and indexing problems.

Use production-like data volumes where possible.

Changing Database Indexes During the Test

If the objective is to measure EF Core translation, keep the schema constant.

Index experiments should be separate.

Benchmarking With the InMemory Provider

It does not represent relational query translation and database execution.

Ignoring Parameter Distribution

One parameter value may produce an entirely different execution plan or runtime profile from another.

Assuming Newer EF Core Is Always Faster

A newer translator contains improvements, but real workloads determine whether those improvements matter.

Measure.

Troubleshooting

Generated SQL Changed Dramatically

First determine whether the semantics are equivalent.

Compare:

Selected columns
Filters
Joins
Ordering
Grouping
Parameters

Then compare execution plans.

Query Is Now Slower

Check:

  1. Generated SQL.

  2. Execution plan.

  3. Index usage.

  4. Rows examined.

  5. Parameter values.

  6. Database statistics.

  7. Provider version.

Do not immediately revert EF Core without identifying the cause.

Query No Longer Translates

Look for:

  • Unsupported method calls

  • Custom methods

  • Provider-specific expressions

  • New expression shapes

  • Changes in mapping

  • Changes in value converters

EF Core's translation behavior is provider- and version-dependent.

SQL Looks Better but Runtime Is Worse

Trust measured execution rather than appearance.

A visually simpler SQL query can still produce a worse execution plan.

Only One Provider Regresses

This may indicate a provider-specific translation or database optimization issue rather than an EF Core-wide problem.

Create a minimal provider-specific reproduction.

Best Practices

  1. Baseline critical queries before upgrading EF Core.

  2. Keep the database schema and data constant during comparisons.

  3. Inspect generated SQL with ToQueryString().

  4. Compare execution plans, not only SQL text.

  5. Use production-like data volumes.

  6. Test realistic parameter distributions.

  7. Measure database reads and CPU where available.

  8. Test cold and warm execution separately.

  9. Test tracking and no-tracking scenarios separately.

  10. Test the actual database provider.

  11. Keep query-translation tests in CI for critical paths.

  12. Record SQL changes during major EF Core upgrades.

  13. Investigate translation failures separately from performance regressions.

  14. Do not claim an improvement without reproducible measurements.

Frequently Asked Questions

Does EF Core 11 automatically make LINQ queries faster?

No.

EF Core 11 Preview 6 includes LINQ query translation improvements, but the impact depends on the query, database provider, schema, indexes, data distribution, and execution plan.

How can I see SQL generated by EF Core?

Use ToQueryString() for inspecting the SQL representation of an IQueryable:

var sql = query.ToQueryString();

For production diagnostics, configure appropriate EF Core logging as well.

Is generated SQL enough to evaluate performance?

No.

You should also inspect the database execution plan and measure actual execution behavior.

Should I benchmark EF Core with an in-memory database?

Not for relational query-translation performance.

Use the actual database provider and database engine you intend to evaluate.

What should I compare during an EF Core upgrade?

At minimum:

Generated SQL
Execution plan
Elapsed time
Logical reads
CPU
Rows returned
Application allocations

The exact available metrics depend on the database engine.

Can a LINQ query become slower even if its SQL looks simpler?

Yes.

SQL appearance is not a reliable proxy for execution performance. The database optimizer, indexes, statistics, cardinality, and parameter values all influence execution.

What is the most important EF Core upgrade test?

There is no single universal test.

For production systems, the highest-value approach is to identify critical queries and compare their generated SQL, execution plans, and measured performance before and after the upgrade.

Conclusion

EF Core abstracts database access, but it does not eliminate the database execution model.

Every LINQ query eventually crosses a boundary:

LINQ
  ↓
EF Core Translator
  ↓
SQL
  ↓
Database Optimizer
  ↓
Execution Plan
  ↓
Database

.NET 11 Preview 6 introduces LINQ query translation improvements in EF Core and expands how keys and indexes can traverse complex-type properties.

Those changes create opportunities for better query behavior, but the only reliable way to determine their impact is to test real workloads.

A strong EF Core upgrade process therefore looks like this:

Baseline
   ↓
Capture LINQ
   ↓
Capture SQL
   ↓
Run Against Realistic Data
   ↓
Inspect Execution Plan
   ↓
Measure
   ↓
Compare Runtime/ORM Versions
   ↓
Investigate Differences

The key lesson is simple:

Do not evaluate EF Core query translation by looking only at C# code or SQL text. Evaluate the complete path from LINQ expression to database execution plan.

That approach gives developers evidence they can use to decide whether an EF Core upgrade genuinely improves their application's database workload, whether a query needs to be rewritten, or whether an apparent regression is actually caused by indexes, data distribution, provider behavior, or database configuration.