From 7a75525ad0366137556e513baac4527ebc365598 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:38:43 +0000 Subject: [PATCH 1/6] Initial plan From dd8a7d8c435fc752ce95975ac9de26ddac032ca1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:10:49 +0000 Subject: [PATCH 2/6] Add Join/LeftJoin/RightJoin tuple overloads to Enumerable, Queryable, AsyncEnumerable Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com> --- .../ref/System.Linq.AsyncEnumerable.cs | 6 + .../src/System/Linq/Join.cs | 135 ++++++++++++++ .../src/System/Linq/LeftJoin.cs | 131 ++++++++++++++ .../src/System/Linq/RightJoin.cs | 132 ++++++++++++++ .../ref/System.Linq.Queryable.cs | 6 + .../src/System/Linq/Queryable.cs | 165 ++++++++++++++++++ src/libraries/System.Linq/ref/System.Linq.cs | 6 + .../System.Linq/src/System/Linq/Join.cs | 104 +++++++++++ .../System.Linq/src/System/Linq/LeftJoin.cs | 106 +++++++++++ .../System.Linq/src/System/Linq/RightJoin.cs | 106 +++++++++++ 10 files changed, 897 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.Queryable/ref/System.Linq.Queryable.cs b/src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs index 2d63b858f0bd26..16399622a8614f 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,8 @@ 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) { 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) { 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 +127,8 @@ 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) { 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) { 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 +162,8 @@ 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) { 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) { 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..635abb385d66b9 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,61 @@ 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. + /// + /// 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. + /// 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) + { + 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>, IQueryable<(TOuter Outer, TInner Inner)>>(Join).Method, + outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector))); + } + + /// + /// Correlates the elements of two sequences based on matching keys. A specified 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. + /// 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) + { + 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 +724,61 @@ 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. + /// + /// 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. + /// 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) + { + 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>, IQueryable<(TOuter Outer, TInner? Inner)>>(LeftJoin).Method, + outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector))); + } + + /// + /// Correlates the elements of two sequences based on matching keys. A specified 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. + /// 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) + { + 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 +1204,61 @@ 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. + /// + /// 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. + /// 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) + { + 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>, IQueryable<(TOuter? Outer, TInner Inner)>>(RightJoin).Method, + outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector))); + } + + /// + /// Correlates the elements of two sequences based on matching keys. A specified 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. + /// 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) + { + 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/ref/System.Linq.cs b/src/libraries/System.Linq/ref/System.Linq.cs index 0c2d82b2111766..a42b352216d574 100644 --- a/src/libraries/System.Linq/ref/System.Linq.cs +++ b/src/libraries/System.Linq/ref/System.Linq.cs @@ -91,6 +91,8 @@ 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) { 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) { 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 +101,8 @@ 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) { 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) { 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 +171,8 @@ 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) { 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) { 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..9ad9b0cf6ef73d 100644 --- a/src/libraries/System.Linq/src/System/Linq/Join.cs +++ b/src/libraries/System.Linq/src/System/Linq/Join.cs @@ -272,5 +272,109 @@ private static IEnumerable JoinIterator( } } } + + /// + /// 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. + /// 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. + /// + /// + /// The default equality comparer, , is used to hash and compare keys. + /// + /// + public static IEnumerable<(TOuter Outer, TInner Inner)> Join(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector) => + Join(outer, inner, outerKeySelector, innerKeySelector, comparer: null); + + /// + /// Correlates the elements of two sequences based on matching keys. A specified 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. + /// 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) + { + 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..aea4bc218b43e0 100644 --- a/src/libraries/System.Linq/src/System/Linq/LeftJoin.cs +++ b/src/libraries/System.Linq/src/System/Linq/LeftJoin.cs @@ -272,5 +272,111 @@ private static IEnumerable LeftJoinIterator + /// Correlates the elements of two sequences based on matching keys. 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. + /// 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. + /// + /// + /// The default equality comparer, , is used to hash and compare keys. + /// + /// + public static IEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector) => + LeftJoin(outer, inner, outerKeySelector, innerKeySelector, comparer: null); + + /// + /// Correlates the elements of two sequences based on matching keys. A specified 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. + /// 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) + { + 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..d0af0d820cf5f6 100644 --- a/src/libraries/System.Linq/src/System/Linq/RightJoin.cs +++ b/src/libraries/System.Linq/src/System/Linq/RightJoin.cs @@ -270,5 +270,111 @@ private static IEnumerable RightJoinIterator + /// Correlates the elements of two sequences based on matching keys. 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. + /// 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. + /// + /// + /// The default equality comparer, , is used to hash and compare keys. + /// + /// + public static IEnumerable<(TOuter? Outer, TInner Inner)> RightJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector) => + RightJoin(outer, inner, outerKeySelector, innerKeySelector, comparer: null); + + /// + /// Correlates the elements of two sequences based on matching keys. A specified 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. + /// 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) + { + 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()); + } + } } } From afae193732cd241fc5cc6cad39bb53c1ff9c4a04 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 26 Nov 2025 20:22:29 +0000 Subject: [PATCH 3/6] Add tests for Join/LeftJoin/RightJoin tuple overloads Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com> --- src/libraries/System.Linq/tests/JoinTests.cs | 104 ++++++++++++++++++ .../System.Linq/tests/LeftJoinTests.cs | 91 +++++++++++++++ .../System.Linq/tests/RightJoinTests.cs | 91 +++++++++++++++ 3 files changed, 286 insertions(+) diff --git a/src/libraries/System.Linq/tests/JoinTests.cs b/src/libraries/System.Linq/tests/JoinTests.cs index 4504fc47521d5c..592560325689ac 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); + + 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_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..dfccfef36d9734 100644 --- a/src/libraries/System.Linq/tests/LeftJoinTests.cs +++ b/src/libraries/System.Linq/tests/LeftJoinTests.cs @@ -446,5 +446,96 @@ 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(); + + Assert.Equal(3, result.Count); + Assert.Contains(result, r => r.Outer == "Prakash" && r.Inner == "prakash"); + Assert.Contains(result, r => r.Outer == "Tim" && r.Inner == null); + Assert.Contains(result, r => r.Outer == "Robert" && r.Inner == "robert"); + } + + [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.orderID == 0); + 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..6b402005610349 100644 --- a/src/libraries/System.Linq/tests/RightJoinTests.cs +++ b/src/libraries/System.Linq/tests/RightJoinTests.cs @@ -443,5 +443,96 @@ 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(); + + Assert.Equal(3, result.Count); + Assert.Contains(result, r => r.Outer == "prakash" && r.Inner == "prakash"); + Assert.Contains(result, r => r.Outer == null && r.Inner == "robert"); + Assert.Contains(result, r => r.Outer == null && r.Inner == "unknown"); + } + + [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 95b9985b4c26523520c97fb21eef303255d38a25 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Nov 2025 13:37:14 +0000 Subject: [PATCH 4/6] Consolidate Join/LeftJoin/RightJoin tuple overloads to use optional comparer parameter Co-authored-by: eiriktsarpalis <2813363+eiriktsarpalis@users.noreply.github.com> --- .../ref/System.Linq.Queryable.cs | 9 +- .../src/System/Linq/Queryable.cs | 87 +------------------ src/libraries/System.Linq/ref/System.Linq.cs | 9 +- .../System.Linq/src/System/Linq/Join.cs | 28 +----- .../System.Linq/src/System/Linq/LeftJoin.cs | 28 +----- .../System.Linq/src/System/Linq/RightJoin.cs | 28 +----- 6 files changed, 12 insertions(+), 177 deletions(-) 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 16399622a8614f..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,8 +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) { 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) { 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; } @@ -127,8 +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) { 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) { 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; } @@ -162,8 +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) { 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) { 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 635abb385d66b9..f213744e9cccf2 100644 --- a/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs +++ b/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs @@ -408,33 +408,6 @@ 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. - /// - /// 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. - /// 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) - { - 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>, IQueryable<(TOuter Outer, TInner Inner)>>(Join).Method, - outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector))); - } - /// /// Correlates the elements of two sequences based on matching keys. A specified is used to compare keys. /// @@ -449,7 +422,7 @@ public static IQueryable Join(this IQuer /// 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) + 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); @@ -724,33 +697,6 @@ 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. - /// - /// 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. - /// 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) - { - 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>, IQueryable<(TOuter Outer, TInner? Inner)>>(LeftJoin).Method, - outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector))); - } - /// /// Correlates the elements of two sequences based on matching keys. A specified is used to compare keys. /// @@ -765,7 +711,7 @@ public static IQueryable LeftJoin(this I /// 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) + 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); @@ -1204,33 +1150,6 @@ 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. - /// - /// 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. - /// 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) - { - 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>, IQueryable<(TOuter? Outer, TInner Inner)>>(RightJoin).Method, - outer.Expression, GetSourceExpression(inner), Expression.Quote(outerKeySelector), Expression.Quote(innerKeySelector))); - } - /// /// Correlates the elements of two sequences based on matching keys. A specified is used to compare keys. /// @@ -1245,7 +1164,7 @@ public static IQueryable RightJoin(this /// 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) + 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); diff --git a/src/libraries/System.Linq/ref/System.Linq.cs b/src/libraries/System.Linq/ref/System.Linq.cs index a42b352216d574..976d567245c18b 100644 --- a/src/libraries/System.Linq/ref/System.Linq.cs +++ b/src/libraries/System.Linq/ref/System.Linq.cs @@ -91,8 +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) { 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) { 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; } @@ -101,8 +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) { 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) { 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; } @@ -171,8 +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) { 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) { 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 9ad9b0cf6ef73d..ef87c1c92d1368 100644 --- a/src/libraries/System.Linq/src/System/Linq/Join.cs +++ b/src/libraries/System.Linq/src/System/Linq/Join.cs @@ -273,32 +273,6 @@ private static IEnumerable JoinIterator( } } - /// - /// 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. - /// 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. - /// - /// - /// The default equality comparer, , is used to hash and compare keys. - /// - /// - public static IEnumerable<(TOuter Outer, TInner Inner)> Join(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector) => - Join(outer, inner, outerKeySelector, innerKeySelector, comparer: null); - /// /// Correlates the elements of two sequences based on matching keys. A specified is used to compare keys. /// @@ -320,7 +294,7 @@ private static IEnumerable JoinIterator( /// 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) + public static IEnumerable<(TOuter Outer, TInner Inner)> Join(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null) { if (outer is null) { diff --git a/src/libraries/System.Linq/src/System/Linq/LeftJoin.cs b/src/libraries/System.Linq/src/System/Linq/LeftJoin.cs index aea4bc218b43e0..7bc67297c243a2 100644 --- a/src/libraries/System.Linq/src/System/Linq/LeftJoin.cs +++ b/src/libraries/System.Linq/src/System/Linq/LeftJoin.cs @@ -273,32 +273,6 @@ private static IEnumerable LeftJoinIterator - /// Correlates the elements of two sequences based on matching keys. 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. - /// 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. - /// - /// - /// The default equality comparer, , is used to hash and compare keys. - /// - /// - public static IEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector) => - LeftJoin(outer, inner, outerKeySelector, innerKeySelector, comparer: null); - /// /// Correlates the elements of two sequences based on matching keys. A specified is used to compare keys. /// @@ -320,7 +294,7 @@ private static IEnumerable LeftJoinIteratorforeach 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) + public static IEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null) { if (outer is null) { diff --git a/src/libraries/System.Linq/src/System/Linq/RightJoin.cs b/src/libraries/System.Linq/src/System/Linq/RightJoin.cs index d0af0d820cf5f6..eb812c132dc0eb 100644 --- a/src/libraries/System.Linq/src/System/Linq/RightJoin.cs +++ b/src/libraries/System.Linq/src/System/Linq/RightJoin.cs @@ -271,32 +271,6 @@ private static IEnumerable RightJoinIterator - /// Correlates the elements of two sequences based on matching keys. 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. - /// 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. - /// - /// - /// The default equality comparer, , is used to hash and compare keys. - /// - /// - public static IEnumerable<(TOuter? Outer, TInner Inner)> RightJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector) => - RightJoin(outer, inner, outerKeySelector, innerKeySelector, comparer: null); - /// /// Correlates the elements of two sequences based on matching keys. A specified is used to compare keys. /// @@ -318,7 +292,7 @@ private static IEnumerable RightJoinIteratorforeach 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) + public static IEnumerable<(TOuter? Outer, TInner Inner)> RightJoin(this IEnumerable outer, IEnumerable inner, Func outerKeySelector, Func innerKeySelector, IEqualityComparer? comparer = null) { if (outer is null) { From 078849da4d7ff07090e7f47a4590a03f1770937c Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Fri, 3 Apr 2026 19:00:01 +0200 Subject: [PATCH 5/6] Improve tests --- .../tests/JoinTests.cs | 78 ++++++++++++++++ .../tests/LeftJoinTests.cs | 80 +++++++++++++++++ .../tests/RightJoinTests.cs | 80 +++++++++++++++++ .../System.Linq.Queryable/tests/JoinTests.cs | 90 +++++++++++++++++++ .../tests/LeftJoinTests.cs | 90 +++++++++++++++++++ .../tests/RightJoinTests.cs | 90 +++++++++++++++++++ .../System.Linq/tests/LeftJoinTests.cs | 2 +- 7 files changed, 509 insertions(+), 1 deletion(-) 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/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/tests/LeftJoinTests.cs b/src/libraries/System.Linq/tests/LeftJoinTests.cs index dfccfef36d9734..0b4c27a440c756 100644 --- a/src/libraries/System.Linq/tests/LeftJoinTests.cs +++ b/src/libraries/System.Linq/tests/LeftJoinTests.cs @@ -498,7 +498,7 @@ public void TupleLeftJoin_WithComparer() 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.orderID == 0); + Assert.Contains(result, r => r.Outer.name == "Prakash" && r.Inner.name == null); Assert.Contains(result, r => r.Outer.name == "Tim" && r.Inner.name == "miT"); } From ed2cd44c46584ce2f3a53f411acece7e02052186 Mon Sep 17 00:00:00 2001 From: Shay Rojansky Date: Sat, 4 Apr 2026 09:23:17 +0200 Subject: [PATCH 6/6] Address review feedback: fix XML docs for optional comparer and strengthen tuple tests - Update XML doc summaries and comparer param docs on all tuple-returning Join/LeftJoin/RightJoin overloads (Enumerable and Queryable) to clarify that null/omitted comparer falls back to EqualityComparer.Default. - Replace Count/Contains assertions in TupleJoin_Basic, TupleLeftJoin_Basic, and TupleRightJoin_Basic with exact sequence equality checks to verify ordering and multiplicity semantics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/System/Linq/Queryable.cs | 12 ++++++------ src/libraries/System.Linq/src/System/Linq/Join.cs | 4 ++-- .../System.Linq/src/System/Linq/LeftJoin.cs | 4 ++-- .../System.Linq/src/System/Linq/RightJoin.cs | 4 ++-- src/libraries/System.Linq/tests/JoinTests.cs | 6 +++--- src/libraries/System.Linq/tests/LeftJoinTests.cs | 11 +++++++---- src/libraries/System.Linq/tests/RightJoinTests.cs | 12 ++++++++---- 7 files changed, 30 insertions(+), 23 deletions(-) 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 f213744e9cccf2..90b71b77de5049 100644 --- a/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs +++ b/src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs @@ -409,13 +409,13 @@ public static IQueryable Join(this IQuer } /// - /// Correlates the elements of two sequences based on matching keys. A specified is used to compare keys. + /// 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. + /// 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. @@ -698,13 +698,13 @@ public static IQueryable LeftJoin(this I } /// - /// Correlates the elements of two sequences based on matching keys. A specified is used to compare keys. + /// 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. + /// 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. @@ -1151,13 +1151,13 @@ public static IQueryable RightJoin(this } /// - /// Correlates the elements of two sequences based on matching keys. A specified is used to compare keys. + /// 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. + /// 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. diff --git a/src/libraries/System.Linq/src/System/Linq/Join.cs b/src/libraries/System.Linq/src/System/Linq/Join.cs index ef87c1c92d1368..265757b021cf96 100644 --- a/src/libraries/System.Linq/src/System/Linq/Join.cs +++ b/src/libraries/System.Linq/src/System/Linq/Join.cs @@ -274,13 +274,13 @@ private static IEnumerable JoinIterator( } /// - /// Correlates the elements of two sequences based on matching keys. A specified is used to compare keys. + /// 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. + /// 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. diff --git a/src/libraries/System.Linq/src/System/Linq/LeftJoin.cs b/src/libraries/System.Linq/src/System/Linq/LeftJoin.cs index 7bc67297c243a2..59fa354aa928f8 100644 --- a/src/libraries/System.Linq/src/System/Linq/LeftJoin.cs +++ b/src/libraries/System.Linq/src/System/Linq/LeftJoin.cs @@ -274,13 +274,13 @@ private static IEnumerable LeftJoinIterator - /// Correlates the elements of two sequences based on matching keys. A specified is used to compare keys. + /// 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. + /// 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. diff --git a/src/libraries/System.Linq/src/System/Linq/RightJoin.cs b/src/libraries/System.Linq/src/System/Linq/RightJoin.cs index eb812c132dc0eb..e54a0aea76ebe0 100644 --- a/src/libraries/System.Linq/src/System/Linq/RightJoin.cs +++ b/src/libraries/System.Linq/src/System/Linq/RightJoin.cs @@ -272,13 +272,13 @@ private static IEnumerable RightJoinIterator - /// Correlates the elements of two sequences based on matching keys. A specified is used to compare keys. + /// 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. + /// 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. diff --git a/src/libraries/System.Linq/tests/JoinTests.cs b/src/libraries/System.Linq/tests/JoinTests.cs index 592560325689ac..5fc9571f0854b5 100644 --- a/src/libraries/System.Linq/tests/JoinTests.cs +++ b/src/libraries/System.Linq/tests/JoinTests.cs @@ -436,9 +436,9 @@ public void TupleJoin_Basic() var result = outer.Join(inner, o => o.custID, i => i.custID); - 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); + var expected = outer.Join(inner, o => o.custID, i => i.custID, (o, i) => (Outer: o, Inner: i)); + + Assert.Equal(expected, result); } [Fact] diff --git a/src/libraries/System.Linq/tests/LeftJoinTests.cs b/src/libraries/System.Linq/tests/LeftJoinTests.cs index 0b4c27a440c756..c5daddd5ec4182 100644 --- a/src/libraries/System.Linq/tests/LeftJoinTests.cs +++ b/src/libraries/System.Linq/tests/LeftJoinTests.cs @@ -455,10 +455,13 @@ public void TupleLeftJoin_Basic() var result = outer.LeftJoin(inner, o => o.ToLowerInvariant(), i => i.ToLowerInvariant()).ToList(); - Assert.Equal(3, result.Count); - Assert.Contains(result, r => r.Outer == "Prakash" && r.Inner == "prakash"); - Assert.Contains(result, r => r.Outer == "Tim" && r.Inner == null); - Assert.Contains(result, r => r.Outer == "Robert" && r.Inner == "robert"); + var expected = outer.LeftJoin( + inner, + o => o.ToLowerInvariant(), + i => i.ToLowerInvariant(), + (o, i) => (Outer: o, Inner: i)).ToList(); + + Assert.Equal(expected, result); } [Fact] diff --git a/src/libraries/System.Linq/tests/RightJoinTests.cs b/src/libraries/System.Linq/tests/RightJoinTests.cs index 6b402005610349..433b1fc21165af 100644 --- a/src/libraries/System.Linq/tests/RightJoinTests.cs +++ b/src/libraries/System.Linq/tests/RightJoinTests.cs @@ -452,10 +452,14 @@ public void TupleRightJoin_Basic() var result = outer.RightJoin(inner, o => o, i => i).ToList(); - Assert.Equal(3, result.Count); - Assert.Contains(result, r => r.Outer == "prakash" && r.Inner == "prakash"); - Assert.Contains(result, r => r.Outer == null && r.Inner == "robert"); - Assert.Contains(result, r => r.Outer == null && r.Inner == "unknown"); + (string? Outer, string Inner)[] expected = + [ + ("prakash", "prakash"), + (null, "robert"), + (null, "unknown") + ]; + + Assert.Equal(expected, result); } [Fact]