Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 26, 2025

Implements tuple-returning overloads for Join, LeftJoin, and RightJoin that eliminate the need for a resultSelector lambda when you just want the joined elements as a tuple.

Changes

  • System.Linq.Enumerable: Added Join<TOuter, TInner, TKey>, LeftJoin<TOuter, TInner, TKey>, RightJoin<TOuter, TInner, TKey> returning (TOuter Outer, TInner Inner) tuples (with nullable element for outer joins)
  • System.Linq.Queryable: Added corresponding overloads with Expression<Func<>> key selectors
  • System.Linq.AsyncEnumerable: Added overloads for both sync and async key selector variants
  • All methods use a single overload with an optional IEqualityComparer<TKey>? comparer = null parameter

Example

Before:

foreach (var (s, pair) in keys.Join(dict, k => k, p => p.Value, (outer, inner) => (outer, inner)))
    Console.WriteLine(s + " : " + pair.Key);

After:

foreach (var (s, pair) in keys.Join(dict, k => k, p => p.Value))
    Console.WriteLine(s + " : " + pair.Key);

Fixes #108799

Original prompt

This section details on the original issue you should resolve

<issue_title>[API Proposal]: Linq Join return tuple similar to Zip</issue_title>
<issue_description>### Background and motivation

For simplicity of Join it should just return (TOuter,TInner) instead of the need for resultSelector

API Proposal

namespace System.Linq;

public static class Enumerable
{
    public static IEnumerable<(TOuter Outer, TInner Inner)> Join<TOuter, TInner, TKey>(
        this IEnumerable<TOuter> outer,
        IEnumerable<TInner> inner,
        Func<TOuter, TKey> outerKeySelector,
        Func<TInner, TKey> innerKeySelector,
        IEqualityComparer<TKey>? comparer = null);

    public static IEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin<TOuter, TInner, TKey>(
        this IEnumerable<TOuter> outer,
        IEnumerable<TInner> inner,
        Func<TOuter, TKey> outerKeySelector,
        Func<TInner, TKey> innerKeySelector,
        IEqualityComparer<TKey>? comparer = null);

    public static IEnumerable<(TOuter? Outer, TInner Inner)> RightJoin<TOuter, TInner, TKey>(
        this IEnumerable<TOuter> outer,
        IEnumerable<TInner> inner,
        Func<TOuter, TKey> outerKeySelector,
        Func<TInner, TKey> innerKeySelector,
        IEqualityComparer<TKey>? comparer = null);
}

public static class Queryable
{
    public static IQueryable<(TOuter Outer, TInner Inner)> Join<TOuter, TInner, TKey>(
        this IQueryable<TOuter> outer,
        IEnumerable<TInner> inner,
        Expression<Func<TOuter, TKey>> outerKeySelector,
        Expression<Func<TInner, TKey>> innerKeySelector,
        IEqualityComparer<TKey>? comparer = null);

    public static IQueryable<(TOuter Outer, TInner? Inner)> LeftJoin<TOuter, TInner, TKey>(
        this IQueryable<TOuter> outer,
        IEnumerable<TInner> inner,
        Expression<Func<TOuter, TKey>> outerKeySelector,
        Expression<Func<TInner, TKey>> innerKeySelector,
        IEqualityComparer<TKey>? comparer = null);

    public static IQueryable<(TOuter? Outer, TInner Inner)> RightJoin<TOuter, TInner, TKey>(
        this IQueryable<TOuter> outer,
        IEnumerable<TInner> inner,
        Expression<Func<TOuter, TKey>> outerKeySelector,
        Expression<Func<TInner, TKey>> innerKeySelector,
        IEqualityComparer<TKey>? comparer = null);
}

public static class AsyncEnumerable
{
    public static IAsyncEnumerable<(TOuter Outer, TInner Inner)> Join<TOuter, TInner, TKey>(
        this IAsyncEnumerable<TOuter> outer, 
        IAsyncEnumerable<TInner> inner,
        Func<TOuter, CancellationToken, ValueTask<TKey>> outerKeySelector,
        Func<TInner, CancellationToken, ValueTask<TKey>> innerKeySelector,
        IEqualityComparer<TKey>? comparer = null);

    public static IAsyncEnumerable<(TOuter Outer, TInner Inner)> Join<TOuter, TInner, TKey>(
        this IAsyncEnumerable<TOuter> outer,
        IAsyncEnumerable<TInner> inner,
        Func<TOuter, TKey> outerKeySelector,
        Func<TInner, TKey> innerKeySelector,
        IEqualityComparer<TKey>? comparer = null);

    public static IAsyncEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin<TOuter, TInner, TKey>(
        this IAsyncEnumerable<TOuter> outer, 
        IAsyncEnumerable<TInner> inner,
        Func<TOuter, CancellationToken, ValueTask<TKey>> outerKeySelector,
        Func<TInner, CancellationToken, ValueTask<TKey>> innerKeySelector,
        IEqualityComparer<TKey>? comparer = null);

