Skip to content

Implement LINQ FullJoin operators#127236

Open
eiriktsarpalis wants to merge 1 commit intodotnet:mainfrom
eiriktsarpalis:feature/full-join
Open

Implement LINQ FullJoin operators#127236
eiriktsarpalis wants to merge 1 commit intodotnet:mainfrom
eiriktsarpalis:feature/full-join

Conversation

@eiriktsarpalis
Copy link
Copy Markdown
Member

Note

This PR description was generated with GitHub Copilot.

Fixes #124787.

Summary

  • add FullJoin to Enumerable, Queryable, and AsyncEnumerable
  • use the approved API shape with a single optional IEqualityComparer<TKey>? comparer = null parameter
  • include the corresponding ref updates and test coverage

Add FullJoin for Enumerable, Queryable, and AsyncEnumerable using the approved optional comparer API shape.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-linq
See info in area-owners.md if you want to be subscribed.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new LINQ FullJoin operator across Enumerable, Queryable, and AsyncEnumerable, including reference-assembly updates and new unit tests, to support full outer join semantics in .NET’s LINQ surface area.

Changes:

  • Introduces Enumerable.FullJoin implementation and Queryable.FullJoin expression-tree entry points.
  • Adds AsyncEnumerable.FullJoin (including async key selector variants) plus ref/csproj wiring.
  • Adds new test coverage for System.Linq, System.Linq.Queryable, and System.Linq.AsyncEnumerable.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/libraries/System.Linq/tests/System.Linq.Tests.csproj Includes new FullJoinTests.cs in System.Linq test project.
src/libraries/System.Linq/tests/FullJoinTests.cs Adds unit tests for Enumerable.FullJoin, including tuple overloads.
src/libraries/System.Linq/src/System/Linq/FullJoin.cs Implements Enumerable.FullJoin iterator logic and public APIs.
src/libraries/System.Linq/src/System.Linq.csproj Adds System\\Linq\\FullJoin.cs to the build.
src/libraries/System.Linq/ref/System.Linq.cs Adds FullJoin APIs to System.Linq reference assembly.
src/libraries/System.Linq.Queryable/tests/System.Linq.Queryable.Tests.csproj Includes new FullJoinTests.cs in Queryable test project.
src/libraries/System.Linq.Queryable/tests/FullJoinTests.cs Adds unit tests for Queryable.FullJoin (including tuple overloads).
src/libraries/System.Linq.Queryable/src/System/Linq/Queryable.cs Adds Queryable.FullJoin methods producing MethodCallExpressions.
src/libraries/System.Linq.Queryable/ref/System.Linq.Queryable.cs Adds FullJoin APIs to System.Linq.Queryable reference assembly.
src/libraries/System.Linq.AsyncEnumerable/tests/System.Linq.AsyncEnumerable.Tests.csproj Includes new FullJoinTests.cs in AsyncEnumerable test project.
src/libraries/System.Linq.AsyncEnumerable/tests/FullJoinTests.cs Adds tests for async result-selector overloads (validation/cancellation/etc.).
src/libraries/System.Linq.AsyncEnumerable/src/System/Linq/FullJoin.cs Implements AsyncEnumerable.FullJoin (sync + async key selector variants) and tuple overloads.
src/libraries/System.Linq.AsyncEnumerable/src/System.Linq.AsyncEnumerable.csproj Adds System\\Linq\\FullJoin.cs to the build.
src/libraries/System.Linq.AsyncEnumerable/ref/System.Linq.AsyncEnumerable.cs Adds FullJoin APIs to System.Linq.AsyncEnumerable reference assembly.

Comment on lines +54 to +58
AsyncLookup<TKey, TInner> innerLookup = await AsyncLookup<TKey, TInner>.CreateForJoinAsync(inner, innerKeySelector, comparer, cancellationToken);

HashSet<Grouping<TKey, TInner>>? matchedGroupings = innerLookup.Count != 0
? new HashSet<Grouping<TKey, TInner>>()
: null;
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

