Repository navigation
Support a sub-selection on IAsyncEnumerable and ValueTask list fields - #555
Merged
Merged
Conversation
An async service field returning IAsyncEnumerable<T> or ValueTask<TCollection>
could not have a sub-selection - `{ people { tags { name } } }` failed with
"No generic method 'SelectWithNullCheck'". MakeSelectWithDynamicType resolves
that overload by the exact type of the expression it projects, and only
IEnumerable<T> and Task<IEnumerable<T>> had one.
IAsyncEnumerable<T> gets an overload that projects lazily, so the engine still
buffers the stream itself with the request's CancellationToken rather than
enumerating it during compilation. ValueTask<T> is handed on as the Task<T>
the rest of the pipeline already handles, next to the existing
Task<TCollection> normalization in SetUpField.
That made 6.2.4's bug reachable one layer down: BufferAsyncEnumerable built a
List<T> from the declared element type before resolving anything, so an item
rebuilt to carry an awaited member no longer fit it. Items are now resolved
first and the list type chosen from them, which is what the IEnumerable path
already did - both now share that step. 6.2.4's guard extends to the
IAsyncEnumerable branch of GetResolvedFieldType for the same reason.
Co-Authored-By: Claude Opus 5 <[email protected]>
lukemurray
force-pushed
the
fix/async-stream-and-valuetask-shapes
branch
from
September 8, 2026 17:28
f2f1f5f to
c8f8bc6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #554. Same family of bug, two shapes that never got there because they fail earlier.
What is broken
An async service field returning
IAsyncEnumerable<T>orValueTask<TCollection>cannot have a sub-selection at all:{ people { tags { name } } }MakeSelectWithDynamicTyperesolves that overload by the exact type of the expression it projects, and onlyIEnumerable<T>andTask<IEnumerable<T>>had one.SetUpFieldalready normalizesTask<TCollection>→Task<IEnumerable<T>>for exactly this reason; neither of these shapes was covered.This is why #554 deferred the
IAsyncEnumerable<T>branch ofGetResolvedFieldType- the shape could not reach it. It is also why that PR'sValueTask<T>guard had nothing to guard yet: aValueTasklist with a sub-selection never compiled.Fix
IAsyncEnumerable<T>gets aSelectWithNullCheckoverload of its own that projects lazily ([EnumeratorCancellation]async iterator). The field stays anIAsyncEnumerablethrough the whole pipeline, so the engine still buffers it - with the request'sCancellationToken- instead of it being enumerated during compilation. Nothing aboutConcurrencyLimitFieldExtension's deliberateIAsyncEnumerableexclusion changes.ValueTask<T>is handed on as theTask<T>the rest of the pipeline already handles, next to the existingTask<TCollection>normalization.The bug underneath
Making these shapes compile made #554's bug reachable one layer down:
BufferAsyncEnumerablecreated aList<T>from the declared element type up front and added each resolved item to it - but resolving an item rebuilds it when its projection holds an async member, so it no longer fits. TheIEnumerablepath already had this right: resolve first, then choose the list type from what you actually have (CanMaterializeTypedCollection). The async path now does the same, and both go through one sharedMaterializeResolvedItems.GetResolvedFieldType'sIAsyncEnumerable<T>branch takes the same guard #554 added toTask<T>/ValueTask<T>, now that it can be reached.Tests
Four new tests, each failing on
mainwith the errors above:AsyncStreamShapeTests.AsyncEnumerableWithSubSelectionAsyncStreamShapeTests.AsyncEnumerableWithAsyncFieldOnTheItem— the deferred shapeValueTaskShapeTests.ValueTaskOfListWithSubSelectionValueTaskShapeTests.ValueTaskOfListWithAnAsyncFieldOnTheItem— Keep a rebuilt async list's own type when the item type changed #554's bug throughValueTaskRelease build green on net8.0/9.0/10.0: 1206 passed / 1 skipped, plus 47 EF and 92 AspNet on each.
No version bump - 6.2.4 is unreleased, so these two entries join it in the CHANGELOG.
Not included
One more shape turned up in the sweep and is left alone as unrelated: an async mutation returning a type that is not part of the context graph (
Task<Tag>/Task<List<Tag>>whereTagcame fromAddType) fails withCannot implicitly convert type 'void' to 'object'. The sync equivalent works, and so doesTask<List<Person>>. Separate issue.🤖 Generated with Claude Code