Skip to content

Add a GroupJoin overload without a result selector, returning an IGrouping #120587

@Thaina

Description

@Thaina

Motivation and Background

GroupJoin currently forces users to pass in a result selector which determines the shape of the return type. In most basic usages, users project out to an anonymous type (or a tuple) which wraps the returned TInner and TOuters - this proposal adds an additional overload of GroupJoin that removes to need to specify a result selector, and returns a tuple. We expect this will cover and simplify a large majority of the use-cases.

This proposal mirrors the simplified Zip overload, and also #120596 for Join.

API Proposal

namespace System.Linq;

public static partial class Enumerable
{
    public static IEnumerable<IGrouping<TOuter, TInner>> GroupJoin<TOuter, TInner, TKey>(
        this IEnumerable<TOuter> outer,
        IEnumerable<TInner> inner,
        Func<TOuter, TKey> outerKeySelector,
        Func<TInner, TKey> innerKeySelector,
        IEqualityComparer<TKey>? comparer = null);
}

public static partial class Queryable
{
    public static IQueryable<IGrouping<TOuter, TInner>> GroupJoin<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<IGrouping<TOuter, TInner>> GroupJoin<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<IGrouping<TOuter, TInner>> Join<TOuter, TInner, TKey>(
        this IAsyncEnumerable<TOuter> outer,
        IAsyncEnumerable<TInner> inner,
        Func<TOuter, TKey> outerKeySelector,
        Func<TInner, TKey> innerKeySelector,
        IEqualityComparer<TKey>? comparer = null);
}

Usage

var departments = new List<Department>
{
    new() { Id = 1, Name = "HR" },
    new() { Id = 2, Name = "IT" },
    new() { Id = 3, Name = "Finance" }
};

var employees = new List<Employee>
{
    new() { Name = "Alice", DepartmentId = 1 },
    new() { Name = "Bob", DepartmentId = 2 },
    new() { Name = "Charlie", DepartmentId = 2 },
    new() { Name = "David", DepartmentId = 3 }
};

// New proposed simplified usage:
foreach (var (emp, depts) in employees.GroupJoin(departments, e => e.DepartmentId, d => d.Id))
{
    Console.WriteLine($"Employee: {emp.Name}, departments: {string.Join(", ", depts.Select(i => i.Name))}");
}

// Existing usage: explicit projection out to tuple required
foreach (var (emp, depts) in employees.GroupJoin(departments, e => e.DepartmentId, d => d.Id, (e, d) => (e, d)))
{
    Console.WriteLine($"Employee: {emp.Name}, departments: {string.Join(", ", depts.Select(i => i.Name))}");
}

class Department
{
    public int Id { get; set; }
    public string Name { get; set; }
}

class Employee
{
    public string Name { get; set; }
    public int DepartmentId { get; set; }
}

Metadata

Metadata

Labels

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions