Skip to content

Fix unnecessary joins in consecutive Select projections with conditionals#37601

Merged
AndriySvyryd merged 12 commits into
mainfrom
copilot/fix-parameter-null-check-issue
Jun 12, 2026
Merged

Fix unnecessary joins in consecutive Select projections with conditionals#37601
AndriySvyryd merged 12 commits into
mainfrom
copilot/fix-parameter-null-check-issue

Conversation

Copilot AI commented Jan 31, 2026

Copy link
Copy Markdown
Contributor

Consecutive .Select() operations with conditional null checks were projecting all properties from earlier selects, generating unnecessary joins and columns in SQL.

users
    .Select(x => new { 
        x.Id, 
        Job = x.Job == null ? null : new { 
            x.Job.Id, 
            Address = new { x.Job.Address.Id, x.Job.Address.Street } 
        } 
    })
    .Select(x => new { 
        x.Id, 
        Job = x.Job == null ? null : new { x.Job.Id }  // Only accessing Id
    })

Generated SQL incorrectly included Address table joins and columns despite the second select only referencing Job.Id.

Changes

Core Fix (NavigationExpandingExpressionVisitor.ExpressionVisitors.cs)

  • Added NavigationTreeMemberPruningVisitor that rewrites composed selectors before navigation expansion using three rules:
    1. (new { A = a, B = b }).A → a — prunes unused branches from anonymous-type projections
    2. (test ? null : ifFalse).M → ifFalse.M — folds member access through null-safe conditionals
    3. (test ? null : new { M = ... }) == null → test — simplifies null checks on conditional new-expressions

Tests

  • Added Consecutive_selects_with_conditional_projection_should_not_include_unnecessary_joins — verifies generated SQL excludes unreferenced navigation joins
  • Added Consecutive_selects_with_conditional_projection_null_navigation_returns_null — verifies null navigation is correctly returned as null
  • Added Consecutive_selects_with_conditional_projection_nested_navigation_accessed_includes_join — verifies referenced navigations still produce their joins
  • Strengthened Consecutive_selects_with_conditional_projection_should_not_include_unnecessary_joins with materialization assertions for result.Job and result.Job.Id
  • Added SQL Server functional test overrides with SQL baselines for all three consecutive conditional projection tests to validate provider-specific generated SQL shape
Original prompt

This section details on the original issue you should resolve

<issue_title>Checking parameter for null causes incorrect projections translation</issue_title>
<issue_description>### Bug description

This is a copy of npgsql/efcore.pg#3295

Hello,

When checking a joined entity for NULL the further projecting of joined table does not take effect causing always selecting all fields and all subsequent joins if any.

Suppose having following entity hierarchy: User -> Job? -> Address

class User {
  public long Id { get; set; }
  public long? JobId { get; set; }
  // other unrelated properties
}

class Job {
 public long Id { get; set; }
 public long AddressId { get; set; }
 // other unrelated properties
}

class Address {
 public long Id { get; set; }
 // other unrelated properties
}

The following linq query

        var claim = await users
            .Select(x => new
            {
                x.Id,
                Job = x.Job == null ? null : new
                {
                    x.Job.Id,
                    Address = new
                    {
                        x.Job.Address.Id,
                        x.Job.Address.Street
                    }
                }
            })
            .Select(x => new
            {
                x.Id,
                Job = x.Job == null ? null : new
                {
                    x.Job.Id
                }
            })
            .Where(x => x.Id == 14501)
            .FirstOrDefaultAsync();

produces this sql:

SELECT c."Id", c0."Id", FALSE, c1."Id", c1."Street"
FROM "User" AS c
INNER JOIN "Job" AS c0 ON c."JobId" = c0."Id"
INNER JOIN "Address" AS c1 ON c0."AddressId" = c1."Id"
WHERE c."Id" = 14501

It includes join to Address table, even though it is not selected in the last Select method.

You may ask why doing consequent selects, the answer is the part before second select may come from elsewhere as IQueryable which you want to further narrow down.

Anyways, is that expected? And is it possible to workaround?

Your code

see description

Stack traces


Verbose output


EF Core version

9.0.0

Database provider

Npgsql.EntityFrameworkCore.PostgreSQL.9.0.1

Target framework

No response

Operating system

MAC OS

IDE

Jetbrains Rider</issue_description>

Comments on the Issue (you are @copilot in this section)

@cincuranet Small repro:
using Microsoft.EntityFrameworkCore;