    public static IAsyncEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin<TOuter, TInner, TKey>(
        this IAsyncEnumerable<TOuter> outer,
        IAsyncEnumerable<TInner> inner,
        Func<TOuter, TKey> outerKeySelector,
        Func<TInner, TKey> innerKeySelector,
        IEqualityComparer<TKey>? comparer = null);

    public static IAsyncEnumerable<(TOuter? Outer, TInner Inner)> RightJoin<TOuter, TInner, TKey>(
        this IAsyncEnumerable<TOuter> outer, 
        IAsyncEnumerable<TInner> inner,
        Func<TOuter, CancellationToken, ValueTask<TKey>> outerKeySelector,
        Func<TInner, CancellationToken, ValueTask<TKey>> innerKeySelector,
        IEqualityComparer<TKey>? comparer = null);

    public static IAsyncEnumerable<(TOuter? Outer, TInner Inner)> RightJoin<TOuter, TInner, TKey>(
        this IAsyncEnumerable<TOuter> outer,
        IAsyncEnumerable<TInner> inner,
        Func<TOuter, TKey> outerKeySelector,
        Func<TInner, TKey> innerKeySelector,
        IEqualityComparer<TKey>? comparer = null);
}

API Usage

var keys = new[] { "x" ,"y" ,"z" };
var dict = new Dictionary<int,string>();

foreach (var (s,pair) in keys.Join(dict,(key) => key,(pair) => pair.Value))
    Console.WriteLine(s + " : " pair.Key);

Alternative Designs

Without this it need to make another lambda just for return tuple

foreach (var (s,pair) in keys.Join(dict,(key) => key,(pair) => pair.Value,(outer,inner) => (outer,inner)))
    Console.WriteLine(s + " : " pair.Key);
```...

</details>

- Fixes dotnet/runtime#120596

<!-- START COPILOT CODING AGENT TIPS -->
---Let Copilot coding agent [set things up for you](https://github.com/dotnet/runtime/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 2 commits November 26, 2025 20:10
… AsyncEnumerable

Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
Copilot AI changed the title [WIP] Add Linq Join returning tuple similar to Zip Add Join/LeftJoin/RightJoin tuple overloads to LINQ APIs Nov 26, 2025
Copilot AI requested a review from eiriktsarpalis November 26, 2025 20:27
Copilot finished work on behalf of eiriktsarpalis November 26, 2025 20:27
@eiriktsarpalis eiriktsarpalis requested a review from roji November 27, 2025 12:54
public static System.Collections.Generic.IEnumerable<TSource> Intersect<TSource>(this System.Collections.Generic.IEnumerable<TSource> first, System.Collections.Generic.IEnumerable<TSource> second, System.Collections.Generic.IEqualityComparer<TSource>? comparer) { throw null; }
public static System.Collections.Generic.IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(this System.Collections.Generic.IEnumerable<TOuter> outer, System.Collections.Generic.IEnumerable<TInner> inner, System.Func<TOuter, TKey> outerKeySelector, System.Func<TInner, TKey> innerKeySelector, System.Func<TOuter, TInner, TResult> resultSelector) { throw null; }
public static System.Collections.Generic.IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(this System.Collections.Generic.IEnumerable<TOuter> outer, System.Collections.Generic.IEnumerable<TInner> inner, System.Func<TOuter, TKey> outerKeySelector, System.Func<TInner, TKey> innerKeySelector, System.Func<TOuter, TInner, TResult> resultSelector, System.Collections.Generic.IEqualityComparer<TKey>? comparer) { throw null; }
public static System.Collections.Generic.IEnumerable<(TOuter Outer, TInner Inner)> Join<TOuter, TInner, TKey>(this System.Collections.Generic.IEnumerable<TOuter> outer, System.Collections.Generic.IEnumerable<TInner> inner, System.Func<TOuter, TKey> outerKeySelector, System.Func<TInner, TKey> innerKeySelector) { throw null; }
Copy link
Member

Choose a reason for hiding this comment

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

@copilot Please remove this overload, replacing it with a single overload that has an optional IEqualitComparer parameter.

Copy link
Member

Choose a reason for hiding this comment

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

@copilot check that this is the case with all overloads across projects as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in 7f53b70. Consolidated the two overloads into a single overload with IEqualityComparer<TKey>? comparer = null for all Join, LeftJoin, and RightJoin tuple-returning methods in Enumerable and Queryable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Verified - all tuple-returning overloads across all three projects use the single overload with optional IEqualityComparer<TKey>? comparer = null pattern:

  • System.Linq: Join, LeftJoin, RightJoin (1 overload each)
  • System.Linq.Queryable: Join, LeftJoin, RightJoin (1 overload each)
  • System.Linq.AsyncEnumerable: Join, LeftJoin, RightJoin (2 overloads each - one for sync key selectors, one for async key selectors, both with comparer = null)

…omparer parameter

Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com>
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.

2 participants