AsyncLookup<TKey, TInner>.CreateForJoinAsync filters out elements whose key is null (see AsyncLookup.CreateForJoinAsync), so any inner elements with a null key are never stored in innerLookup and thus will never be yielded as unmatched inner rows. This can cause FullJoin to drop inner elements entirely. Consider tracking null-key inner elements separately during lookup creation and yielding them as (default, innerItem) at the end (and adding equivalent logic for the async-key-selector overload).

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +27
[Fact]
public void InvalidInputs_Throws()
{
AssertExtensions.Throws<ArgumentNullException>("outer", () => AsyncEnumerable.FullJoin((IAsyncEnumerable<string>)null, AsyncEnumerable.Empty<string>(), outer => outer, inner => inner, (outer, inner) => outer + inner));
AssertExtensions.Throws<ArgumentNullException>("inner", () => AsyncEnumerable.FullJoin(AsyncEnumerable.Empty<string>(), (IAsyncEnumerable<string>)null, outer => outer, inner => inner, (outer, inner) => outer + inner));
AssertExtensions.Throws<ArgumentNullException>("outerKeySelector", () => AsyncEnumerable.FullJoin(AsyncEnumerable.Empty<string>(), AsyncEnumerable.Empty<string>(), (Func<string, string>)null, inner => inner, (outer, inner) => outer + inner));
AssertExtensions.Throws<ArgumentNullException>("innerKeySelector", () => AsyncEnumerable.FullJoin(AsyncEnumerable.Empty<string>(), AsyncEnumerable.Empty<string>(), outer => outer, (Func<string, string>)null, (outer, inner) => outer + inner));
AssertExtensions.Throws<ArgumentNullException>("resultSelector", () => AsyncEnumerable.FullJoin(AsyncEnumerable.Empty<string>(), AsyncEnumerable.Empty<string>(), outer => outer, inner => inner, (Func<string, string, string>)null));

AssertExtensions.Throws<ArgumentNullException>("outer", () => AsyncEnumerable.FullJoin((IAsyncEnumerable<string>)null, AsyncEnumerable.Empty<string>(), async (outer, ct) => outer, async (inner, ct) => inner, async (outer, inner, ct) => outer + inner));
AssertExtensions.Throws<ArgumentNullException>("inner", () => AsyncEnumerable.FullJoin(AsyncEnumerable.Empty<string>(), (IAsyncEnumerable<string>)null, async (outer, ct) => outer, async (inner, ct) => inner, async (outer, inner, ct) => outer + inner));
AssertExtensions.Throws<ArgumentNullException>("outerKeySelector", () => AsyncEnumerable.FullJoin(AsyncEnumerable.Empty<string>(), AsyncEnumerable.Empty<string>(), (Func<string, CancellationToken, ValueTask<string>>)null, async (inner, ct) => inner, async (outer, inner, ct) => outer + inner));
AssertExtensions.Throws<ArgumentNullException>("innerKeySelector", () => AsyncEnumerable.FullJoin(AsyncEnumerable.Empty<string>(), AsyncEnumerable.Empty<string>(), async (outer, ct) => outer, (Func<string, CancellationToken, ValueTask<string>>)null, async (outer, inner, ct) => outer + inner));
AssertExtensions.Throws<ArgumentNullException>("resultSelector", () => AsyncEnumerable.FullJoin(AsyncEnumerable.Empty<string>(), AsyncEnumerable.Empty<string>(), async (outer, ct) => outer, async (inner, ct) => inner, (Func<string, string, CancellationToken, ValueTask<string>>)null));
}
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

This test file exercises the result-selector overloads, but it doesn’t cover either of the tuple-returning FullJoin overloads (sync key selectors and async key selectors). Since these overloads have distinct code paths, it would be good to add at least one test validating tuple results (including with a non-default comparer, and ideally also for the async key-selector overload).

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +104
Lookup<TKey, TInner> innerLookup = Lookup<TKey, TInner>.CreateForJoin(inner, innerKeySelector, comparer);

HashSet<Grouping<TKey, TInner>>? matchedGroupings = innerLookup.Count != 0
? new HashSet<Grouping<TKey, TInner>>()
: null;
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

Lookup<TKey, TInner>.CreateForJoin filters out elements whose key is null (see Lookup.CreateForJoin), so any inner elements with a null key will never be present in innerLookup and therefore will never be yielded as unmatched inner rows. That means FullJoin can silently drop inner elements, which contradicts full-outer-join semantics (“all elements from both sequences”). Consider explicitly tracking inner items with null keys while building the lookup (and yielding them as (default, innerItem)), or using a lookup construction that preserves null-key elements for the final unmatched-inner pass without allowing null-key matches if that’s desired.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Introduce FullJoin() LINQ operator

2 participants