Skip to content

    Back to articles

    6 min read

    Treat richer EF Core translation as a SQL change to verify

    LINQ is only the front of the boundary

    .NET 11 Preview 6 includes EF Core changes to LINQ translation and complex-type modeling. The Preview 6 announcement and EF Core release notes describe, among other things, support for translating full outer joins and for defining keys and indexes that reach into non-collection complex types. With .NET 11’s general release scheduled for November 2026, this is a useful point to examine what those capabilities mean at the database boundary, rather than treating richer LINQ as an automatic improvement. The official .NET support policy lists the November release event.

    I treat a change in query expression as a reason to inspect the command EF Core produces. The expression tree is not the operation the database executes. Translation affects the SQL shape, and the database evaluates that SQL against its schema, indexes, statistics, and data. A query that reads more naturally in C# may still ask the database to do more work, return more rows, or use a plan that is a poor fit for the application’s workload.

    Full outer join support is a good example of why the distinction matters. Being able to express a join directly in LINQ can make the intent clearer than a workaround. But the application still needs the semantics it expects for unmatched rows on both sides, and the provider still needs to translate the expression appropriately. I would review the generated SQL and the results for the cases that matter, not assume that a shorter query is a faster one.

    Check the SQL, then check the plan

    I start by capturing the SQL for the query I am changing. ToQueryString() is useful for inspecting an IQueryable; logging executed commands is useful when I need to see what the application actually sends. These are complementary views: one helps me inspect the translation, and the other helps me understand the query path the application takes.

    var query = context.Customers
        .Where(customer => customer.Address.PostCode == postCode)
        .Select(customer => new CustomerSummary(
            customer.Id,
            customer.Address.PostCode));
    
    var sql = query.ToQueryString();

    I would compare the SQL before and after an EF Core upgrade for important queries, paying attention to joins, predicates, selected columns, and whether the translation still matches the intended result. I would also check how many commands the application executes for the use case, rather than inspecting one query in isolation. A query can look reasonable by itself while the surrounding access pattern repeatedly runs it or loads more data than the caller needs.

    SQL text is not the final verdict. I run the query with representative data and parameters, then inspect the database’s execution plan. Microsoft’s EF Core performance diagnosis guidance recommends examining the plan when investigating a problematic query; the plan reflects how the database intends to execute the statement using the available schema and indexes. I compare meaningful differences between versions, such as access paths, joins, and the amount of work indicated by the plan, rather than treating a changed SQL string as proof of a regression or a win.

    Representative matters. A tiny local database can hide a bad access pattern; a single convenient parameter can miss differences in data distribution. I use data and parameter values that reflect the application’s real reads and writes. If I do not have that representative workload, I treat a result as a preliminary check, not evidence that the change is safe in production.

    Complex types reach into schema decisions

    The modeling change deserves the same scrutiny. EF Core 11 allows keys and indexes to target scalar properties inside non-collection complex types. For example, the complex types documentation shows configuring an index through a nested property:

    modelBuilder.Entity<Customer>()
        .HasIndex(customer => customer.Address.PostCode);

    That is a more direct way to express where the property belongs in the object model. It does not make the database consequence disappear. An index is a schema decision: I want to know which column or mapped value it covers, whether the migration changes the intended table, and whether the index fits the queries that actually use that property. The EF Core indexes documentation explains how indexes are configured and how composite indexes relate to the columns a query filters on.

    I also check the model’s constraints, not just whether the new configuration compiles. The Preview 6 release notes say that properties used in keys or indexes become required, and that optional complex properties in keys are rejected. That behavior can affect whether the model represents the application’s existing rules. If an address is optional in the domain, I would not quietly make a nested address field part of a key just because the API now permits writing that configuration. I would resolve the mismatch deliberately, then inspect the migration and verify that the database and application agree.

    This is where I validate both directions: how the application reads the nested value and how it writes it. A model that makes a query pleasant but misstates nullability, uniqueness, or the ownership of the data is not a better model. I want the complex type to describe the application’s data accurately, and the relational constraints to support the actual access patterns.

    Make the upgrade a workload decision

    For a critical query, I would keep a small review record: the LINQ expression, the generated SQL, the database provider, the relevant migration or model change, the representative parameters and data, and the execution plan before and after. That makes the review about a specific behavior rather than a general impression of the new release.

    I would not benchmark every query just because translation changed. I would prioritize the queries and writes that matter to the application, especially where the new LINQ feature replaces a workaround or where a complex-type property is added to a key or index. Then I would check the actual provider and database used by the application, because the translated command and the database’s plan are the things the workload depends on.

    My rule is simple: accept a richer expression when it makes the model or query clearer, but treat its SQL and schema effects as changes to verify. LINQ describes what I want. The emitted SQL, the plan, and the application’s read-and-write behavior tell me whether that choice fits the system.

    Drafted with AI assistance, reviewed and edited by Ali.