Repository navigation
Keep a rebuilt async list's own type when the item type changed - #554
Merged
lukemurray merged 1 commit intoSep 8, 2026
Merged
Conversation
Ko-Lin
force-pushed
the
fix/async-list-item-rebuild-type
branch
from
September 8, 2026 12:18
4fa24bf to
dcfd907
Compare
An async service field selected below an async service list field crashed with
"Object of type 'List`1[System.Object]' cannot be converted to type
'IEnumerable`1[Dynamic_tags_...]'" - a query like { people { tags { name label } } }
where tags resolves a list via ResolveAsync and label on the item type resolves
via ResolveAsync too.
Resolving an item of the outer list rebuilds it, because the item projection
holds an async member of its own, so the finished list no longer holds the item
type the outer field was declared with and correctly falls back to List<object>.
GetResolvedFieldType unwrapped Task<T> to T unconditionally, so the rebuilt
parent still declared that member IEnumerable<T> and setting the resolved list
on it threw.
Only keep T when the resolved value still is one, otherwise take the resolved
value's own type - which is what the non-async path in the same method already
does. ValueTask<T> had the same hole and takes the same guard.
Bump to 6.2.4.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ko-Lin
force-pushed
the
fix/async-list-item-rebuild-type
branch
from
September 8, 2026 12:21
dcfd907 to
b41301d
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.
An async service field selected below an async service list field throws while the result is being assembled. Every read has already succeeded — it fails on the way back out.
The added test is the minimum shape that reproduces it —
Person.tagsresolves aList<Tag>throughResolveAsync, andTag.labelhas aResolveAsyncof its own:{ people { id tags { name label } } }Cause
tagsis declaredTask<IEnumerable<Dynamic_tags_X>>on the person's projection. EachDynamic_tags_Xholds an async member, soResolveComplexObjectrebuilds every item into a new dynamic type — the finished list no longer holdsDynamic_tags_X, andCanMaterializeTypedCollectioncorrectly declines to make a typed list, falling back toList<object>.That fallback is right. The member type the rebuilt parent is given for it is not:
GetResolvedFieldTypeunwrappedTask<T>toTunconditionally, so the parent still declared the memberIEnumerable<Dynamic_tags_X>and setting theList<object>on it threw.The non-async path at the bottom of the same method already falls through to
resolvedValue?.GetType(), which is why the same async child under a plain (non-Task) list field works today — its member type is taken from the rebuilt list rather than from a promise made before the rebuild.Fix
Only keep
Twhen the resolved value still is one:ValueTask<T>had the same hole and takes the same guard. TheIAsyncEnumerable<T>branch looked like it needed one too, but is deliberately left alone: anIAsyncEnumerablefield with a sub-selection fails earlier withNo generic method 'SelectWithNull', so it cannot carry a projected, rebuildable item type. Worth revisiting together if that selection limitation is ever lifted.This also sits next to the nullable-member fix from 6.2.2 a few lines below — that one exists because the resolved value's runtime type is untrustworthy for nullability, this one because the declared type is untrustworthy for shape. Each now defers to the other where it knows better.
Tests
AsyncShapesTests.AsyncListOfProjectedItems_WithAnAsyncFieldOnTheItem_StaysAssignableToTheField— fails onmainwith the error above, passes with the fix.Full suite green: 1202 passed / 1 skipped, plus 47 EF tests.
Bumped to 6.2.4 with a CHANGELOG entry, following #553 — happy to drop either if you would rather cut the release yourself.