using var db = new MyContext();
Console.WriteLine(db.Users
    .Select(x => new
    {
        x.Id,
        Job = x.Job == null ? null : new
        {
            x.Job.Id,
            Address = new
            {
                x.Job.Address.Id,
                x.Job.Address.Street
            }
        }
    })
    .Select(x => new
    {
        x.Id,
        Job = x.Job == null ? null : new
        {
            x.Job.Id
        }
    })
    .Where(x => x.Id == 14501)
    .ToQueryString());

class MyContext : DbContext
{
    public DbSet<User> Users { get; set; } = null!;

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite();
    }
}
class User
{
    public long Id { get; set; }
    public long? JobId { get; set; }
    public Job? Job { get; set; }
}
class Job
{
    public long Id { get; set; }
    public long AddressId { get; set; }
    public Address Address { get; set; } = null!;
}
class Address
{
    public long Id { get; set; }
    public string Street { get; set; } = null!;
}

BTW, removing the second conditional (the x.Job == null ? null : new) "fixes" it. </comment_new>


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 5 commits January 31, 2026 21:46
Co-authored-by: roji <1862641+roji@users.noreply.github.com>
Co-authored-by: roji <1862641+roji@users.noreply.github.com>
Co-authored-by: roji <1862641+roji@users.noreply.github.com>
Co-authored-by: roji <1862641+roji@users.noreply.github.com>
When two consecutive Select() operations are applied with conditional expressions,
the second Select was including unnecessary joins and columns from the first Select.

Root cause: In IncludeExpandingExpressionVisitor.VisitMember, when accessing a
specific member of a NavigationTreeExpression with a NewExpression (anonymous type),
the code was reconstructing the entire anonymous type instead of just extracting
the accessed member.

Fix: Modified VisitMember to detect when a member is accessed on a NavigationTreeExpression
with a NewExpression value, and directly visit only the specific argument corresponding to
that member, avoiding reconstruction of the entire anonymous type.

This ensures that expressions like:
  .Select(x => new { x.Job.Id, x.Job.Address.Street })
  .Select(x => new { x.Job.Id })

Only include Job.Id in the final SQL, not Address properties.

Fixes #XXXXX

Co-authored-by: roji <1862641+roji@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix parameter check causing incorrect projections translation Fix unnecessary joins in consecutive Select projections with conditionals Jan 31, 2026
Copilot AI requested a review from roji January 31, 2026 22:22
@AndriySvyryd AndriySvyryd assigned AndriySvyryd and unassigned roji and Copilot Jun 10, 2026
@AndriySvyryd AndriySvyryd requested a review from Copilot June 11, 2026 00:58
Comment thread src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses a query translation inefficiency where consecutive Select() projections combined with conditional null checks would cause EF Core to keep projecting earlier navigation members, resulting in unnecessary joins/columns in generated SQL. The change is implemented in the navigation-expansion pipeline and validated with a new relational test plus a SQLite SQL baseline to ensure the unreferenced navigation join is eliminated.

Changes:

  • Adjust navigation expansion/member access handling to avoid expanding/reconstructing entire anonymous-type navigation shapes when only a specific member is accessed.
  • Add a new relational specification test covering consecutive conditional projections.
  • Add a SQLite functional test override asserting the absence of the unnecessary join in generated SQL.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs Adds/changes visitors to prune navigation-tree member expansions so later projections don’t carry unused navigation joins.
src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs Runs the new pruning visitor during Select processing.
test/EFCore.Relational.Specification.Tests/Query/AdHocNavigationsQueryRelationalTestBase.cs Adds a new relational spec test reproducing the consecutive conditional projection scenario.
test/EFCore.Sqlite.FunctionalTests/Query/AdHocNavigationsQuerySqliteTest.cs Adds SQL assertion ensuring the generated query doesn’t include the unnecessary join.

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…tests

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
@AndriySvyryd AndriySvyryd removed their assignment Jun 11, 2026
@AndriySvyryd AndriySvyryd requested a review from Copilot June 11, 2026 19:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
Copilot AI requested a review from AndriySvyryd June 11, 2026 21:52
@AndriySvyryd AndriySvyryd marked this pull request as ready for review June 12, 2026 00:22
@AndriySvyryd AndriySvyryd requested a review from a team as a code owner June 12, 2026 00:22
Copilot AI review requested due to automatic review settings June 12, 2026 00:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 12, 2026 01:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@AndriySvyryd AndriySvyryd merged commit 14e4677 into main Jun 12, 2026
14 checks passed
@AndriySvyryd AndriySvyryd deleted the copilot/fix-parameter-null-check-issue branch June 12, 2026 01:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Checking parameter for null causes incorrect projections translation

4 participants