Repository navigation
Closed
Feature
Copy link
Labels
api-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implementedarea-System.Linq
Milestone
Description
Background and motivation
For simplicity of Join it should just return (TOuter,TInner) instead of the need for resultSelector
API Proposal
namespace System.Linq;
public static class Enumerable
{
public static IEnumerable<(TOuter Outer, TInner Inner)> Join<TOuter, TInner, TKey>(
this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
IEqualityComparer<TKey>? comparer = null);
public static IEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin<TOuter, TInner, TKey>(
this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
IEqualityComparer<TKey>? comparer = null);
public static IEnumerable<(TOuter? Outer, TInner Inner)> RightJoin<TOuter, TInner, TKey>(
this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
IEqualityComparer<TKey>? comparer = null);
}
public static class Queryable
{
public static IQueryable<(TOuter Outer, TInner Inner)> Join<TOuter, TInner, TKey>(
this IQueryable<TOuter> outer,
IEnumerable<TInner> inner,
Expression<Func<TOuter, TKey>> outerKeySelector,
Expression<Func<TInner, TKey>> innerKeySelector,
IEqualityComparer<TKey>? comparer = null);
public static IQueryable<(TOuter Outer, TInner? Inner)> LeftJoin<TOuter, TInner, TKey>(
this IQueryable<TOuter> outer,
IEnumerable<TInner> inner,
Expression<Func<TOuter, TKey>> outerKeySelector,
Expression<Func<TInner, TKey>> innerKeySelector,
IEqualityComparer<TKey>? comparer = null);
public static IQueryable<(TOuter? Outer, TInner Inner)> RightJoin<TOuter, TInner, TKey>(
this IQueryable<TOuter> outer,
IEnumerable<TInner> inner,
Expression<Func<TOuter, TKey>> outerKeySelector,
Expression<Func<TInner, TKey>> innerKeySelector,
IEqualityComparer<TKey>? comparer = null);
}
public static class AsyncEnumerable
{
public static IAsyncEnumerable<(TOuter Outer, TInner Inner)> Join<TOuter, TInner, TKey>(
this IAsyncEnumerable<TOuter> outer,
IAsyncEnumerable<TInner> inner,
Func<TOuter, CancellationToken, ValueTask<TKey>> outerKeySelector,
Func<TInner, CancellationToken, ValueTask<TKey>> innerKeySelector,
IEqualityComparer<TKey>? comparer = null);
public static IAsyncEnumerable<(TOuter Outer, TInner Inner)> Join<TOuter, TInner, TKey>(
this IAsyncEnumerable<TOuter> outer,
IAsyncEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
IEqualityComparer<TKey>? comparer = null);
public static IAsyncEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin<TOuter, TInner, TKey>(
this IAsyncEnumerable<TOuter> outer,
IAsyncEnumerable<TInner> inner,
Func<TOuter, CancellationToken, ValueTask<TKey>> outerKeySelector,
Func<TInner, CancellationToken, ValueTask<TKey>> innerKeySelector,
IEqualityComparer<TKey>? comparer = null);
public static IAsyncEnumerable<(TOuter Outer, TInner? Inner)> LeftJoin<TOuter, TInner, TKey>(
this IAsyncEnumerable<TOuter> outer,
IAsyncEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
IEqualityComparer<TKey>? comparer = null);
public static IAsyncEnumerable<(TOuter? Outer, TInner Inner)> RightJoin<TOuter, TInner, TKey>(
this IAsyncEnumerable<TOuter> outer,
IAsyncEnumerable<TInner> inner,
Func<TOuter, CancellationToken, ValueTask<TKey>> outerKeySelector,
Func<TInner, CancellationToken, ValueTask<TKey>> innerKeySelector,
IEqualityComparer<TKey>? comparer = null);
public static IAsyncEnumerable<(TOuter? Outer, TInner Inner)> RightJoin<TOuter, TInner, TKey>(
this IAsyncEnumerable<TOuter> outer,
IAsyncEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
IEqualityComparer<TKey>? comparer = null);
}API Usage
var keys = new[] { "x" ,"y" ,"z" };
var dict = new Dictionary<int,string>();
foreach (var (s,pair) in keys.Join(dict,(key) => key,(pair) => pair.Value))
Console.WriteLine(s + " : " pair.Key);Alternative Designs
Without this it need to make another lambda just for return tuple
foreach (var (s,pair) in keys.Join(dict,(key) => key,(pair) => pair.Value,(outer,inner) => (outer,inner)))
Console.WriteLine(s + " : " pair.Key);Also since the key might be calculated maybe the return should include key too?
public static partial class Enumerable
{
public static IEnumerable<(TKey,TOuter,TInner)> Join<TOuter, TInner, TKey>(this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter, TKey> outerKeySelector, Func<TInner, TKey> innerKeySelector);
}Risks
No response
Reacted by Tim Cassell
Reacted by Copilot
Metadata
Metadata
Assignees
Labels
api-approvedAPI was approved in API review, it can be implementedAPI was approved in API review, it can be implementedarea-System.Linq
Activity
MihuBot commentedon Oct 10, 2025
I'm a bot. Here are possible related and/or duplicate issues (I may be wrong):
eiriktsarpalis commentedon Oct 10, 2025
Adding an overload that doesn't require a resultSelector seems doable, although I'm not sure what our design guidance is for methods in the same group having different return types.
Shouldn't this also be returning the key, aka
IEnumerable<(TKey, TOuter,TInner)>?cc @bartonjs @roji
timcassell commentedon Oct 12, 2025
It should not return the TKey, the existing resultSelector only passes TOuter and TInner. If you were to return the key, it would have to return
TKey innerKeyandTKey outerKey, and it's not really useful since you can just get the keys again from the TOuter and TInner items.roji commentedon Oct 16, 2025
FWIW I'm also not sure we have public APIs returning tuples, though I agree that the proposed Join overloads would be useful; tons of code out there projects to an anonymous type via the result selector:
x.Join(y, x => x.Id, y => y.Id, (x, y) => new { X = x, Y = y }); these usages would be simplified, with the added bonus of the returned tuple being deconstructible.timcassell commentedon Oct 16, 2025
There is precedent with the
Zipextension. The original used a resultSelector, then overloads were later added that return tuple.roji commentedon Oct 17, 2025
Good point. Yeah, Join is conceptually very similar to Zip - both correlate items from two streams and return those items (or a projection thereof), the difference is in the correlation method (Join is key-based, Zip is position-based).
So I'd say this indeed makes sense and would make a nice quality-of-life improvement.
10 remaining items
manandre commentedon Nov 24, 2025
@roji @eiriktsarpalis For consistency, similar overloads should be added to
System.Linq.AsyncEnumerable, recently added in .NET 10 (#79782)Could we add them to the current issue or do we have to create a new one?
eiriktsarpalis commentedon Nov 25, 2025
Good callout, similar to IQueryable we should ensure IAE also has parity.
manandre commentedon Nov 25, 2025
API Proposal (IAsyncEnumerable)
eiriktsarpalis commentedon Nov 26, 2025
Updated.
Fix dotnet#120596: Add tuple-returning Join overload
Fix dotnet#120596: Add tuple-returning Join overload
[-][API Proposal]: Linq Join return tuple similar to Zip[/-][+]Add Join/LeftJoin/RightJoin overloads without a result selector, returning tuples[/+]Add Join/LeftJoin/RightJoin tuple overloads to LINQ APIs (dotnet#121998)
Add Join/LeftJoin/RightJoin tuple overloads to LINQ APIs (dotnet#121998)
Add Join/LeftJoin/RightJoin tuple overloads to LINQ APIs (dotnet#121998)
Add Join/LeftJoin/RightJoin tuple overloads to LINQ APIs (#121998)