From dc8035abe3222fed525149b86f5edd21e6e90f46 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Apr 2026 13:20:09 +0000 Subject: [PATCH 1/4] Add Join/LeftJoin/RightJoin tuple overloads to LINQ APIs (#121998) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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`, `LeftJoin`, `RightJoin` returning `(TOuter Outer, TInner Inner)` tuples (with nullable element for outer joins) - **System.Linq.Queryable**: Added corresponding overloads with `Expression>` 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? comparer = null` parameter ## Example Before: ```csharp foreach (var (s, pair) in keys.Join(dict, k => k, p => p.Value, (outer, inner) => (outer, inner))) Console.WriteLine(s + " : " + pair.Key); ``` After: ```csharp foreach (var (s, pair) in keys.Join(dict, k => k, p => p.Value)) Console.WriteLine(s + " : " + pair.Key); ``` Fixes #120596
Original prompt ---- *This section details on the original issue you should resolve* [API Proposal]: Linq Join return tuple similar to Zip ### Background and motivation For simplicity of `Join` it should just return `(TOuter,TInner)` instead of the need for `resultSelector` ### API Proposal ```csharp namespace System.Linq; public static class Enumerable { public static IEnumerable<(TOuter Outer, TInner Inner)> Join( this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null); public static IEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin( this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null); public static IEnumerable<(TOuter? Outer, TInner Inner)> RightJoin( this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null); } public static class Queryable { public static IQueryable<(TOuter Outer, TInner Inner)> Join( this IQueryable outer, IEnumerable inner, Expression> outerKeySelector, Expression> innerKeySelector, IEqualityComparer? comparer = null); public static IQueryable<(TOuter Outer, TInner? Inner)> LeftJoin( this IQueryable outer, IEnumerable inner, Expression> outerKeySelector, Expression> innerKeySelector, IEqualityComparer? comparer = null); public static IQueryable<(TOuter? Outer, TInner Inner)> RightJoin( this IQueryable outer, IEnumerable inner, Expression> outerKeySelector, Expression> innerKeySelector, IEqualityComparer? comparer = null); } public static class AsyncEnumerable { public static IAsyncEnumerable<(TOuter Outer, TInner Inner)> Join( this IAsyncEnumerable outer, IAsyncEnumerable inner, Func> outerKeySelector, Func> innerKeySelector, IEqualityComparer? comparer = null); public static IAsyncEnumerable<(TOuter Outer, TInner Inner)> Join( this IAsyncEnumerable outer, IAsyncEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null); public static IAsyncEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin( this IAsyncEnumerable outer, IAsyncEnumerable inner, Func> outerKeySelector, Func> innerKeySelector, IEqualityComparer? comparer = null); public static IAsyncEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin( this IAsyncEnumerable outer, IAsyncEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null); public static IAsyncEnumerable<(TOuter? Outer, TInner Inner)> RightJoin( this IAsyncEnumerable outer, IAsyncEnumerable inner, Func> outerKeySelector, Func> innerKeySelector, IEqualityComparer? comparer = null); public static IAsyncEnumerable<(TOuter? Outer, TInner Inner)> RightJoin( this IAsyncEnumerable outer, IAsyncEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null); } ``` ### API Usage ```csharp var keys = new[] { "x" ,"y" ,"z" }; var dict = new Dictionary(); 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 ```C# foreach (var (s,pair) in keys.Join(dict,(key) => key,(pair) => pair.Value,(outer,inner) => (outer,inner))) Console.WriteLine(s + " : " pair.Key); ```...
- Fixes dotnet/runtime#120596 --- ✨ 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. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com> Co-authored-by: Shay Rojansky Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Eirik Tsarpalis (cherry picked from commit 2916d73755eb875a8803dfbb4d3a72a29cc6bd9b) --- .../ref/System.Linq.AsyncEnumerable.cs | 6 + .../src/System/Linq/Join.cs | 135 ++++++++++++++++++ .../src/System/Linq/LeftJoin.cs | 131 +++++++++++++++++ .../src/System/Linq/RightJoin.cs | 132 +++++++++++++++++ .../tests/JoinTests.cs | 78 ++++++++++ .../tests/LeftJoinTests.cs | 80 +++++++++++ .../tests/RightJoinTests.cs | 80 +++++++++++ .../ref/System.Linq.Queryable.cs | 3 + .../src/System/Linq/Queryable.cs | 84 +++++++++++ .../System.Linq.Queryable/tests/JoinTests.cs | 90 ++++++++++++ .../tests/LeftJoinTests.cs | 90 ++++++++++++ .../tests/RightJoinTests.cs | 90 ++++++++++++ src/libraries/System.Linq/ref/System.Linq.cs | 3 + .../System.Linq/src/System/Linq/Join.cs | 78 ++++++++++ .../System.Linq/src/System/Linq/LeftJoin.cs | 80 +++++++++++ .../System.Linq/src/System/Linq/RightJoin.cs | 80 +++++++++++ src/libraries/System.Linq/tests/JoinTests.cs | 104 ++++++++++++++ .../System.Linq/tests/LeftJoinTests.cs | 94 ++++++++++++ .../System.Linq/tests/RightJoinTests.cs | 95 ++++++++++++ 19 files changed, 1533 insertions(+) diff --git a/src/libraries/System.Linq.AsyncEnumerable/ref/System.Linq.AsyncEnumerable.cs b/src/libraries/System.Linq.AsyncEnumerable/ref/System.Linq.AsyncEnumerable.cs index 881b679a19f3ae..36d3fb6f1efa5e 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/ref/System.Linq.AsyncEnumerable.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/ref/System.Linq.AsyncEnumerable.cs @@ -81,6 +81,8 @@ public static partial class AsyncEnumerable public static System.Collections.Generic.IAsyncEnumerable Intersect(this System.Collections.Generic.IAsyncEnumerable first, System.Collections.Generic.IAsyncEnumerable second, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable Join(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func> outerKeySelector, System.Func> innerKeySelector, System.Func> resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable Join(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable<(TOuter Outer, TInner Inner)> Join(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func> outerKeySelector, System.Func> innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable<(TOuter Outer, TInner Inner)> Join(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Threading.Tasks.ValueTask LastAsync(this System.Collections.Generic.IAsyncEnumerable source, System.Func predicate, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Threading.Tasks.ValueTask LastAsync(this System.Collections.Generic.IAsyncEnumerable source, System.Func> predicate, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Threading.Tasks.ValueTask LastAsync(this System.Collections.Generic.IAsyncEnumerable source, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } @@ -92,6 +94,8 @@ public static partial class AsyncEnumerable public static System.Threading.Tasks.ValueTask LastOrDefaultAsync(this System.Collections.Generic.IAsyncEnumerable source, TSource defaultValue, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Collections.Generic.IAsyncEnumerable LeftJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func> outerKeySelector, System.Func> innerKeySelector, System.Func> resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable LeftJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func> outerKeySelector, System.Func> innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Threading.Tasks.ValueTask LongCountAsync(this System.Collections.Generic.IAsyncEnumerable source, System.Func predicate, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Threading.Tasks.ValueTask LongCountAsync(this System.Collections.Generic.IAsyncEnumerable source, System.Func> predicate, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Threading.Tasks.ValueTask LongCountAsync(this System.Collections.Generic.IAsyncEnumerable source, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } @@ -114,6 +118,8 @@ public static partial class AsyncEnumerable public static System.Collections.Generic.IAsyncEnumerable Reverse(this System.Collections.Generic.IAsyncEnumerable source) { throw null; } public static System.Collections.Generic.IAsyncEnumerable RightJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func> outerKeySelector, System.Func> innerKeySelector, System.Func> resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable RightJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable<(TOuter? Outer, TInner Inner)> RightJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func> outerKeySelector, System.Func> innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable<(TOuter? Outer, TInner Inner)> RightJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable SelectMany(this System.Collections.Generic.IAsyncEnumerable source, System.Func> selector) { throw null; } public static System.Collections.Generic.IAsyncEnumerable SelectMany(this System.Collections.Generic.IAsyncEnumerable source, System.Func> selector) { throw null; } public static System.Collections.Generic.IAsyncEnumerable SelectMany(this System.Collections.Generic.IAsyncEnumerable source, System.Func> selector) { throw null; } diff --git a/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/Join.cs b/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/Join.cs index eb086126386a30..ba67fe31b7392c 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/Join.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/Join.cs @@ -156,5 +156,140 @@ static async IAsyncEnumerable Impl( } } } + + /// Correlates the elements of two sequences based on matching keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to use to hash and compare keys. + /// + /// An that has elements of type (TOuter Outer, TInner Inner) + /// that are obtained by performing an inner join on two sequences. + /// + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable<(TOuter Outer, TInner Inner)> Join( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return + outer.IsKnownEmpty() || inner.IsKnownEmpty() ? Empty<(TOuter Outer, TInner Inner)>() : + Impl(outer, inner, outerKeySelector, innerKeySelector, comparer, default); + + static async IAsyncEnumerable<(TOuter Outer, TInner Inner)> Impl( + IAsyncEnumerable outer, IAsyncEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + IEqualityComparer? comparer, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await using IAsyncEnumerator e = outer.GetAsyncEnumerator(cancellationToken); + + if (await e.MoveNextAsync()) + { + AsyncLookup lookup = await AsyncLookup.CreateForJoinAsync(inner, innerKeySelector, comparer, cancellationToken); + if (lookup.Count != 0) + { + do + { + TOuter item = e.Current; + Grouping? g = lookup.GetGrouping(outerKeySelector(item), create: false); + if (g is not null) + { + int count = g._count; + TInner[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return (item, elements[i]); + } + } + } + while (await e.MoveNextAsync()); + } + } + } + } + + /// Correlates the elements of two sequences based on matching keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to use to hash and compare keys. + /// + /// An that has elements of type (TOuter Outer, TInner Inner) + /// that are obtained by performing an inner join on two sequences. + /// + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable<(TOuter Outer, TInner Inner)> Join( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func> outerKeySelector, + Func> innerKeySelector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return + outer.IsKnownEmpty() || inner.IsKnownEmpty() ? Empty<(TOuter Outer, TInner Inner)>() : + Impl(outer, inner, outerKeySelector, innerKeySelector, comparer, default); + + static async IAsyncEnumerable<(TOuter Outer, TInner Inner)> Impl( + IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func> outerKeySelector, + Func> innerKeySelector, + IEqualityComparer? comparer, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await using IAsyncEnumerator e = outer.GetAsyncEnumerator(cancellationToken); + + if (await e.MoveNextAsync()) + { + AsyncLookup lookup = await AsyncLookup.CreateForJoinAsync(inner, innerKeySelector, comparer, cancellationToken); + if (lookup.Count != 0) + { + do + { + TOuter item = e.Current; + Grouping? g = lookup.GetGrouping(await outerKeySelector(item, cancellationToken), create: false); + if (g is not null) + { + int count = g._count; + TInner[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return (item, elements[i]); + } + } + } + while (await e.MoveNextAsync()); + } + } + } + } } } diff --git a/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/LeftJoin.cs b/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/LeftJoin.cs index a86645ece88d69..8438bdaac6963f 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/LeftJoin.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/LeftJoin.cs @@ -152,5 +152,136 @@ static async IAsyncEnumerable Impl( } } } + + /// Correlates the elements of two sequences based on matching keys. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to use to hash and compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter Outer, TInner? Inner) that are obtained by performing a left outer join on two sequences. + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return + outer.IsKnownEmpty() ? Empty<(TOuter Outer, TInner? Inner)>() : + Impl(outer, inner, outerKeySelector, innerKeySelector, comparer, default); + + static async IAsyncEnumerable<(TOuter Outer, TInner? Inner)> Impl( + IAsyncEnumerable outer, IAsyncEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + IEqualityComparer? comparer, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await using IAsyncEnumerator e = outer.GetAsyncEnumerator(cancellationToken); + + if (await e.MoveNextAsync()) + { + AsyncLookup innerLookup = await AsyncLookup.CreateForJoinAsync(inner, innerKeySelector, comparer, cancellationToken); + do + { + TOuter item = e.Current; + Grouping? g = innerLookup.GetGrouping(outerKeySelector(item), create: false); + if (g is null) + { + yield return (item, default); + } + else + { + int count = g._count; + TInner[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return (item, elements[i]); + } + } + } + while (await e.MoveNextAsync()); + } + } + } + + /// Correlates the elements of two sequences based on matching keys. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to use to hash and compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter Outer, TInner? Inner) that are obtained by performing a left outer join on two sequences. + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func> outerKeySelector, + Func> innerKeySelector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return + outer.IsKnownEmpty() ? Empty<(TOuter Outer, TInner? Inner)>() : + Impl(outer, inner, outerKeySelector, innerKeySelector, comparer, default); + + static async IAsyncEnumerable<(TOuter Outer, TInner? Inner)> Impl( + IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func> outerKeySelector, + Func> innerKeySelector, + IEqualityComparer? comparer, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await using IAsyncEnumerator e = outer.GetAsyncEnumerator(cancellationToken); + + if (await e.MoveNextAsync()) + { + AsyncLookup innerLookup = await AsyncLookup.CreateForJoinAsync(inner, innerKeySelector, comparer, cancellationToken); + do + { + TOuter item = e.Current; + Grouping? g = innerLookup.GetGrouping(await outerKeySelector(item, cancellationToken), create: false); + if (g is null) + { + yield return (item, default); + } + else + { + int count = g._count; + TInner[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return (item, elements[i]); + } + } + } + while (await e.MoveNextAsync()); + } + } + } } } diff --git a/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/RightJoin.cs b/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/RightJoin.cs index 9328acb4597abc..6e83ddccad533c 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/RightJoin.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/RightJoin.cs @@ -153,5 +153,137 @@ static async IAsyncEnumerable Impl( } } } + + /// Correlates the elements of two sequences based on matching keys. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to use to hash and compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter? Outer, TInner Inner) that are obtained by performing a right outer join on two sequences. + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable<(TOuter? Outer, TInner Inner)> RightJoin( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return + inner.IsKnownEmpty() ? Empty<(TOuter? Outer, TInner Inner)>() : + Impl(outer, inner, outerKeySelector, innerKeySelector, comparer, default); + + static async IAsyncEnumerable<(TOuter? Outer, TInner Inner)> Impl( + IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + IEqualityComparer? comparer, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await using IAsyncEnumerator e = inner.GetAsyncEnumerator(cancellationToken); + + if (await e.MoveNextAsync()) + { + AsyncLookup outerLookup = await AsyncLookup.CreateForJoinAsync(outer, outerKeySelector, comparer, cancellationToken); + do + { + TInner item = e.Current; + Grouping? g = outerLookup.GetGrouping(innerKeySelector(item), create: false); + if (g is null) + { + yield return (default, item); + } + else + { + int count = g._count; + TOuter[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return (elements[i], item); + } + } + } + while (await e.MoveNextAsync()); + } + } + } + + /// Correlates the elements of two sequences based on matching keys. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to use to hash and compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter? Outer, TInner Inner) that are obtained by performing a right outer join on two sequences. + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable<(TOuter? Outer, TInner Inner)> RightJoin( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func> outerKeySelector, + Func> innerKeySelector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return + inner.IsKnownEmpty() ? Empty<(TOuter? Outer, TInner Inner)>() : + Impl(outer, inner, outerKeySelector, innerKeySelector, comparer, default); + + static async IAsyncEnumerable<(TOuter? Outer, TInner Inner)> Impl( + IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func> outerKeySelector, + Func> innerKeySelector, + IEqualityComparer? comparer, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await using IAsyncEnumerator e = inner.GetAsyncEnumerator(cancellationToken); + + if (await e.MoveNextAsync()) + { + AsyncLookup outerLookup = await AsyncLookup.CreateForJoinAsync(outer, outerKeySelector, comparer, cancellationToken); + do + { + TInner item = e.Current; + Grouping? g = outerLookup.GetGrouping(await innerKeySelector(item, cancellationToken), create: false); + if (g is null) + { + yield return (default, item); + } + else + { + int count = g._count; + TOuter[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return (elements[i], item); + } + } + } + while (await e.MoveNextAsync()); + } + } + } } } diff --git a/src/libraries/System.Linq.AsyncEnumerable/tests/JoinTests.cs b/src/libraries/System.Linq.AsyncEnumerable/tests/JoinTests.cs index 258ea44c57ba49..d84c6cf05f7f2d 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/tests/JoinTests.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/tests/JoinTests.cs @@ -176,5 +176,83 @@ public async Task InterfaceCalls_ExpectedCounts() Assert.Equal(4, inner.CurrentCount); Assert.Equal(1, inner.DisposeAsyncCount); } + + [Fact] + public void TupleJoin_InvalidInputs_Throws() + { + AssertExtensions.Throws("outer", () => AsyncEnumerable.Join((IAsyncEnumerable)null, AsyncEnumerable.Empty(), outer => outer, inner => inner)); + AssertExtensions.Throws("inner", () => AsyncEnumerable.Join(AsyncEnumerable.Empty(), (IAsyncEnumerable)null, outer => outer, inner => inner)); + AssertExtensions.Throws("outerKeySelector", () => AsyncEnumerable.Join(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), (Func)null, inner => inner)); + AssertExtensions.Throws("innerKeySelector", () => AsyncEnumerable.Join(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), outer => outer, (Func)null)); + + AssertExtensions.Throws("outer", () => AsyncEnumerable.Join((IAsyncEnumerable)null, AsyncEnumerable.Empty(), async (outer, ct) => outer, async (inner, ct) => inner)); + AssertExtensions.Throws("inner", () => AsyncEnumerable.Join(AsyncEnumerable.Empty(), (IAsyncEnumerable)null, async (outer, ct) => outer, async (inner, ct) => inner)); + AssertExtensions.Throws("outerKeySelector", () => AsyncEnumerable.Join(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), (Func>)null, async (inner, ct) => inner)); + AssertExtensions.Throws("innerKeySelector", () => AsyncEnumerable.Join(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), async (outer, ct) => outer, (Func>)null)); + } + + [Fact] + public void TupleJoin_Empty_ProducesEmpty() + { + IAsyncEnumerable empty = AsyncEnumerable.Empty(); + IAsyncEnumerable nonEmpty = CreateSource("1", "2", "3"); + + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.Join(empty, s => s, s => s)); + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.Join(empty, async (s, ct) => s, async (s, ct) => s)); + + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), nonEmpty.Join(empty, s => s, s => s)); + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), nonEmpty.Join(empty, async (s, ct) => s, async (s, ct) => s)); + + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.Join(nonEmpty, s => s, s => s)); + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.Join(nonEmpty, async (s, ct) => s, async (s, ct) => s)); + } + + [Fact] + public async Task TupleJoin_VariousValues_MatchesEnumerable_String() + { + Random rand = new(42); + foreach (int length in new[] { 0, 1, 2, 1000 }) + { + string[] values = new string[length]; + FillRandom(rand, values); + + foreach (IAsyncEnumerable source in CreateSources(values)) + { + await AssertEqual( + values.Join(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner), + source.Join(source, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner)); + + await AssertEqual( + values.Join(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner), + source.Join(source, async (s, ct) => s.Length > 0 ? s[0] : ' ', async (s, ct) => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner)); + } + } + } + + [Fact] + public async Task TupleJoin_InterfaceCalls_ExpectedCounts() + { + TrackingAsyncEnumerable outer, inner; + + outer = CreateSource(2, 4, 8, 16).Track(); + inner = CreateSource(1, 2, 3, 4).Track(); + await ConsumeAsync(outer.Join(inner, outer => outer, inner => inner)); + Assert.Equal(5, outer.MoveNextAsyncCount); + Assert.Equal(4, outer.CurrentCount); + Assert.Equal(1, outer.DisposeAsyncCount); + Assert.Equal(5, inner.MoveNextAsyncCount); + Assert.Equal(4, inner.CurrentCount); + Assert.Equal(1, inner.DisposeAsyncCount); + + outer = CreateSource(2, 4, 8, 16).Track(); + inner = CreateSource(1, 2, 3, 4).Track(); + await ConsumeAsync(outer.Join(inner, async (outer, ct) => outer, async (inner, ct) => inner)); + Assert.Equal(5, outer.MoveNextAsyncCount); + Assert.Equal(4, outer.CurrentCount); + Assert.Equal(1, outer.DisposeAsyncCount); + Assert.Equal(5, inner.MoveNextAsyncCount); + Assert.Equal(4, inner.CurrentCount); + Assert.Equal(1, inner.DisposeAsyncCount); + } } } diff --git a/src/libraries/System.Linq.AsyncEnumerable/tests/LeftJoinTests.cs b/src/libraries/System.Linq.AsyncEnumerable/tests/LeftJoinTests.cs index 759ae3ab1ed1d8..917371e9ebe958 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/tests/LeftJoinTests.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/tests/LeftJoinTests.cs @@ -178,5 +178,85 @@ public async Task InterfaceCalls_ExpectedCounts() Assert.Equal(4, inner.CurrentCount); Assert.Equal(1, inner.DisposeAsyncCount); } + + [Fact] + public void TupleLeftJoin_InvalidInputs_Throws() + { + AssertExtensions.Throws("outer", () => AsyncEnumerable.LeftJoin((IAsyncEnumerable)null, AsyncEnumerable.Empty(), outer => outer, inner => inner)); + AssertExtensions.Throws("inner", () => AsyncEnumerable.LeftJoin(AsyncEnumerable.Empty(), (IAsyncEnumerable)null, outer => outer, inner => inner)); + AssertExtensions.Throws("outerKeySelector", () => AsyncEnumerable.LeftJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), (Func)null, inner => inner)); + AssertExtensions.Throws("innerKeySelector", () => AsyncEnumerable.LeftJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), outer => outer, (Func)null)); + + AssertExtensions.Throws("outer", () => AsyncEnumerable.LeftJoin((IAsyncEnumerable)null, AsyncEnumerable.Empty(), async (outer, ct) => outer, async (inner, ct) => inner)); + AssertExtensions.Throws("inner", () => AsyncEnumerable.LeftJoin(AsyncEnumerable.Empty(), (IAsyncEnumerable)null, async (outer, ct) => outer, async (inner, ct) => inner)); + AssertExtensions.Throws("outerKeySelector", () => AsyncEnumerable.LeftJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), (Func>)null, async (inner, ct) => inner)); + AssertExtensions.Throws("innerKeySelector", () => AsyncEnumerable.LeftJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), async (outer, ct) => outer, (Func>)null)); + } + + [Fact] + public void TupleLeftJoin_Empty_ProducesEmpty() + { + IAsyncEnumerable empty = AsyncEnumerable.Empty(); + IAsyncEnumerable nonEmpty = CreateSource("1", "2", "3"); + + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.LeftJoin(empty, s => s, s => s)); + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.LeftJoin(empty, async (s, ct) => s, async (s, ct) => s)); + + Assert.NotSame(AsyncEnumerable.Empty<(string, string)>(), nonEmpty.LeftJoin(empty, s => s, s => s)); + Assert.NotSame(AsyncEnumerable.Empty<(string, string)>(), nonEmpty.LeftJoin(empty, async (s, ct) => s, async (s, ct) => s)); + + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.LeftJoin(nonEmpty, s => s, s => s)); + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.LeftJoin(nonEmpty, async (s, ct) => s, async (s, ct) => s)); + } + +#if NET + [Fact] + public async Task TupleLeftJoin_VariousValues_MatchesEnumerable_String() + { + Random rand = new(42); + foreach (int length in new[] { 0, 1, 2, 1000 }) + { + string[] values = new string[length]; + FillRandom(rand, values); + + foreach (IAsyncEnumerable source in CreateSources(values)) + { + await AssertEqual( + values.LeftJoin(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner), + source.LeftJoin(source, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner)); + + await AssertEqual( + values.LeftJoin(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner), + source.LeftJoin(source, async (s, ct) => s.Length > 0 ? s[0] : ' ', async (s, ct) => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner)); + } + } + } +#endif + + [Fact] + public async Task TupleLeftJoin_InterfaceCalls_ExpectedCounts() + { + TrackingAsyncEnumerable outer, inner; + + outer = CreateSource(2, 4, 8, 16).Track(); + inner = CreateSource(1, 2, 3, 4).Track(); + await ConsumeAsync(outer.LeftJoin(inner, outer => outer, inner => inner)); + Assert.Equal(5, outer.MoveNextAsyncCount); + Assert.Equal(4, outer.CurrentCount); + Assert.Equal(1, outer.DisposeAsyncCount); + Assert.Equal(5, inner.MoveNextAsyncCount); + Assert.Equal(4, inner.CurrentCount); + Assert.Equal(1, inner.DisposeAsyncCount); + + outer = CreateSource(2, 4, 8, 16).Track(); + inner = CreateSource(1, 2, 3, 4).Track(); + await ConsumeAsync(outer.LeftJoin(inner, async (outer, ct) => outer, async (inner, ct) => inner)); + Assert.Equal(5, outer.MoveNextAsyncCount); + Assert.Equal(4, outer.CurrentCount); + Assert.Equal(1, outer.DisposeAsyncCount); + Assert.Equal(5, inner.MoveNextAsyncCount); + Assert.Equal(4, inner.CurrentCount); + Assert.Equal(1, inner.DisposeAsyncCount); + } } } diff --git a/src/libraries/System.Linq.AsyncEnumerable/tests/RightJoinTests.cs b/src/libraries/System.Linq.AsyncEnumerable/tests/RightJoinTests.cs index e7f123a464e463..de91b6201c4403 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/tests/RightJoinTests.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/tests/RightJoinTests.cs @@ -178,5 +178,85 @@ public async Task InterfaceCalls_ExpectedCounts() Assert.Equal(4, inner.CurrentCount); Assert.Equal(1, inner.DisposeAsyncCount); } + + [Fact] + public void TupleRightJoin_InvalidInputs_Throws() + { + AssertExtensions.Throws("outer", () => AsyncEnumerable.RightJoin((IAsyncEnumerable)null, AsyncEnumerable.Empty(), outer => outer, inner => inner)); + AssertExtensions.Throws("inner", () => AsyncEnumerable.RightJoin(AsyncEnumerable.Empty(), (IAsyncEnumerable)null, outer => outer, inner => inner)); + AssertExtensions.Throws("outerKeySelector", () => AsyncEnumerable.RightJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), (Func)null, inner => inner)); + AssertExtensions.Throws("innerKeySelector", () => AsyncEnumerable.RightJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), outer => outer, (Func)null)); + + AssertExtensions.Throws("outer", () => AsyncEnumerable.RightJoin((IAsyncEnumerable)null, AsyncEnumerable.Empty(), async (outer, ct) => outer, async (inner, ct) => inner)); + AssertExtensions.Throws("inner", () => AsyncEnumerable.RightJoin(AsyncEnumerable.Empty(), (IAsyncEnumerable)null, async (outer, ct) => outer, async (inner, ct) => inner)); + AssertExtensions.Throws("outerKeySelector", () => AsyncEnumerable.RightJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), (Func>)null, async (inner, ct) => inner)); + AssertExtensions.Throws("innerKeySelector", () => AsyncEnumerable.RightJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), async (outer, ct) => outer, (Func>)null)); + } + + [Fact] + public void TupleRightJoin_Empty_ProducesEmpty() + { + IAsyncEnumerable empty = AsyncEnumerable.Empty(); + IAsyncEnumerable nonEmpty = CreateSource("1", "2", "3"); + + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.RightJoin(empty, s => s, s => s)); + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), empty.RightJoin(empty, async (s, ct) => s, async (s, ct) => s)); + + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), nonEmpty.RightJoin(empty, s => s, s => s)); + Assert.Same(AsyncEnumerable.Empty<(string, string)>(), nonEmpty.RightJoin(empty, async (s, ct) => s, async (s, ct) => s)); + + Assert.NotSame(AsyncEnumerable.Empty<(string, string)>(), empty.RightJoin(nonEmpty, s => s, s => s)); + Assert.NotSame(AsyncEnumerable.Empty<(string, string)>(), empty.RightJoin(nonEmpty, async (s, ct) => s, async (s, ct) => s)); + } + +#if NET + [Fact] + public async Task TupleRightJoin_VariousValues_MatchesEnumerable_String() + { + Random rand = new(42); + foreach (int length in new[] { 0, 1, 2, 1000 }) + { + string[] values = new string[length]; + FillRandom(rand, values); + + foreach (IAsyncEnumerable source in CreateSources(values)) + { + await AssertEqual( + values.RightJoin(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner), + source.RightJoin(source, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner)); + + await AssertEqual( + values.RightJoin(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner), + source.RightJoin(source, async (s, ct) => s.Length > 0 ? s[0] : ' ', async (s, ct) => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner)); + } + } + } +#endif + + [Fact] + public async Task TupleRightJoin_InterfaceCalls_ExpectedCounts() + { + TrackingAsyncEnumerable outer, inner; + + outer = CreateSource(2, 4, 8, 16).Track(); + inner = CreateSource(1, 2, 3, 4).Track(); + await ConsumeAsync(outer.RightJoin(inner, outer => outer, inner => inner)); + Assert.Equal(5, outer.MoveNextAsyncCount); + Assert.Equal(4, outer.CurrentCount); + Assert.Equal(1, outer.DisposeAsyncCount); + Assert.Equal(5, inner.MoveNextAsyncCount); + Assert.Equal(4, inner.CurrentCount); + Assert.Equal(1, inner.DisposeAsyncCount); + + outer = CreateSource(2, 4, 8, 16).Track(); + inner = CreateSource(1, 2, 3, 4).Track(); + await ConsumeAsync(outer.RightJoin(inner, async (outer, ct) => outer, async (inner, ct) => inner)); + Assert.Equal(5, outer.MoveNextAsyncCount); + Assert.Equal(4, outer.CurrentCount); + Assert.Equal(1, outer.DisposeAsyncCount); + Assert.Equal(5, inner.MoveNextAsyncCount); + Assert.Equal(4, inner.CurrentCount); + Assert.Equal(1, inner.DisposeAsyncCount); + } } } diff --git a/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs b/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs index 2d63b858f0bd26..42a98a7cd3c3d9 100644 --- a/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs +++ b/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs @@ -117,6 +117,7 @@ public static partial class Queryable public static System.Linq.IQueryable Intersect(this System.Linq.IQueryable source1, System.Collections.Generic.IEnumerable source2, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } public static System.Linq.IQueryable Join(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Linq.Expressions.Expression> resultSelector) { throw null; } public static System.Linq.IQueryable Join(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Linq.Expressions.Expression> resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } + public static System.Linq.IQueryable<(TOuter Outer, TInner Inner)> Join(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static TSource? LastOrDefault(this System.Linq.IQueryable source) { throw null; } public static TSource? LastOrDefault(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> predicate) { throw null; } public static TSource LastOrDefault(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> predicate, TSource defaultValue) { throw null; } @@ -125,6 +126,7 @@ public static partial class Queryable public static TSource Last(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> predicate) { throw null; } public static System.Linq.IQueryable LeftJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Linq.Expressions.Expression> resultSelector) { throw null; } public static System.Linq.IQueryable LeftJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Linq.Expressions.Expression> resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } + public static System.Linq.IQueryable<(TOuter Outer, TInner? Inner)> LeftJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static long LongCount(this System.Linq.IQueryable source) { throw null; } public static long LongCount(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> predicate) { throw null; } public static TSource? MaxBy(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> keySelector) { throw null; } @@ -158,6 +160,7 @@ public static partial class Queryable public static System.Linq.IQueryable Reverse(this System.Linq.IQueryable source) { throw null; } public static System.Linq.IQueryable RightJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Linq.Expressions.Expression> resultSelector) { throw null; } public static System.Linq.IQueryable RightJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Linq.Expressions.Expression> resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } + public static System.Linq.IQueryable<(TOuter? Outer, TInner Inner)> RightJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Linq.IQueryable SelectMany(this System.Linq.IQueryable source, System.Linq.Expressions.Expression>> selector) { throw null; } public static System.Linq.IQueryable SelectMany(this System.Linq.IQueryable source, System.Linq.Expressions.Expression>> selector) { throw null; } public static System.Linq.IQueryable SelectMany(this System.Linq.IQueryable source, System.Linq.Expressions.Expression>> collectionSelector, System.Linq.Expressions.Expression> resultSelector) { throw null; } diff --git a/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs b/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs index 1290b8f6819e23..90b71b77de5049 100644 --- a/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs +++ b/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs @@ -408,6 +408,34 @@ public static IQueryable Join(this IQuer outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector), Expression.Quote(resultSelector), Expression.Constant(comparer, typeof(IEqualityComparer)))); } + /// + /// Correlates the elements of two sequences based on matching keys. If is or omitted, the default equality comparer is used to compare keys. + /// + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter Outer, TInner Inner) that are obtained by performing an inner join on two sequences. + /// or or or is . + [DynamicDependency("Join`3", typeof(Enumerable))] + public static IQueryable<(TOuter Outer, TInner Inner)> Join(this IQueryable outer, IEnumerable inner, Expression> outerKeySelector, Expression> innerKeySelector, IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return outer.Provider.CreateQuery<(TOuter Outer, TInner Inner)>( + Expression.Call( + null, + new Func, IEnumerable, Expression>, Expression>, IEqualityComparer, IQueryable<(TOuter Outer, TInner Inner)>>(Join).Method, + outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector), Expression.Constant(comparer, typeof(IEqualityComparer)))); + } + [DynamicDependency("GroupJoin`4", typeof(Enumerable))] public static IQueryable GroupJoin(this IQueryable outer, IEnumerable inner, Expression> outerKeySelector, Expression> innerKeySelector, Expression, TResult>> resultSelector) { @@ -669,6 +697,34 @@ public static IQueryable LeftJoin(this I outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector), Expression.Quote(resultSelector), Expression.Constant(comparer, typeof(IEqualityComparer)))); } + /// + /// Correlates the elements of two sequences based on matching keys. If is or omitted, the default equality comparer is used to compare keys. + /// + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter Outer, TInner? Inner) that are obtained by performing a left outer join on two sequences. + /// or or or is . + [DynamicDependency("LeftJoin`3", typeof(Enumerable))] + public static IQueryable<(TOuter Outer, TInner? Inner)> LeftJoin(this IQueryable outer, IEnumerable inner, Expression> outerKeySelector, Expression> innerKeySelector, IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return outer.Provider.CreateQuery<(TOuter Outer, TInner? Inner)>( + Expression.Call( + null, + new Func, IEnumerable, Expression>, Expression>, IEqualityComparer, IQueryable<(TOuter Outer, TInner? Inner)>>(LeftJoin).Method, + outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector), Expression.Constant(comparer, typeof(IEqualityComparer)))); + } + /// /// Sorts the elements of a sequence in ascending order. /// @@ -1094,6 +1150,34 @@ public static IQueryable RightJoin(this outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector), Expression.Quote(resultSelector), Expression.Constant(comparer, typeof(IEqualityComparer)))); } + /// + /// Correlates the elements of two sequences based on matching keys. If is or omitted, the default equality comparer is used to compare keys. + /// + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter? Outer, TInner Inner) that are obtained by performing a right outer join on two sequences. + /// or or or is . + [DynamicDependency("RightJoin`3", typeof(Enumerable))] + public static IQueryable<(TOuter? Outer, TInner Inner)> RightJoin(this IQueryable outer, IEnumerable inner, Expression> outerKeySelector, Expression> innerKeySelector, IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return outer.Provider.CreateQuery<(TOuter? Outer, TInner Inner)>( + Expression.Call( + null, + new Func, IEnumerable, Expression>, Expression>, IEqualityComparer, IQueryable<(TOuter? Outer, TInner Inner)>>(RightJoin).Method, + outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector), Expression.Constant(comparer, typeof(IEqualityComparer)))); + } + [DynamicDependency("ThenBy`2", typeof(Enumerable))] public static IOrderedQueryable ThenBy(this IOrderedQueryable source, Expression> keySelector) { diff --git a/src/libraries/System.Linq.Queryable/tests/JoinTests.cs b/src/libraries/System.Linq.Queryable/tests/JoinTests.cs index 2677a404871d98..0b0b9c536c1ecc 100644 --- a/src/libraries/System.Linq.Queryable/tests/JoinTests.cs +++ b/src/libraries/System.Linq.Queryable/tests/JoinTests.cs @@ -262,5 +262,95 @@ public void Join2() var count = new[] { 0, 1, 2 }.AsQueryable().Join(new[] { 1, 2, 3 }, n1 => n1, n2 => n2, (n1, n2) => n1 + n2, EqualityComparer.Default).Count(); Assert.Equal(2, count); } + + [Fact] + public void TupleJoin_Basic() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + }; + OrderRec[] inner = { + new OrderRec{ orderID = 45321, custID = 99022, total = 50 }, + new OrderRec{ orderID = 43421, custID = 29022, total = 20 }, + new OrderRec{ orderID = 95421, custID = 98022, total = 9 } + }; + + var result = outer.AsQueryable().Join(inner.AsQueryable(), e => e.custID, e => e.custID).ToList(); + + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Outer.name == "Prakash" && r.Inner.orderID == 95421); + Assert.Contains(result, r => r.Outer.name == "Robert" && r.Inner.orderID == 45321); + } + + [Fact] + public void TupleJoin_WithComparer() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 } + }; + AnagramRec[] inner = { + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + }; + + var result = outer.AsQueryable().Join(inner.AsQueryable(), e => e.name, e => e.name, new AnagramEqualityComparer()).ToList(); + + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Outer.name == "Prakash" && r.Inner.name == "Prakash"); + Assert.Contains(result, r => r.Outer.name == "Tim" && r.Inner.name == "miT"); + } + + [Fact] + public void TupleJoin_OuterNull() + { + IQueryable outer = null; + OrderRec[] inner = { new OrderRec{ orderID = 45321, custID = 98022, total = 50 } }; + + AssertExtensions.Throws("outer", () => outer.Join(inner.AsQueryable(), e => e.custID, e => e.custID)); + } + + [Fact] + public void TupleJoin_InnerNull() + { + CustomerRec[] outer = { new CustomerRec{ name = "Prakash", custID = 98022 } }; + IEnumerable inner = null; + + AssertExtensions.Throws("inner", () => outer.AsQueryable().Join(inner, e => e.custID, e => e.custID)); + } + + [Fact] + public void TupleJoin_OuterKeySelectorNull() + { + CustomerRec[] outer = { new CustomerRec{ name = "Prakash", custID = 98022 } }; + OrderRec[] inner = { new OrderRec{ orderID = 45321, custID = 98022, total = 50 } }; + + AssertExtensions.Throws("outerKeySelector", () => outer.AsQueryable().Join(inner.AsQueryable(), (Expression>)null, e => e.custID)); + } + + [Fact] + public void TupleJoin_InnerKeySelectorNull() + { + CustomerRec[] outer = { new CustomerRec{ name = "Prakash", custID = 98022 } }; + OrderRec[] inner = { new OrderRec{ orderID = 45321, custID = 98022, total = 50 } }; + + AssertExtensions.Throws("innerKeySelector", () => outer.AsQueryable().Join(inner.AsQueryable(), e => e.custID, (Expression>)null)); + } + + [Fact] + public void TupleJoin1() + { + var count = new[] { 0, 1, 2 }.AsQueryable().Join(new[] { 1, 2, 3 }, n1 => n1, n2 => n2).Count(); + Assert.Equal(2, count); + } + + [Fact] + public void TupleJoin2() + { + var count = new[] { 0, 1, 2 }.AsQueryable().Join(new[] { 1, 2, 3 }, n1 => n1, n2 => n2, EqualityComparer.Default).Count(); + Assert.Equal(2, count); + } } } diff --git a/src/libraries/System.Linq.Queryable/tests/LeftJoinTests.cs b/src/libraries/System.Linq.Queryable/tests/LeftJoinTests.cs index 9666a1c7c8bb29..7d8b2d6282944e 100644 --- a/src/libraries/System.Linq.Queryable/tests/LeftJoinTests.cs +++ b/src/libraries/System.Linq.Queryable/tests/LeftJoinTests.cs @@ -270,5 +270,95 @@ public void Join2() var count = new[] { 0, 1, 2 }.AsQueryable().LeftJoin(new[] { 1, 2, 3 }, n1 => n1, n2 => n2, (n1, n2) => n1 + n2, EqualityComparer.Default).Count(); Assert.Equal(3, count); } + + [Fact] + public void TupleLeftJoin_Basic() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + }; + OrderRec[] inner = { + new OrderRec{ orderID = 45321, custID = 99022, total = 50 }, + new OrderRec{ orderID = 43421, custID = 29022, total = 20 }, + new OrderRec{ orderID = 95421, custID = 98022, total = 9 } + }; + + var result = outer.AsQueryable().LeftJoin(inner.AsQueryable(), e => e.custID, e => e.custID).ToList(); + + Assert.Equal(3, result.Count); + Assert.Contains(result, r => r.Outer.name == "Prakash" && r.Inner.orderID == 95421); + Assert.Contains(result, r => r.Outer.name == "Tim" && r.Inner.orderID == 0); + Assert.Contains(result, r => r.Outer.name == "Robert" && r.Inner.orderID == 45321); + } + + [Fact] + public void TupleLeftJoin_WithComparer() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 } + }; + AnagramRec[] inner = { + new AnagramRec{ name = "miT", orderID = 43455, total = 10 } + }; + + var result = outer.AsQueryable().LeftJoin(inner.AsQueryable(), e => e.name, e => e.name, new AnagramEqualityComparer()).ToList(); + + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Outer.name == "Tim" && r.Inner.name == "miT"); + Assert.Contains(result, r => r.Outer.name == "Prakash" && r.Inner.name == null); + } + + [Fact] + public void TupleLeftJoin_OuterNull() + { + IQueryable outer = null; + OrderRec[] inner = { new OrderRec{ orderID = 45321, custID = 98022, total = 50 } }; + + AssertExtensions.Throws("outer", () => outer.LeftJoin(inner.AsQueryable(), e => e.custID, e => e.custID)); + } + + [Fact] + public void TupleLeftJoin_InnerNull() + { + CustomerRec[] outer = { new CustomerRec{ name = "Prakash", custID = 98022 } }; + IEnumerable inner = null; + + AssertExtensions.Throws("inner", () => outer.AsQueryable().LeftJoin(inner, e => e.custID, e => e.custID)); + } + + [Fact] + public void TupleLeftJoin_OuterKeySelectorNull() + { + CustomerRec[] outer = { new CustomerRec{ name = "Prakash", custID = 98022 } }; + OrderRec[] inner = { new OrderRec{ orderID = 45321, custID = 98022, total = 50 } }; + + AssertExtensions.Throws("outerKeySelector", () => outer.AsQueryable().LeftJoin(inner.AsQueryable(), (Expression>)null, e => e.custID)); + } + + [Fact] + public void TupleLeftJoin_InnerKeySelectorNull() + { + CustomerRec[] outer = { new CustomerRec{ name = "Prakash", custID = 98022 } }; + OrderRec[] inner = { new OrderRec{ orderID = 45321, custID = 98022, total = 50 } }; + + AssertExtensions.Throws("innerKeySelector", () => outer.AsQueryable().LeftJoin(inner.AsQueryable(), e => e.custID, (Expression>)null)); + } + + [Fact] + public void TupleLeftJoin1() + { + var count = new[] { 0, 1, 2 }.AsQueryable().LeftJoin(new[] { 1, 2, 3 }, n1 => n1, n2 => n2).Count(); + Assert.Equal(3, count); + } + + [Fact] + public void TupleLeftJoin2() + { + var count = new[] { 0, 1, 2 }.AsQueryable().LeftJoin(new[] { 1, 2, 3 }, n1 => n1, n2 => n2, EqualityComparer.Default).Count(); + Assert.Equal(3, count); + } } } diff --git a/src/libraries/System.Linq.Queryable/tests/RightJoinTests.cs b/src/libraries/System.Linq.Queryable/tests/RightJoinTests.cs index 4ca4930a350edf..de0120176d6f20 100644 --- a/src/libraries/System.Linq.Queryable/tests/RightJoinTests.cs +++ b/src/libraries/System.Linq.Queryable/tests/RightJoinTests.cs @@ -269,5 +269,95 @@ public void Join2() var count = new[] { 0, 1, 2 }.AsQueryable().RightJoin(new[] { 1, 2, 3 }, n1 => n1, n2 => n2, (n1, n2) => n1 + n2, EqualityComparer.Default).Count(); Assert.Equal(3, count); } + + [Fact] + public void TupleRightJoin_Basic() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + }; + OrderRec[] inner = { + new OrderRec{ orderID = 45321, custID = 99022, total = 50 }, + new OrderRec{ orderID = 43421, custID = 29022, total = 20 }, + new OrderRec{ orderID = 95421, custID = 98022, total = 9 } + }; + + var result = outer.AsQueryable().RightJoin(inner.AsQueryable(), e => e.custID, e => e.custID).ToList(); + + Assert.Equal(3, result.Count); + Assert.Contains(result, r => r.Outer.name == "Robert" && r.Inner.orderID == 45321); + Assert.Contains(result, r => r.Outer.name == null && r.Inner.orderID == 43421); + Assert.Contains(result, r => r.Outer.name == "Prakash" && r.Inner.orderID == 95421); + } + + [Fact] + public void TupleRightJoin_WithComparer() + { + CustomerRec[] outer = { + new CustomerRec{ name = "Tim", custID = 99021 } + }; + AnagramRec[] inner = { + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + }; + + var result = outer.AsQueryable().RightJoin(inner.AsQueryable(), e => e.name, e => e.name, new AnagramEqualityComparer()).ToList(); + + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Outer.name == "Tim" && r.Inner.name == "miT"); + Assert.Contains(result, r => r.Outer.name == null && r.Inner.name == "Prakash"); + } + + [Fact] + public void TupleRightJoin_OuterNull() + { + IQueryable outer = null; + OrderRec[] inner = { new OrderRec{ orderID = 45321, custID = 98022, total = 50 } }; + + AssertExtensions.Throws("outer", () => outer.RightJoin(inner.AsQueryable(), e => e.custID, e => e.custID)); + } + + [Fact] + public void TupleRightJoin_InnerNull() + { + CustomerRec[] outer = { new CustomerRec{ name = "Prakash", custID = 98022 } }; + IEnumerable inner = null; + + AssertExtensions.Throws("inner", () => outer.AsQueryable().RightJoin(inner, e => e.custID, e => e.custID)); + } + + [Fact] + public void TupleRightJoin_OuterKeySelectorNull() + { + CustomerRec[] outer = { new CustomerRec{ name = "Prakash", custID = 98022 } }; + OrderRec[] inner = { new OrderRec{ orderID = 45321, custID = 98022, total = 50 } }; + + AssertExtensions.Throws("outerKeySelector", () => outer.AsQueryable().RightJoin(inner.AsQueryable(), (Expression>)null, e => e.custID)); + } + + [Fact] + public void TupleRightJoin_InnerKeySelectorNull() + { + CustomerRec[] outer = { new CustomerRec{ name = "Prakash", custID = 98022 } }; + OrderRec[] inner = { new OrderRec{ orderID = 45321, custID = 98022, total = 50 } }; + + AssertExtensions.Throws("innerKeySelector", () => outer.AsQueryable().RightJoin(inner.AsQueryable(), e => e.custID, (Expression>)null)); + } + + [Fact] + public void TupleRightJoin1() + { + var count = new[] { 0, 1, 2 }.AsQueryable().RightJoin(new[] { 1, 2, 3 }, n1 => n1, n2 => n2).Count(); + Assert.Equal(3, count); + } + + [Fact] + public void TupleRightJoin2() + { + var count = new[] { 0, 1, 2 }.AsQueryable().RightJoin(new[] { 1, 2, 3 }, n1 => n1, n2 => n2, EqualityComparer.Default).Count(); + Assert.Equal(3, count); + } } } diff --git a/src/libraries/System.Linq/ref/System.Linq.cs b/src/libraries/System.Linq/ref/System.Linq.cs index 0c2d82b2111766..976d567245c18b 100644 --- a/src/libraries/System.Linq/ref/System.Linq.cs +++ b/src/libraries/System.Linq/ref/System.Linq.cs @@ -91,6 +91,7 @@ public static System.Collections.Generic.IEnumerable< public static System.Collections.Generic.IEnumerable Intersect(this System.Collections.Generic.IEnumerable first, System.Collections.Generic.IEnumerable second, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } public static System.Collections.Generic.IEnumerable Join(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector) { throw null; } public static System.Collections.Generic.IEnumerable Join(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } + public static System.Collections.Generic.IEnumerable<(TOuter Outer, TInner Inner)> Join(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static TSource? LastOrDefault(this System.Collections.Generic.IEnumerable source) { throw null; } public static TSource LastOrDefault(this System.Collections.Generic.IEnumerable source, TSource defaultValue) { throw null; } public static TSource? LastOrDefault(this System.Collections.Generic.IEnumerable source, System.Func predicate) { throw null; } @@ -99,6 +100,7 @@ public static System.Collections.Generic.IEnumerable< public static TSource Last(this System.Collections.Generic.IEnumerable source, System.Func predicate) { throw null; } public static System.Collections.Generic.IEnumerable LeftJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector) { throw null; } public static System.Collections.Generic.IEnumerable LeftJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } + public static System.Collections.Generic.IEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static long LongCount(this System.Collections.Generic.IEnumerable source) { throw null; } public static long LongCount(this System.Collections.Generic.IEnumerable source, System.Func predicate) { throw null; } public static decimal Max(this System.Collections.Generic.IEnumerable source) { throw null; } @@ -167,6 +169,7 @@ public static System.Collections.Generic.IEnumerable< public static System.Collections.Generic.IEnumerable Reverse(this TSource[] source) { throw null; } public static System.Collections.Generic.IEnumerable RightJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector) { throw null; } public static System.Collections.Generic.IEnumerable RightJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } + public static System.Collections.Generic.IEnumerable<(TOuter? Outer, TInner Inner)> RightJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IEnumerable SelectMany(this System.Collections.Generic.IEnumerable source, System.Func> selector) { throw null; } public static System.Collections.Generic.IEnumerable SelectMany(this System.Collections.Generic.IEnumerable source, System.Func> selector) { throw null; } public static System.Collections.Generic.IEnumerable SelectMany(this System.Collections.Generic.IEnumerable source, System.Func> collectionSelector, System.Func resultSelector) { throw null; } diff --git a/src/libraries/System.Linq/src/System/Linq/Join.cs b/src/libraries/System.Linq/src/System/Linq/Join.cs index 677f6bd0eb8b36..265757b021cf96 100644 --- a/src/libraries/System.Linq/src/System/Linq/Join.cs +++ b/src/libraries/System.Linq/src/System/Linq/Join.cs @@ -272,5 +272,83 @@ private static IEnumerable JoinIterator( } } } + + /// + /// Correlates the elements of two sequences based on matching keys. If is or omitted, the default equality comparer is used to compare keys. + /// + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter Outer, TInner Inner) that are obtained by performing an inner join on two sequences. + /// or or or is . + /// + /// + /// This method is implemented by using deferred execution. The immediate return value is an object that stores + /// all the information that is required to perform the action. The query represented by this method is not + /// executed until the object is enumerated either by calling its GetEnumerator method directly or by + /// using foreach in C# or For Each in Visual Basic. + /// + /// + public static IEnumerable<(TOuter Outer, TInner Inner)> Join(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null) + { + if (outer is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.outer); + } + + if (inner is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.inner); + } + + if (outerKeySelector is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.outerKeySelector); + } + + if (innerKeySelector is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.innerKeySelector); + } + + if (IsEmptyArray(outer)) + { + return []; + } + + return JoinIterator(outer, inner, outerKeySelector, innerKeySelector, comparer); + } + + private static IEnumerable<(TOuter Outer, TInner Inner)> JoinIterator(IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer) + { + using IEnumerator e = outer.GetEnumerator(); + + if (e.MoveNext()) + { + Lookup lookup = Lookup.CreateForJoin(inner, innerKeySelector, comparer); + if (lookup.Count != 0) + { + do + { + TOuter item = e.Current; + Grouping? g = lookup.GetGrouping(outerKeySelector(item), create: false); + if (g is not null) + { + int count = g._count; + TInner[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return (item, elements[i]); + } + } + } while (e.MoveNext()); + } + } + } } } diff --git a/src/libraries/System.Linq/src/System/Linq/LeftJoin.cs b/src/libraries/System.Linq/src/System/Linq/LeftJoin.cs index 097e2453d0167e..59fa354aa928f8 100644 --- a/src/libraries/System.Linq/src/System/Linq/LeftJoin.cs +++ b/src/libraries/System.Linq/src/System/Linq/LeftJoin.cs @@ -272,5 +272,85 @@ private static IEnumerable LeftJoinIterator + /// Correlates the elements of two sequences based on matching keys. If is or omitted, the default equality comparer is used to compare keys. + /// + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter Outer, TInner? Inner) that are obtained by performing a left outer join on two sequences. + /// or or or is . + /// + /// + /// This method is implemented by using deferred execution. The immediate return value is an object that stores + /// all the information that is required to perform the action. The query represented by this method is not + /// executed until the object is enumerated either by calling its GetEnumerator method directly or by + /// using foreach in C# or For Each in Visual Basic. + /// + /// + public static IEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null) + { + if (outer is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.outer); + } + + if (inner is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.inner); + } + + if (outerKeySelector is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.outerKeySelector); + } + + if (innerKeySelector is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.innerKeySelector); + } + + if (IsEmptyArray(outer)) + { + return []; + } + + return LeftJoinIterator(outer, inner, outerKeySelector, innerKeySelector, comparer); + } + + private static IEnumerable<(TOuter Outer, TInner? Inner)> LeftJoinIterator(IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer) + { + using IEnumerator e = outer.GetEnumerator(); + + if (e.MoveNext()) + { + Lookup innerLookup = Lookup.CreateForJoin(inner, innerKeySelector, comparer); + do + { + TOuter item = e.Current; + Grouping? g = innerLookup.GetGrouping(outerKeySelector(item), create: false); + if (g is null) + { + yield return (item, default); + } + else + { + int count = g._count; + TInner[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return (item, elements[i]); + } + } + } + while (e.MoveNext()); + } + } } } diff --git a/src/libraries/System.Linq/src/System/Linq/RightJoin.cs b/src/libraries/System.Linq/src/System/Linq/RightJoin.cs index 2485b7c13a281c..e54a0aea76ebe0 100644 --- a/src/libraries/System.Linq/src/System/Linq/RightJoin.cs +++ b/src/libraries/System.Linq/src/System/Linq/RightJoin.cs @@ -270,5 +270,85 @@ private static IEnumerable RightJoinIterator + /// Correlates the elements of two sequences based on matching keys. If is or omitted, the default equality comparer is used to compare keys. + /// + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// An that has elements of type (TOuter? Outer, TInner Inner) that are obtained by performing a right outer join on two sequences. + /// or or or is . + /// + /// + /// This method is implemented by using deferred execution. The immediate return value is an object that stores + /// all the information that is required to perform the action. The query represented by this method is not + /// executed until the object is enumerated either by calling its GetEnumerator method directly or by + /// using foreach in C# or For Each in Visual Basic. + /// + /// + public static IEnumerable<(TOuter? Outer, TInner Inner)> RightJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null) + { + if (outer is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.outer); + } + + if (inner is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.inner); + } + + if (outerKeySelector is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.outerKeySelector); + } + + if (innerKeySelector is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.innerKeySelector); + } + + if (IsEmptyArray(inner)) + { + return []; + } + + return RightJoinIterator(outer, inner, outerKeySelector, innerKeySelector, comparer); + } + + private static IEnumerable<(TOuter? Outer, TInner Inner)> RightJoinIterator(IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer) + { + using IEnumerator e = inner.GetEnumerator(); + + if (e.MoveNext()) + { + Lookup outerLookup = Lookup.CreateForJoin(outer, outerKeySelector, comparer); + do + { + TInner item = e.Current; + Grouping? g = outerLookup.GetGrouping(innerKeySelector(item), create: false); + if (g is null) + { + yield return (default, item); + } + else + { + int count = g._count; + TOuter[] elements = g._elements; + for (int i = 0; i != count; ++i) + { + yield return (elements[i], item); + } + } + } + while (e.MoveNext()); + } + } } } diff --git a/src/libraries/System.Linq/tests/JoinTests.cs b/src/libraries/System.Linq/tests/JoinTests.cs index 4504fc47521d5c..5fc9571f0854b5 100644 --- a/src/libraries/System.Linq/tests/JoinTests.cs +++ b/src/libraries/System.Linq/tests/JoinTests.cs @@ -417,5 +417,109 @@ public void ForcedToEnumeratorDoesntEnumerate() var en = iterator as IEnumerator; Assert.False(en is not null && en.MoveNext()); } + + [Fact] + public void TupleJoin_Basic() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 }, + new CustomerRec{ name = "Robert", custID = 99022 } + ]; + OrderRec[] inner = + [ + new OrderRec{ orderID = 45321, custID = 99022, total = 50 }, + new OrderRec{ orderID = 43421, custID = 29022, total = 20 }, + new OrderRec{ orderID = 95421, custID = 98022, total = 9 } + ]; + + var result = outer.Join(inner, o => o.custID, i => i.custID); + + var expected = outer.Join(inner, o => o.custID, i => i.custID, (o, i) => (Outer: o, Inner: i)); + + Assert.Equal(expected, result); + } + + [Fact] + public void TupleJoin_EmptyOuter() + { + CustomerRec[] outer = []; + OrderRec[] inner = + [ + new OrderRec{ orderID = 45321, custID = 98022, total = 50 } + ]; + + Assert.Empty(outer.Join(inner, o => o.custID, i => i.custID)); + } + + [Fact] + public void TupleJoin_EmptyInner() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 } + ]; + OrderRec[] inner = []; + + Assert.Empty(outer.Join(inner, o => o.custID, i => i.custID)); + } + + [Fact] + public void TupleJoin_WithComparer() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 } + ]; + AnagramRec[] inner = + [ + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + ]; + + var result = outer.Join(inner, o => o.name, i => i.name, new AnagramEqualityComparer()); + + Assert.Equal(2, result.Count()); + Assert.Contains(result, r => r.Outer.name == "Prakash" && r.Inner.name == "Prakash"); + Assert.Contains(result, r => r.Outer.name == "Tim" && r.Inner.name == "miT"); + } + + [Fact] + public void TupleJoin_OuterNull() + { + CustomerRec[] outer = null; + OrderRec[] inner = [new OrderRec{ orderID = 45321, custID = 98022, total = 50 }]; + + AssertExtensions.Throws("outer", () => outer.Join(inner, o => o.custID, i => i.custID)); + } + + [Fact] + public void TupleJoin_InnerNull() + { + CustomerRec[] outer = [new CustomerRec{ name = "Prakash", custID = 98022 }]; + OrderRec[] inner = null; + + AssertExtensions.Throws("inner", () => outer.Join(inner, o => o.custID, i => i.custID)); + } + + [Fact] + public void TupleJoin_OuterKeySelectorNull() + { + CustomerRec[] outer = [new CustomerRec{ name = "Prakash", custID = 98022 }]; + OrderRec[] inner = [new OrderRec{ orderID = 45321, custID = 98022, total = 50 }]; + + AssertExtensions.Throws("outerKeySelector", () => outer.Join(inner, (Func)null, i => i.custID)); + } + + [Fact] + public void TupleJoin_InnerKeySelectorNull() + { + CustomerRec[] outer = [new CustomerRec{ name = "Prakash", custID = 98022 }]; + OrderRec[] inner = [new OrderRec{ orderID = 45321, custID = 98022, total = 50 }]; + + AssertExtensions.Throws("innerKeySelector", () => outer.Join(inner, o => o.custID, (Func)null)); + } } } diff --git a/src/libraries/System.Linq/tests/LeftJoinTests.cs b/src/libraries/System.Linq/tests/LeftJoinTests.cs index 43e81014aad5b1..c5daddd5ec4182 100644 --- a/src/libraries/System.Linq/tests/LeftJoinTests.cs +++ b/src/libraries/System.Linq/tests/LeftJoinTests.cs @@ -446,5 +446,99 @@ public void ForcedToEnumeratorDoesntEnumerate() var en = iterator as IEnumerator; Assert.False(en is not null && en.MoveNext()); } + + [Fact] + public void TupleLeftJoin_Basic() + { + string[] outer = ["Prakash", "Tim", "Robert"]; + string[] inner = ["prakash", "robert"]; + + var result = outer.LeftJoin(inner, o => o.ToLowerInvariant(), i => i.ToLowerInvariant()).ToList(); + + var expected = outer.LeftJoin( + inner, + o => o.ToLowerInvariant(), + i => i.ToLowerInvariant(), + (o, i) => (Outer: o, Inner: i)).ToList(); + + Assert.Equal(expected, result); + } + + [Fact] + public void TupleLeftJoin_EmptyOuter() + { + string[] outer = []; + string[] inner = ["prakash"]; + + Assert.Empty(outer.LeftJoin(inner, o => o, i => i)); + } + + [Fact] + public void TupleLeftJoin_EmptyInner() + { + string[] outer = ["Prakash"]; + string[] inner = Array.Empty(); + + var result = outer.LeftJoin(inner, o => o, i => i).ToList(); + Assert.Single(result); + Assert.Equal("Prakash", result[0].Outer); + Assert.Null(result[0].Inner); + } + + [Fact] + public void TupleLeftJoin_WithComparer() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Prakash", custID = 98022 }, + new CustomerRec{ name = "Tim", custID = 99021 } + ]; + AnagramRec[] inner = + [ + new AnagramRec{ name = "miT", orderID = 43455, total = 10 } + ]; + + var result = outer.LeftJoin(inner, o => o.name, i => i.name, new AnagramEqualityComparer()).ToList(); + + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Outer.name == "Prakash" && r.Inner.name == null); + Assert.Contains(result, r => r.Outer.name == "Tim" && r.Inner.name == "miT"); + } + + [Fact] + public void TupleLeftJoin_OuterNull() + { + CustomerRec[] outer = null; + OrderRec[] inner = [new OrderRec{ orderID = 45321, custID = 98022, total = 50 }]; + + AssertExtensions.Throws("outer", () => outer.LeftJoin(inner, o => o.custID, i => i.custID)); + } + + [Fact] + public void TupleLeftJoin_InnerNull() + { + CustomerRec[] outer = [new CustomerRec{ name = "Prakash", custID = 98022 }]; + OrderRec[] inner = null; + + AssertExtensions.Throws("inner", () => outer.LeftJoin(inner, o => o.custID, i => i.custID)); + } + + [Fact] + public void TupleLeftJoin_OuterKeySelectorNull() + { + CustomerRec[] outer = [new CustomerRec{ name = "Prakash", custID = 98022 }]; + OrderRec[] inner = [new OrderRec{ orderID = 45321, custID = 98022, total = 50 }]; + + AssertExtensions.Throws("outerKeySelector", () => outer.LeftJoin(inner, (Func)null, i => i.custID)); + } + + [Fact] + public void TupleLeftJoin_InnerKeySelectorNull() + { + CustomerRec[] outer = [new CustomerRec{ name = "Prakash", custID = 98022 }]; + OrderRec[] inner = [new OrderRec{ orderID = 45321, custID = 98022, total = 50 }]; + + AssertExtensions.Throws("innerKeySelector", () => outer.LeftJoin(inner, o => o.custID, (Func)null)); + } } } diff --git a/src/libraries/System.Linq/tests/RightJoinTests.cs b/src/libraries/System.Linq/tests/RightJoinTests.cs index 9eb51920f48be3..433b1fc21165af 100644 --- a/src/libraries/System.Linq/tests/RightJoinTests.cs +++ b/src/libraries/System.Linq/tests/RightJoinTests.cs @@ -443,5 +443,100 @@ public void ForcedToEnumeratorDoesntEnumerate() var en = iterator as IEnumerator; Assert.False(en is not null && en.MoveNext()); } + + [Fact] + public void TupleRightJoin_Basic() + { + string[] outer = ["prakash", "tim"]; + string[] inner = ["prakash", "robert", "unknown"]; + + var result = outer.RightJoin(inner, o => o, i => i).ToList(); + + (string? Outer, string Inner)[] expected = + [ + ("prakash", "prakash"), + (null, "robert"), + (null, "unknown") + ]; + + Assert.Equal(expected, result); + } + + [Fact] + public void TupleRightJoin_EmptyOuter() + { + string[] outer = []; + string[] inner = ["prakash"]; + + var result = outer.RightJoin(inner, o => o, i => i).ToList(); + Assert.Single(result); + Assert.Null(result[0].Outer); + Assert.Equal("prakash", result[0].Inner); + } + + [Fact] + public void TupleRightJoin_EmptyInner() + { + string[] outer = ["Prakash"]; + string[] inner = Array.Empty(); + + Assert.Empty(outer.RightJoin(inner, o => o, i => i)); + } + + [Fact] + public void TupleRightJoin_WithComparer() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Tim", custID = 99021 } + ]; + AnagramRec[] inner = + [ + new AnagramRec{ name = "miT", orderID = 43455, total = 10 }, + new AnagramRec{ name = "Prakash", orderID = 323232, total = 9 } + ]; + + var result = outer.RightJoin(inner, o => o.name, i => i.name, new AnagramEqualityComparer()).ToList(); + + Assert.Equal(2, result.Count); + Assert.Contains(result, r => r.Outer.name == "Tim" && r.Inner.name == "miT"); + Assert.Contains(result, r => r.Outer.name == null && r.Inner.name == "Prakash"); + } + + [Fact] + public void TupleRightJoin_OuterNull() + { + CustomerRec[] outer = null; + OrderRec[] inner = [new OrderRec{ orderID = 45321, custID = 98022, total = 50 }]; + + AssertExtensions.Throws("outer", () => outer.RightJoin(inner, o => o.custID, i => i.custID)); + } + + [Fact] + public void TupleRightJoin_InnerNull() + { + CustomerRec[] outer = [new CustomerRec{ name = "Prakash", custID = 98022 }]; + OrderRec[] inner = null; + + AssertExtensions.Throws("inner", () => outer.RightJoin(inner, o => o.custID, i => i.custID)); + } + + [Fact] + public void TupleRightJoin_OuterKeySelectorNull() + { + CustomerRec[] outer = [new CustomerRec{ name = "Prakash", custID = 98022 }]; + OrderRec[] inner = [new OrderRec{ orderID = 45321, custID = 98022, total = 50 }]; + + AssertExtensions.Throws("outerKeySelector", () => outer.RightJoin(inner, (Func)null, i => i.custID)); + } + + [Fact] + public void TupleRightJoin_InnerKeySelectorNull() + { + CustomerRec[] outer = [new CustomerRec{ name = "Prakash", custID = 98022 }]; + OrderRec[] inner = [new OrderRec{ orderID = 45321, custID = 98022, total = 50 }]; + + AssertExtensions.Throws("innerKeySelector", () => outer.RightJoin(inner, o => o.custID, (Func)null)); + } } } From e3abcc564f65e4788182c21e2b4c7a2976a6ee26 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:37:27 +0300 Subject: [PATCH 2/4] Add GroupJoin overload returning IGrouping (#121999) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the approved API from #120587 — a simplified `GroupJoin` overload that removes the need for an explicit result selector, returning `IGrouping` where the outer element is the key and the correlated inner elements are the grouping contents. ## Description ### API ```csharp namespace System.Linq; public static partial class Enumerable { public static IEnumerable> GroupJoin( this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null); } public static partial class Queryable { public static IQueryable> GroupJoin( this IQueryable outer, IEnumerable inner, Expression> outerKeySelector, Expression> innerKeySelector, IEqualityComparer? comparer = null); } public static partial class AsyncEnumerable { public static IAsyncEnumerable> GroupJoin( this IAsyncEnumerable outer, IAsyncEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null); public static IAsyncEnumerable> GroupJoin( this IAsyncEnumerable outer, IAsyncEnumerable inner, Func> outerKeySelector, Func> innerKeySelector, IEqualityComparer? comparer = null); } ``` ### Changes - **System.Linq**: New `GroupJoin` overload with optional `IEqualityComparer?` parameter + internal `GroupJoinGrouping` wrapper class. XML doc comments with correct `` references added to the new public API. - **System.Linq.Queryable**: New overload with optional `IEqualityComparer?` parameter and `[DynamicDependency]` on `Enumerable.GroupJoin`3`. XML doc comments added. - **System.Linq.AsyncEnumerable**: Two new overloads (sync and async key selector variants) with optional `IEqualityComparer?` parameter + internal `AsyncGroupJoinGrouping` wrapper class. XML doc comments added. - Reference assemblies updated for all three projects. - Tests for all three projects, with `#if NET` guards in the async enumerable tests for net481 compatibility (the new `Enumerable.GroupJoin` overload and tuple-returning `Zip` are unavailable on .NET Framework 4.8.1). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com> Co-authored-by: Shay Rojansky Co-authored-by: roji <1862641+roji@users.noreply.github.com> Co-authored-by: Eirik Tsarpalis (cherry picked from commit 375857b4c453bbd2fd687039f867f0c49ce0f1be) --- .../ref/System.Linq.AsyncEnumerable.cs | 2 + .../src/System/Linq/GroupJoin.cs | 133 +++++++++++++ .../tests/GroupJoinTests.cs | 79 ++++++++ .../ref/System.Linq.Queryable.cs | 1 + .../src/System/Linq/Queryable.cs | 32 ++++ .../tests/GroupJoinTests.cs | 62 +++++++ src/libraries/System.Linq/ref/System.Linq.cs | 1 + .../System.Linq/src/System/Linq/GroupJoin.cs | 81 ++++++++ .../System.Linq/tests/GroupJoinTests.cs | 174 ++++++++++++++++++ 9 files changed, 565 insertions(+) diff --git a/src/libraries/System.Linq.AsyncEnumerable/ref/System.Linq.AsyncEnumerable.cs b/src/libraries/System.Linq.AsyncEnumerable/ref/System.Linq.AsyncEnumerable.cs index 36d3fb6f1efa5e..b13c0504a02459 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/ref/System.Linq.AsyncEnumerable.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/ref/System.Linq.AsyncEnumerable.cs @@ -73,6 +73,8 @@ public static partial class AsyncEnumerable public static System.Collections.Generic.IAsyncEnumerable GroupBy(this System.Collections.Generic.IAsyncEnumerable source, System.Func keySelector, System.Func, TResult> resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable GroupBy(this System.Collections.Generic.IAsyncEnumerable source, System.Func> keySelector, System.Func> elementSelector, System.Func, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask> resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable GroupBy(this System.Collections.Generic.IAsyncEnumerable source, System.Func keySelector, System.Func elementSelector, System.Func, TResult> resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable> GroupJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func> outerKeySelector, System.Func> innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } + public static System.Collections.Generic.IAsyncEnumerable> GroupJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable GroupJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func> outerKeySelector, System.Func> innerKeySelector, System.Func, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask> resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable GroupJoin(this System.Collections.Generic.IAsyncEnumerable outer, System.Collections.Generic.IAsyncEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func, TResult> resultSelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IAsyncEnumerable<(int Index, TSource Item)> Index(this System.Collections.Generic.IAsyncEnumerable source) { throw null; } diff --git a/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/GroupJoin.cs b/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/GroupJoin.cs index 3673aa85956f52..f00866f850bfbe 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/GroupJoin.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/GroupJoin.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; @@ -10,6 +11,120 @@ namespace System.Linq { public static partial class AsyncEnumerable { + /// Correlates the elements of two sequences based on key equality and groups the results. If is or omitted, the default equality comparer is used to compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// + /// An that contains elements of type + /// where each grouping contains the outer element as the key and the matching inner elements. + /// + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable> GroupJoin( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return + outer.IsKnownEmpty() ? Empty>() : + Impl(outer, inner, outerKeySelector, innerKeySelector, comparer, default); + + static async IAsyncEnumerable> Impl( + IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + IEqualityComparer? comparer, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await using IAsyncEnumerator e = outer.GetAsyncEnumerator(cancellationToken); + + if (await e.MoveNextAsync()) + { + AsyncLookup lookup = await AsyncLookup.CreateForJoinAsync(inner, innerKeySelector, comparer, cancellationToken); + do + { + TOuter item = e.Current; + yield return new AsyncGroupJoinGrouping(item, lookup[outerKeySelector(item)]); + } + while (await e.MoveNextAsync()); + } + } + } + + /// Correlates the elements of two sequences based on key equality and groups the results. If is or omitted, the default equality comparer is used to compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// + /// An that contains elements of type + /// where each grouping contains the outer element as the key and the matching inner elements. + /// + /// is . + /// is . + /// is . + /// is . + public static IAsyncEnumerable> GroupJoin( + this IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func> outerKeySelector, + Func> innerKeySelector, + IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return + outer.IsKnownEmpty() ? Empty>() : + Impl(outer, inner, outerKeySelector, innerKeySelector, comparer, default); + + static async IAsyncEnumerable> Impl( + IAsyncEnumerable outer, + IAsyncEnumerable inner, + Func> outerKeySelector, + Func> innerKeySelector, + IEqualityComparer? comparer, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await using IAsyncEnumerator e = outer.GetAsyncEnumerator(cancellationToken); + + if (await e.MoveNextAsync()) + { + AsyncLookup lookup = await AsyncLookup.CreateForJoinAsync(inner, innerKeySelector, comparer, cancellationToken); + do + { + TOuter item = e.Current; + yield return new AsyncGroupJoinGrouping( + item, + lookup[await outerKeySelector(item, cancellationToken)]); + } + while (await e.MoveNextAsync()); + } + } + } + /// Correlates the elements of two sequences based on key equality and groups the results. /// /// @@ -143,4 +258,22 @@ lookup[await outerKeySelector(item, cancellationToken)], } } } + + internal sealed class AsyncGroupJoinGrouping : IGrouping + { + private readonly TKey _key; + private readonly IEnumerable _elements; + + public AsyncGroupJoinGrouping(TKey key, IEnumerable elements) + { + _key = key; + _elements = elements; + } + + public TKey Key => _key; + + public IEnumerator GetEnumerator() => _elements.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } } diff --git a/src/libraries/System.Linq.AsyncEnumerable/tests/GroupJoinTests.cs b/src/libraries/System.Linq.AsyncEnumerable/tests/GroupJoinTests.cs index 305faf3c77681f..c09a68c9482822 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/tests/GroupJoinTests.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/tests/GroupJoinTests.cs @@ -26,6 +26,20 @@ public void InvalidInputs_Throws() AssertExtensions.Throws("resultSelector", () => AsyncEnumerable.GroupJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), async (outer, ct) => outer, async (inner, ct) => inner, (Func, CancellationToken, ValueTask>)null)); } + [Fact] + public void InvalidInputs_WithoutResultSelector_Throws() + { + AssertExtensions.Throws("outer", () => AsyncEnumerable.GroupJoin((IAsyncEnumerable)null, AsyncEnumerable.Empty(), outer => outer, inner => inner)); + AssertExtensions.Throws("inner", () => AsyncEnumerable.GroupJoin(AsyncEnumerable.Empty(), (IAsyncEnumerable)null, outer => outer, inner => inner)); + AssertExtensions.Throws("outerKeySelector", () => AsyncEnumerable.GroupJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), (Func)null, inner => inner)); + AssertExtensions.Throws("innerKeySelector", () => AsyncEnumerable.GroupJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), outer => outer, (Func)null)); + + AssertExtensions.Throws("outer", () => AsyncEnumerable.GroupJoin((IAsyncEnumerable)null, AsyncEnumerable.Empty(), async (outer, ct) => outer, async (inner, ct) => inner)); + AssertExtensions.Throws("inner", () => AsyncEnumerable.GroupJoin(AsyncEnumerable.Empty(), (IAsyncEnumerable)null, async (outer, ct) => outer, async (inner, ct) => inner)); + AssertExtensions.Throws("outerKeySelector", () => AsyncEnumerable.GroupJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), (Func>)null, async (inner, ct) => inner)); + AssertExtensions.Throws("innerKeySelector", () => AsyncEnumerable.GroupJoin(AsyncEnumerable.Empty(), AsyncEnumerable.Empty(), async (outer, ct) => outer, (Func>)null)); + } + [Fact] public void Empty_ProducesEmpty() // validating an optimization / implementation detail { @@ -33,6 +47,13 @@ public void Empty_ProducesEmpty() // validating an optimization / implementation Assert.Same(AsyncEnumerable.Empty(), AsyncEnumerable.Empty().GroupJoin(CreateSource(1, 2, 3), async (s, ct) => s, async (i, ct) => i.ToString(), async (s, e, ct) => s)); } + [Fact] + public void Empty_WithoutResultSelector_ProducesEmpty() + { + Assert.Same(AsyncEnumerable.Empty>(), AsyncEnumerable.Empty().GroupJoin(CreateSource(1, 2, 3), s => s, i => i.ToString())); + Assert.Same(AsyncEnumerable.Empty>(), AsyncEnumerable.Empty().GroupJoin(CreateSource(1, 2, 3), async (s, ct) => s, async (i, ct) => i.ToString())); + } + [Fact] public async Task VariousValues_MatchesEnumerable_String() { @@ -55,6 +76,38 @@ await AssertEqual( } } + [Fact] + public async Task VariousValues_WithoutResultSelector_MatchesEnumerable() + { + int[] outer = [1, 2, 3]; + int[] inner = [1, 2, 2, 3, 3, 3]; + + foreach (IAsyncEnumerable outerSource in CreateSources(outer)) + foreach (IAsyncEnumerable innerSource in CreateSources(inner)) + { +#if NET + var expected = outer.GroupJoin(inner, o => o, i => i); + var result = await outerSource.GroupJoin(innerSource, o => o, i => i).ToListAsync(); + + Assert.Equal(expected.Count(), result.Count); + foreach (var (exp, act) in expected.Zip(result)) + { + Assert.Equal(exp.Key, act.Key); + Assert.Equal(exp.ToList(), act.ToList()); + } + + var resultAsync = await outerSource.GroupJoin(innerSource, async (o, ct) => o, async (i, ct) => i).ToListAsync(); + + Assert.Equal(expected.Count(), resultAsync.Count); + foreach (var (exp, act) in expected.Zip(resultAsync)) + { + Assert.Equal(exp.Key, act.Key); + Assert.Equal(exp.ToList(), act.ToList()); + } +#endif + } + } + [Fact] public async Task Cancellation_Cancels() { @@ -167,5 +220,31 @@ public async Task InterfaceCalls_ExpectedCounts() Assert.Equal(4, inner.CurrentCount); Assert.Equal(1, inner.DisposeAsyncCount); } + + [Fact] + public async Task InterfaceCalls_WithoutResultSelector_ExpectedCounts() + { + TrackingAsyncEnumerable outer, inner; + + outer = CreateSource(2, 4, 8, 16).Track(); + inner = CreateSource(1, 2, 3, 4).Track(); + await ConsumeAsync(outer.GroupJoin(inner, outer => outer, inner => inner)); + Assert.Equal(5, outer.MoveNextAsyncCount); + Assert.Equal(4, outer.CurrentCount); + Assert.Equal(1, outer.DisposeAsyncCount); + Assert.Equal(5, inner.MoveNextAsyncCount); + Assert.Equal(4, inner.CurrentCount); + Assert.Equal(1, inner.DisposeAsyncCount); + + outer = CreateSource(2, 4, 8, 16).Track(); + inner = CreateSource(1, 2, 3, 4).Track(); + await ConsumeAsync(outer.GroupJoin(inner, async (outer, ct) => outer, async (inner, ct) => inner)); + Assert.Equal(5, outer.MoveNextAsyncCount); + Assert.Equal(4, outer.CurrentCount); + Assert.Equal(1, outer.DisposeAsyncCount); + Assert.Equal(5, inner.MoveNextAsyncCount); + Assert.Equal(4, inner.CurrentCount); + Assert.Equal(1, inner.DisposeAsyncCount); + } } } diff --git a/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs b/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs index 42a98a7cd3c3d9..82d85662557874 100644 --- a/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs +++ b/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs @@ -108,6 +108,7 @@ public static partial class Queryable public static System.Linq.IQueryable GroupBy(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> keySelector, System.Linq.Expressions.Expression, TResult>> resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } public static System.Linq.IQueryable GroupBy(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> keySelector, System.Linq.Expressions.Expression> elementSelector, System.Linq.Expressions.Expression, TResult>> resultSelector) { throw null; } public static System.Linq.IQueryable GroupBy(this System.Linq.IQueryable source, System.Linq.Expressions.Expression> keySelector, System.Linq.Expressions.Expression> elementSelector, System.Linq.Expressions.Expression, TResult>> resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } + public static System.Linq.IQueryable> GroupJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Linq.IQueryable GroupJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Linq.Expressions.Expression, TResult>> resultSelector) { throw null; } public static System.Linq.IQueryable GroupJoin(this System.Linq.IQueryable outer, System.Collections.Generic.IEnumerable inner, System.Linq.Expressions.Expression> outerKeySelector, System.Linq.Expressions.Expression> innerKeySelector, System.Linq.Expressions.Expression, TResult>> resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } public static System.Linq.IQueryable<(int Index, TSource Item)> Index(this System.Linq.IQueryable source) { throw null; } diff --git a/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs b/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs index 90b71b77de5049..09243d4c8431ac 100644 --- a/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs +++ b/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs @@ -408,6 +408,38 @@ public static IQueryable Join(this IQuer outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector), Expression.Quote(resultSelector), Expression.Constant(comparer, typeof(IEqualityComparer)))); } + /// Correlates the elements of two sequences based on key equality and groups the results. If is or omitted, the default equality comparer is used to compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// + /// An that contains elements of type + /// where each grouping contains the outer element as the key and the matching inner elements. + /// + /// is . + /// is . + /// is . + /// is . + [DynamicDependency("GroupJoin`3", typeof(Enumerable))] + public static IQueryable> GroupJoin(this IQueryable outer, IEnumerable inner, Expression> outerKeySelector, Expression> innerKeySelector, IEqualityComparer? comparer = null) + { + ArgumentNullException.ThrowIfNull(outer); + ArgumentNullException.ThrowIfNull(inner); + ArgumentNullException.ThrowIfNull(outerKeySelector); + ArgumentNullException.ThrowIfNull(innerKeySelector); + + return outer.Provider.CreateQuery>( + Expression.Call( + null, + new Func, IEnumerable, Expression>, Expression>, IEqualityComparer, IQueryable>>(GroupJoin).Method, + outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector), Expression.Constant(comparer, typeof(IEqualityComparer)))); + } + /// /// Correlates the elements of two sequences based on matching keys. If is or omitted, the default equality comparer is used to compare keys. /// diff --git a/src/libraries/System.Linq.Queryable/tests/GroupJoinTests.cs b/src/libraries/System.Linq.Queryable/tests/GroupJoinTests.cs index 3db5cb8cad203e..8d79f7e1c17919 100644 --- a/src/libraries/System.Linq.Queryable/tests/GroupJoinTests.cs +++ b/src/libraries/System.Linq.Queryable/tests/GroupJoinTests.cs @@ -287,5 +287,67 @@ public void GroupJoin2() var count = new[] { 0, 1, 2 }.AsQueryable().GroupJoin(new[] { 1, 2, 3 }, n1 => n1, n2 => n2, (n1, n2) => n1, EqualityComparer.Default).Count(); Assert.Equal(3, count); } + + [Fact] + public void GroupJoinWithoutResultSelector() + { + var result = new[] { 0, 1, 2 }.AsQueryable().GroupJoin(new[] { 1, 2, 3 }, n1 => n1, n2 => n2).ToList(); + Assert.Equal(3, result.Count); + Assert.Equal(0, result[0].Key); + Assert.Empty(result[0]); + Assert.Equal(1, result[1].Key); + Assert.Single(result[1]); + Assert.Equal(2, result[2].Key); + Assert.Single(result[2]); + } + + [Fact] + public void GroupJoinWithoutResultSelector_OuterNull() + { + IQueryable outer = null; + int[] inner = { 1, 2, 3 }; + + AssertExtensions.Throws("outer", () => outer.GroupJoin(inner.AsQueryable(), n1 => n1, n2 => n2)); + } + + [Fact] + public void GroupJoinWithoutResultSelector_InnerNull() + { + int[] outer = { 0, 1, 2 }; + IQueryable inner = null; + + AssertExtensions.Throws("inner", () => outer.AsQueryable().GroupJoin(inner, n1 => n1, n2 => n2)); + } + + [Fact] + public void GroupJoinWithoutResultSelector_OuterKeySelectorNull() + { + int[] outer = { 0, 1, 2 }; + int[] inner = { 1, 2, 3 }; + + AssertExtensions.Throws("outerKeySelector", () => outer.AsQueryable().GroupJoin(inner.AsQueryable(), null, n2 => n2)); + } + + [Fact] + public void GroupJoinWithoutResultSelector_InnerKeySelectorNull() + { + int[] outer = { 0, 1, 2 }; + int[] inner = { 1, 2, 3 }; + + AssertExtensions.Throws("innerKeySelector", () => outer.AsQueryable().GroupJoin(inner.AsQueryable(), n1 => n1, null)); + } + + [Fact] + public void GroupJoinWithoutResultSelector_CustomComparer() + { + var result = new[] { "Tim", "Bob", "Robert" }.AsQueryable().GroupJoin(new[] { "miT", "Robert" }, n1 => n1, n2 => n2, new AnagramEqualityComparer()).ToList(); + Assert.Equal(3, result.Count); + Assert.Equal("Tim", result[0].Key); + Assert.Single(result[0]); + Assert.Equal("Bob", result[1].Key); + Assert.Empty(result[1]); + Assert.Equal("Robert", result[2].Key); + Assert.Single(result[2]); + } } } diff --git a/src/libraries/System.Linq/ref/System.Linq.cs b/src/libraries/System.Linq/ref/System.Linq.cs index 976d567245c18b..8451e05ac4fab6 100644 --- a/src/libraries/System.Linq/ref/System.Linq.cs +++ b/src/libraries/System.Linq/ref/System.Linq.cs @@ -81,6 +81,7 @@ public static System.Collections.Generic.IEnumerable< public static System.Collections.Generic.IEnumerable GroupBy(this System.Collections.Generic.IEnumerable source, System.Func keySelector, System.Func, TResult> resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } public static System.Collections.Generic.IEnumerable GroupBy(this System.Collections.Generic.IEnumerable source, System.Func keySelector, System.Func elementSelector, System.Func, TResult> resultSelector) { throw null; } public static System.Collections.Generic.IEnumerable GroupBy(this System.Collections.Generic.IEnumerable source, System.Func keySelector, System.Func elementSelector, System.Func, TResult> resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } + public static System.Collections.Generic.IEnumerable> GroupJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Collections.Generic.IEqualityComparer? comparer = null) { throw null; } public static System.Collections.Generic.IEnumerable GroupJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func, TResult> resultSelector) { throw null; } public static System.Collections.Generic.IEnumerable GroupJoin(this System.Collections.Generic.IEnumerable outer, System.Collections.Generic.IEnumerable inner, System.Func outerKeySelector, System.Func innerKeySelector, System.Func, TResult> resultSelector, System.Collections.Generic.IEqualityComparer? comparer) { throw null; } public static System.Collections.Generic.IEnumerable<(int Index, TSource Item)> Index(this System.Collections.Generic.IEnumerable source) { throw null; } diff --git a/src/libraries/System.Linq/src/System/Linq/GroupJoin.cs b/src/libraries/System.Linq/src/System/Linq/GroupJoin.cs index b59ca859ec568c..0fefce7dfc842e 100644 --- a/src/libraries/System.Linq/src/System/Linq/GroupJoin.cs +++ b/src/libraries/System.Linq/src/System/Linq/GroupJoin.cs @@ -1,12 +1,60 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections; using System.Collections.Generic; namespace System.Linq { public static partial class Enumerable { + /// Correlates the elements of two sequences based on key equality and groups the results. If is or omitted, the default equality comparer is used to compare keys. + /// The type of the elements of the first sequence. + /// The type of the elements of the second sequence. + /// The type of the keys returned by the key selector functions. + /// The first sequence to join. + /// The sequence to join to the first sequence. + /// A function to extract the join key from each element of the first sequence. + /// A function to extract the join key from each element of the second sequence. + /// An to hash and compare keys, or to use . + /// + /// An that contains elements of type + /// where each grouping contains the outer element as the key and the matching inner elements. + /// + /// is . + /// is . + /// is . + /// is . + public static IEnumerable> GroupJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null) + { + if (outer is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.outer); + } + + if (inner is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.inner); + } + + if (outerKeySelector is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.outerKeySelector); + } + + if (innerKeySelector is null) + { + ThrowHelper.ThrowArgumentNullException(ExceptionArgument.innerKeySelector); + } + + if (IsEmptyArray(outer)) + { + return []; + } + + return GroupJoinIterator(outer, inner, outerKeySelector, innerKeySelector, comparer); + } + public static IEnumerable GroupJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func, TResult> resultSelector) => GroupJoin(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer: null); @@ -45,6 +93,21 @@ public static IEnumerable GroupJoin(this return GroupJoinIterator(outer, inner, outerKeySelector, innerKeySelector, resultSelector, comparer); } + private static IEnumerable> GroupJoinIterator(IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer) + { + using IEnumerator e = outer.GetEnumerator(); + if (e.MoveNext()) + { + Lookup lookup = Lookup.CreateForJoin(inner, innerKeySelector, comparer); + do + { + TOuter item = e.Current; + yield return new GroupJoinGrouping(item, lookup[outerKeySelector(item)]); + } + while (e.MoveNext()); + } + } + private static IEnumerable GroupJoinIterator(IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, Func, TResult> resultSelector, IEqualityComparer? comparer) { using IEnumerator e = outer.GetEnumerator(); @@ -60,4 +123,22 @@ private static IEnumerable GroupJoinIterator : IGrouping + { + private readonly TKey _key; + private readonly IEnumerable _elements; + + public GroupJoinGrouping(TKey key, IEnumerable elements) + { + _key = key; + _elements = elements; + } + + public TKey Key => _key; + + public IEnumerator GetEnumerator() => _elements.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } } diff --git a/src/libraries/System.Linq/tests/GroupJoinTests.cs b/src/libraries/System.Linq/tests/GroupJoinTests.cs index 833ada49a5d89c..9144b718423333 100644 --- a/src/libraries/System.Linq/tests/GroupJoinTests.cs +++ b/src/libraries/System.Linq/tests/GroupJoinTests.cs @@ -513,5 +513,179 @@ public void ForcedToEnumeratorDoesntEnumerate() var en = iterator as IEnumerator>; Assert.False(en is not null && en.MoveNext()); } + + [Fact] + public void GroupJoinWithoutResultSelector_OuterEmptyInnerNonEmpty() + { + CustomerRec[] outer = []; + OrderRec[] inner = + [ + new OrderRec{ orderID = 45321, custID = 98022, total = 50 }, + new OrderRec{ orderID = 97865, custID = 32103, total = 25 } + ]; + Assert.Empty(outer.GroupJoin(inner, e => e.custID, e => e.custID)); + } + + [Fact] + public void GroupJoinWithoutResultSelector_OuterNonEmptyInnerEmpty() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Tim", custID = 43434 }, + new CustomerRec{ name = "Bob", custID = 34093 } + ]; + OrderRec[] inner = []; + + var result = outer.GroupJoin(inner, e => e.custID, e => e.custID).ToList(); + + Assert.Equal(2, result.Count); + Assert.Equal(outer[0], result[0].Key); + Assert.Empty(result[0]); + Assert.Equal(outer[1], result[1].Key); + Assert.Empty(result[1]); + } + + [Fact] + public void GroupJoinWithoutResultSelector_SingleElementEachAndMatches() + { + CustomerRec[] outer = [new CustomerRec{ name = "Tim", custID = 43434 }]; + OrderRec[] inner = [new OrderRec{ orderID = 97865, custID = 43434, total = 25 }]; + + var result = outer.GroupJoin(inner, e => e.custID, e => e.custID).ToList(); + + Assert.Single(result); + Assert.Equal(outer[0], result[0].Key); + Assert.Single(result[0]); + Assert.Equal(inner[0], result[0].First()); + } + + [Fact] + public void GroupJoinWithoutResultSelector_InnerSameKeyMoreThanOneElementAndMatches() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Tim", custID = 1234 }, + new CustomerRec{ name = "Bob", custID = 9865 } + ]; + OrderRec[] inner = + [ + new OrderRec{ orderID = 97865, custID = 1234, total = 25 }, + new OrderRec{ orderID = 34390, custID = 1234, total = 19 }, + new OrderRec{ orderID = 34390, custID = 9865, total = 19 } + ]; + + var result = outer.GroupJoin(inner, e => e.custID, e => e.custID).ToList(); + + Assert.Equal(2, result.Count); + Assert.Equal(outer[0], result[0].Key); + Assert.Equal(2, result[0].Count()); + Assert.Equal(outer[1], result[1].Key); + Assert.Single(result[1]); + } + + [Fact] + public void GroupJoinWithoutResultSelector_OuterNull() + { + CustomerRec[] outer = null; + OrderRec[] inner = + [ + new OrderRec{ orderID = 45321, custID = 98022, total = 50 } + ]; + + AssertExtensions.Throws("outer", () => outer.GroupJoin(inner, e => e.custID, e => e.custID)); + } + + [Fact] + public void GroupJoinWithoutResultSelector_InnerNull() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Tim", custID = 1234 } + ]; + OrderRec[] inner = null; + + AssertExtensions.Throws("inner", () => outer.GroupJoin(inner, e => e.custID, e => e.custID)); + } + + [Fact] + public void GroupJoinWithoutResultSelector_OuterKeySelectorNull() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Tim", custID = 1234 } + ]; + OrderRec[] inner = + [ + new OrderRec{ orderID = 45321, custID = 98022, total = 50 } + ]; + + AssertExtensions.Throws("outerKeySelector", () => outer.GroupJoin(inner, null, e => e.custID)); + } + + [Fact] + public void GroupJoinWithoutResultSelector_InnerKeySelectorNull() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Tim", custID = 1234 } + ]; + OrderRec[] inner = + [ + new OrderRec{ orderID = 45321, custID = 98022, total = 50 } + ]; + + AssertExtensions.Throws("innerKeySelector", () => outer.GroupJoin(inner, e => e.custID, null)); + } + + [Fact] + public void GroupJoinWithoutResultSelector_CanIterateMultipleTimes() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Tim", custID = 1234 } + ]; + OrderRec[] inner = + [ + new OrderRec{ orderID = 97865, custID = 1234, total = 25 } + ]; + + var result = outer.GroupJoin(inner, e => e.custID, e => e.custID).ToList(); + + Assert.Single(result); + + // Iterate the grouped elements multiple times + Assert.Single(result[0]); + Assert.Single(result[0]); + Assert.Equal(inner[0], result[0].First()); + Assert.Equal(inner[0], result[0].First()); + } + + [Fact] + public void GroupJoinWithoutResultSelector_CustomComparer() + { + CustomerRec[] outer = + [ + new CustomerRec{ name = "Tim", custID = 1234 }, + new CustomerRec{ name = "Bob", custID = 9865 }, + new CustomerRec{ name = "Robert", custID = 9895 } + ]; + AnagramRec[] inner = + [ + new AnagramRec{ name = "Robert", orderID = 93483, total = 19 }, + new AnagramRec{ name = "miT", orderID = 93489, total = 45 } + ]; + + var result = outer.GroupJoin(inner, e => e.name, e => e.name, new AnagramEqualityComparer()).ToList(); + + Assert.Equal(3, result.Count); + Assert.Equal(outer[0], result[0].Key); + Assert.Single(result[0]); + Assert.Equal(inner[1], result[0].First()); + Assert.Equal(outer[1], result[1].Key); + Assert.Empty(result[1]); + Assert.Equal(outer[2], result[2].Key); + Assert.Single(result[2]); + Assert.Equal(inner[0], result[2].First()); + } } } From bca206b39d6a5a250aaffbb2851f14338a7b9b37 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Wed, 8 Apr 2026 18:47:34 +0300 Subject: [PATCH 3/4] Fix failing AsyncEnumerable tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/libraries/System.Linq.AsyncEnumerable/tests/JoinTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Linq.AsyncEnumerable/tests/JoinTests.cs b/src/libraries/System.Linq.AsyncEnumerable/tests/JoinTests.cs index d84c6cf05f7f2d..1b445bf936f7b3 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/tests/JoinTests.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/tests/JoinTests.cs @@ -219,11 +219,11 @@ public async Task TupleJoin_VariousValues_MatchesEnumerable_String() foreach (IAsyncEnumerable source in CreateSources(values)) { await AssertEqual( - values.Join(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner), + values.Join(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ', resultSelector: (inner, outer) => (Inner: inner, Outer: outer)).Select(t => t.Outer + t.Inner), source.Join(source, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner)); await AssertEqual( - values.Join(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner), + values.Join(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ', resultSelector: (inner, outer) => (Inner: inner, Outer: outer)).Select(t => t.Outer + t.Inner), source.Join(source, async (s, ct) => s.Length > 0 ? s[0] : ' ', async (s, ct) => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner)); } } From 7cb9b7e7cfd05d90a6268f319a8046e2d36f3ca2 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Thu, 9 Apr 2026 20:46:26 +0300 Subject: [PATCH 4/4] Fix swapped tuple field order in JoinTests expected sequence The resultSelector lambda had its parameter names swapped relative to the actual (outer, inner) positions, producing reversed concatenation in the expected sequence. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/libraries/System.Linq.AsyncEnumerable/tests/JoinTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Linq.AsyncEnumerable/tests/JoinTests.cs b/src/libraries/System.Linq.AsyncEnumerable/tests/JoinTests.cs index 1b445bf936f7b3..a9f2598c6b9c3e 100644 --- a/src/libraries/System.Linq.AsyncEnumerable/tests/JoinTests.cs +++ b/src/libraries/System.Linq.AsyncEnumerable/tests/JoinTests.cs @@ -219,11 +219,11 @@ public async Task TupleJoin_VariousValues_MatchesEnumerable_String() foreach (IAsyncEnumerable source in CreateSources(values)) { await AssertEqual( - values.Join(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ', resultSelector: (inner, outer) => (Inner: inner, Outer: outer)).Select(t => t.Outer + t.Inner), + values.Join(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ', resultSelector: (outer, inner) => (Outer: outer, Inner: inner)).Select(t => t.Outer + t.Inner), source.Join(source, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner)); await AssertEqual( - values.Join(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ', resultSelector: (inner, outer) => (Inner: inner, Outer: outer)).Select(t => t.Outer + t.Inner), + values.Join(values, s => s.Length > 0 ? s[0] : ' ', s => s.Length > 1 ? s[1] : ' ', resultSelector: (outer, inner) => (Outer: outer, Inner: inner)).Select(t => t.Outer + t.Inner), source.Join(source, async (s, ct) => s.Length > 0 ? s[0] : ' ', async (s, ct) => s.Length > 1 ? s[1] : ' ').Select(t => t.Outer + t.Inner)); } }