Skip to content

Translate Join/LeftJoin/RightJoin/FullJoin overloads without a result selector - #39612

Open
officialasishkumar wants to merge 1 commit into
dotnet:mainfrom
officialasishkumar:feature/38047-tuple-join-overloads
Open

officialasishkumar wants to merge 1 commit into
dotnet:mainfrom
officialasishkumar:feature/38047-tuple-join-overloads

Conversation

@officialasishkumar

Copy link
Copy Markdown
Contributor

Fixes #38047

.NET 11 added Queryable.Join, LeftJoin, RightJoin and FullJoin overloads without a result selector, which return (TOuter Outer, TInner Inner) tuples (dotnet/runtime#120596). EF didn't recognize them, so a query like the following failed with a translation error:

var results = await context.Customers
    .LeftJoin(context.Orders, c => c.CustomerID, o => o.CustomerID)
    .Where(t => t.Inner.OrderDate > someDate)
    .Select(t => new { t.Outer.ContactName, t.Inner.OrderID })
    .ToListAsync();

Change

QueryableMethodNormalizingExpressionVisitor now rewrites these overloads to the existing overloads with a result selector that creates the tuple, next to the MinBy/MaxBy normalization:

outer.Join(inner, o => o.Key, i => i.Key)
// becomes
outer.Join(inner, o => o.Key, i => i.Key, (o, i) => new ValueTuple<TOuter, TInner>(o, i))

The same applies to LeftJoin, RightJoin and FullJoin. The rest of the pipeline already handles this shape, so every provider that supports the corresponding join gets the translation:

SELECT [c].[ContactName], [o].[OrderID]
FROM [Customers] AS [c]
INNER JOIN [Orders] AS [o] ON [c].[CustomerID] = [o].[CustomerID]
WHERE [c].[City] = N'London' AND [o].[OrderID] < 10500
  • Outer and Inner are Item1 and Item2 of the ValueTuple, so operators composed after the join are folded back to the joined sources through the ValueTuple member folding in ReplacingExpressionVisitor (Fold ValueTuple/Tuple member access through constructor-bound projections #38560).
  • The Enumerable overloads, for example over a collection navigation inside a projection, are converted to the Queryable ones by the same visitor and are rewritten the same way.
  • A custom comparer isn't supported, as for the existing join overloads (including FullJoin): the call is left unchanged and fails translation.
  • QueryableMethods gets JoinWithoutResultSelector, LeftJoinWithoutResultSelector, RightJoinWithoutResultSelector and FullJoinWithoutResultSelector, following the existing ...WithoutPredicate/...WithoutSelector naming, with their API baseline entries.

The selectorless GroupJoin overload from the same .NET release (dotnet/runtime#120587) isn't covered: it returns IGrouping and, like GroupJoin in general, needs different handling.

Tests

New tests in NorthwindJoinQueryTestBase: each of the four overloads (comparing the materialized Outer/Inner entities), Where/Select composed over the tuple, the Enumerable overload over a collection navigation, and a custom comparer (translation failure). They come with SQL Server baselines. On InMemory the right and full join tests assert a translation failure, like the existing RightJoin/FullJoin tests there.

Without the change, all of them except the comparer test fail with a translation error. With it, I ran the Microsoft.EntityFrameworkCore.Query tests on SQLite (17691 passed, 82 skipped) and InMemory (9414 passed, 43 skipped), the EFCore.Tests query tests, and EFCoreApiBaselineTest. I couldn't run a SQL Server instance on my machine's architecture, so I generated the SQL Server baselines with ToQueryString() on NorthwindSqlServerContext for the same queries. For the matching existing tests (LeftJoin, RightJoin_with_filtered_outer, FullJoin_with_unmatched_rows_on_both_sides), this produces SQL identical to their committed baselines. The SQL Server test project builds.

  • I've read the guidelines for contributing and seen the walkthrough
  • I've posted a comment on an issue with a detailed description of how I am planning to contribute and got approval from a member of the team
  • The code builds and tests pass locally (also verified by our automated build checks)
  • Commit messages follow this format:
        Summary of the changes
        - Detail 1
        - Detail 2

        Fixes #bugnumber
  • Tests for the changes have been added (for bug fixes / features)
  • Code follows the same patterns and style as existing code in this repo

… selector

.NET 11 added Queryable.Join, LeftJoin, RightJoin and FullJoin overloads that
take no result selector and return (TOuter Outer, TInner Inner) tuples. EF
didn't recognize them, so queries using them failed translation.

- Add QueryableMethods.JoinWithoutResultSelector, LeftJoinWithoutResultSelector,
  RightJoinWithoutResultSelector and FullJoinWithoutResultSelector.
- In QueryableMethodNormalizingExpressionVisitor, rewrite a call to one of these
  overloads into the corresponding overload with a result selector that creates
  the ValueTuple: (o, i) => new ValueTuple<TOuter, TInner>(o, i). Accesses to the
  tuple's Outer/Inner members later in the query are folded back to the joined
  sources, so composing operators over the tuple translates as well.
- A custom comparer isn't supported, as for the existing join overloads; such a
  call is left as is and fails translation.
- Add specification tests for each overload, for composing over the tuple, for
  the Enumerable overload over a collection navigation and for a custom
  comparer, with SQL Server baselines and InMemory overrides for the right/full
  joins it doesn't support.

Fixes dotnet#38047
@officialasishkumar
officialasishkumar requested a review from a team as a code owner November 12, 2026 08:03
Copilot AI balanced review requested due to automatic review settings November 12, 2026 08:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add query translation support for new tuple-returning Join overloads (without a result selector)

2 participants