Add @defer on fragment spreads and inline fragments - #615
Conversation
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Fragment condition handling, overlapping selections, spread ordering, and deferred root argument coercion have correctness issues.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 4
Open (5)
Pre-coerce root fields inside deferred fragments · New Merge deferred nested selections into direct field plans · New Preserve variable conditions for deferred fragments · New Ensure non-deferred fragment occurrences take precedence · New Document breaking changes to public discriminated unions · New
What changed in this PR
Adds fragment-level @defer planning, execution events, transport translation, validation fixes, and coverage.
Changes:
- Introduces deferred-fragment execution plans and events.
- Translates fragment events into incremental WebSocket payloads.
- Adds validation fixes, tests, and documentation.
| File | Description |
|---|---|
RELEASE_NOTES.md |
Documents fragment defer support. |
docs/type-system.md |
Describes fragment delivery behavior. |
src/FSharp.Data.GraphQL.Shared/TypeSystem.fs |
Adds the deferred-fragment plan kind. |
src/FSharp.Data.GraphQL.Shared/Validation.fs |
Fixes typeless inline-fragment validation. |
src/FSharp.Data.GraphQL.Server/Execution.fs |
Executes deferred fragments. |
src/FSharp.Data.GraphQL.Server/IO.fs |
Adds fragment delivery events. |
src/FSharp.Data.GraphQL.Server/Linq.fs |
Tracks deferred-fragment fields. |
src/FSharp.Data.GraphQL.Server/Planning.fs |
Plans and groups deferred fragments. |
src/FSharp.Data.GraphQL.Server.Middleware/MiddlewareDefinitions.fs |
Includes fragment fields in query weight. |
src/FSharp.Data.GraphQL.Server.AspNetCore/GraphQLRequestHandler.fs |
Logs fragment events. |
src/FSharp.Data.GraphQL.Server.AspNetCore/IncrementalDelivery.fs |
Translates fragment events into payloads. |
src/FSharp.Data.GraphQL.Server.AspNetCore/SubscriptionPayloads.fs |
Absorbs fragment announcements. |
tests/FSharp.Data.GraphQL.Tests/DeferredTests.fs |
Covers fragment execution behavior. |
tests/FSharp.Data.GraphQL.Tests/Helpers.fs |
Filters fragment lifecycle events. |
tests/FSharp.Data.GraphQL.Tests/AspNetCore/IncrementalDeliveryTests.fs |
Tests fragment payload translation. |
tests/FSharp.Data.GraphQL.Tests/AspNetCore/IncrementalDeliveryEndToEndTests.fs |
Tests end-to-end delivery. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
fb49cbd to
77a7531
Compare
`... { fields }` applies to the parent type; validation used to read the
absent type condition with `ValueOption.Value` and throw, and left the
fragment's fields out of the validation context.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A deferred fragment is planned as the new `ResolveDeferredFragment` kind: its fields stand among the fields of the object containing them, are resolved together against that object once the object's own fields were delivered, and go out as one payload announced at the object's path with the fragment's label. The engine reports fragments through the new `DeferredFragmentPending`, `DeferredFragmentResult` and `DeferredFragmentCompleted` events, and the `graphql-transport-ws` translator delivers them as `pending`/`incremental`/`completed` entries with the fragment's fields as the `data` object map. A field also selected directly on the object is executed with it and left out of the fragment; a fragment spread twice at the same place is delivered once; two labeled fragments at the same path get distinct ids; fragments at the operation root and on abstract types are supported; an error that propagates up to the fragment completes it with the errors and no data. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- `ResolveDeferredFragment` carries the directive's `if` condition, so a fragment disabled through a variable is resolved with the object, at the root too, instead of always being deferred. - A fragment selecting under a field the object selects directly adds that selection to the direct field instead of losing it. - A fragment spread directly is resolved with the object whichever of its spreads comes first: direct and deferred spreads are tracked apart, as graphql-js does. - The root fields of a deferred root fragment are coerced up front, so an argument they reject fails the request before any resolver runs. - Review suggestions applied: `String.Join` over the path instead of materializing it for `Path.Join`, `List.vchoose`, `yield!`, entries added to the abstraction map directly, the `StringBuilder` pipeline, XML comments on the planning helpers, and the fragment release note marked as a breaking change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
77a7531 to
81c8fde
Compare
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Deferred-fragment ordering, conditional inclusion, and mutation-root validation contain unresolved correctness issues.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 2
Open (2)
Resolved since last review (5)
A deferred fragment standing before the field it selects under now adds its selection to that field too: the object's own fields are collected first and the fragments' overlapping selections are merged into them afterwards, so the result does not depend on document order. The deferred fragment entry carries the includer of its spread or inline fragment, so `@skip`/`@include` on the fragment exclude it at the root and inside any object. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The fragment-level defer implementation is coherent across planning, execution, transport translation, validation, and focused tests, with prior findings addressed.
Review effort: Balanced
Findings: None
Resolved since last review (2)
Test Results 9 files 9 suites 15m 8s ⏱️ Results for commit 1e1c4e0. |



Summary
Adds
@deferon fragment spreads and inline fragments, the last piece of the incremental delivery specification v0.2 missing after #609, on top of theincremental-delivery-v02branch.ResolveDeferredFragmentkind: its fields stand among the fields of the object containing them, are resolved together against that object once the object's own fields were delivered, and go out as one payload announced at the object's path with the fragment'slabel.DeferredFragmentPending,DeferredFragmentResultandDeferredFragmentCompleted; thegraphql-transport-wstranslator delivers them aspending/incremental/completedentries with the fragment's fields as thedataobject map.... on C @defer) work; an error propagating up to the fragment completes it with the errors and no data; a fragment left without fields delivers nothing.... { … }), which threwValueOption.Valueinstead of applying to the parent type.Review scope
Please review only fragment-level
@defer:Planning.fs, the fragment parts ofExecution.fs,IncrementalDelivery.fsand the new tests. The lock-free middleware, directive arguments and validation rules are reviewed in #609.Dependency chain: #609 → this PR.
Verification
FSharp.Data.GraphQL.Tests: 751 passed, 5 skipped (pre-existing), 0 failed.FSharp.Data.GraphQL.IntegrationTestsWebSocketTests(real socket throughWebApplicationFactory): 3 passed.dotnet build FSharp.Data.GraphQL.slnxsucceeds.🤖 Generated with Claude Code