Conversation
src/HotChocolate/Fusion/src/Fusion.Execution.Types/Completion/CompositeSchemaBuilder.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Adds @defer support to the HotChocolate Fusion gateway by splitting deferred fragments during planning and streaming incremental payloads during execution.
Changes:
- Introduces a defer-aware operation rewriter + planner path that produces
DeferredExecutionGroups. - Adds an execution path that returns an incremental
ResponseStreamwithpending/incremental/completed. - Registers
@deferin the composite schema and adds planner/runtime tests + updated schema snapshots.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/DeferPlannerTests.cs |
New planner-level tests validating group creation, labels, nesting, and conditions. |
src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Planning/__snapshots__/LookupTests.Require_Inaccessible_Data.graphql |
Snapshot updated to include @defer directive in schema output. |
src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Types/__snapshots__/SerializeAsTests.SerializeAs_Will_Not_Be_In_The_Schema.graphql |
Snapshot updated to include @defer directive. |
src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Types/__snapshots__/SerializeAsTests.SerializeAs_Will_Be_In_The_Schema.graphql |
Snapshot updated to include @defer directive. |
src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/DeferTests.cs |
New end-to-end tests asserting incremental delivery payloads. |
src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.Defer.cs |
New planner module to plan deferred fragments and build deferred execution nodes. |
src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs |
Hooks defer splitting + deferred group planning into the main planning flow. |
src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.BuildExecutionTree.cs |
Extends plan creation to carry deferred groups; strips @defer from subgraph operations. |
src/HotChocolate/Fusion/src/Fusion.Execution/Planning/DeferOperationRewriter.cs |
New rewriter that extracts deferred fragments into standalone operations. |
src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Pipeline/OperationExecutionMiddleware.cs |
Routes requests with deferred groups through a new executor path; blocks variable batching with @defer. |
src/HotChocolate/Fusion/src/Fusion.Execution/Execution/OperationPlanExecutor.cs |
Implements streaming execution for deferred groups (pending/incremental/completed). |
src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Nodes/SelectionSet.cs |
Implements HasIncrementalParts for Fusion selection sets. |
src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Nodes/Selection.cs |
Implements IsDeferred based on a defer mask. |
src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Nodes/OperationPlan.cs |
Stores deferred groups on the operation plan. |
src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Nodes/OperationCompiler.cs |
Detects presence of @defer to set HasIncrementalParts on compiled operations. |
src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Nodes/Operation.cs |
Implements HasIncrementalParts. |
src/HotChocolate/Fusion/src/Fusion.Execution/Execution/Nodes/DeferredExecutionGroup.cs |
New execution model type representing one deferred fragment group. |
src/HotChocolate/Fusion/src/Fusion.Execution/Execution/FusionOperationInfo.cs |
Broadens Reset visibility to enable cross-assembly usage. |
src/HotChocolate/Fusion/src/Fusion.Execution/Execution/ExecutionState.cs |
Broadens AddToBacklog visibility to enable cross-assembly usage. |
src/HotChocolate/Fusion/src/Fusion.Execution.Types/Completion/CompositeSchemaBuilder.cs |
Registers the @defer directive in the gateway schema for validation. |
src/HotChocolate/Core/src/Execution.Abstractions/HotChocolate.Execution.Abstractions.csproj |
Adds InternalsVisibleTo for HotChocolate.Fusion.Execution. |
global.json |
Updates SDK version pin. |
dotnet-install.sh |
Adds .NET install helper script (vendored). |
.vscode/mcp.json |
Updates MCP NuGet command invocation (dotnet dnx ...). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Check if this group has children that should now start | ||
| var childGroups = new List<DeferredExecutionGroup>(); | ||
| foreach (var candidate in deferredGroups) | ||
| { | ||
| if (candidate.Parent?.DeferId == group.DeferId) | ||
| { | ||
| childGroups.Add(candidate); | ||
| pendingCount++; | ||
| _ = ExecuteDeferredGroupInBackground( | ||
| requestContext, variables, operationPlan, candidate, channel.Writer, cancellationToken); | ||
| } |
There was a problem hiding this comment.
Nested deferred groups are started/announced unconditionally when a parent group completes. This ignores the child group's @defer(if: $var) condition, so a child defer can execute and be reported as pending even when its condition evaluates to false. Filter candidate the same way as top-level groups (evaluate candidate.IfVariable against variables) before adding to childGroups, incrementing pendingCount, and starting execution.
|
|
||
| context.Begin(); | ||
|
|
||
| await ExecuteQueryAsync(context, deferPlan, executionCts.Token); |
There was a problem hiding this comment.
Deferred groups are always executed via ExecuteQueryAsync, even when the overall operation is a mutation and the deferred group's compiled operation may be a mutation selection set. This will break @defer on mutation results (the test is currently skipped for this). Consider switching based on deferPlan.Operation.Definition.Operation (query vs mutation) and executing with ExecuteMutationAsync when appropriate.
| await ExecuteQueryAsync(context, deferPlan, executionCts.Token); | |
| if (deferPlan.Operation.Definition.Operation is OperationType.Mutation) | |
| { | |
| await ExecuteMutationAsync(context, deferPlan, executionCts.Token); | |
| } | |
| else | |
| { | |
| await ExecuteQueryAsync(context, deferPlan, executionCts.Token); | |
| } |
| } | ||
|
|
||
| return rootOperation | ||
| .WithOperation(OperationType.Query) |
There was a problem hiding this comment.
BuildDeferredOperation forces the deferred fragment operation type to Query, which prevents @defer from working for mutations (and can lead to planning/execution failures for mutation-only root fields). It would be safer to preserve rootOperation.Operation when building the deferred operation, and then plan/execute the deferred group using the same operation type.
| .WithOperation(OperationType.Query) | |
| .WithOperation(rootOperation.Operation) |
|
|
||
| var index = SelectionSetIndexer.Create(deferredOperation); | ||
|
|
||
| var (node, selectionSet) = CreateQueryPlanBase(deferredOperation, "defer", index); |
There was a problem hiding this comment.
Deferred fragments are planned using CreateQueryPlanBase unconditionally. If deferred operations are allowed to keep their original operation type (e.g., for mutation defers), this needs to switch on deferredOperation.Operation (query/mutation/subscription) just like the main planner does, otherwise mutation deferred groups will be planned incorrectly.
| var (node, selectionSet) = CreateQueryPlanBase(deferredOperation, "defer", index); | |
| var (node, selectionSet) = deferredOperation.Operation switch | |
| { | |
| OperationType.Query => CreateQueryPlanBase(deferredOperation, "defer", index), | |
| OperationType.Mutation => CreateMutationPlanBase(deferredOperation, "defer", index), | |
| OperationType.Subscription => CreateSubscriptionPlanBase(deferredOperation, "defer", index), | |
| _ => throw new GraphQLException( | |
| ErrorBuilder.New() | |
| .SetMessage("Unsupported operation type.") | |
| .Build()) | |
| }; |
| // Empty deferred group — all fields may have been conditional and excluded. | ||
| // Report a successful completion with no data. | ||
| // We use FromError to create a valid OperationResult, then clear | ||
| // top-level errors since this is a successful completion. | ||
| var placeholder = ErrorBuilder.New() | ||
| .SetMessage("placeholder") | ||
| .Build(); | ||
| payload = OperationResult.FromError(placeholder); | ||
| payload.Completed = [new CompletedResult(group.DeferId)]; | ||
| payload.Data = null; | ||
| payload.Errors = []; | ||
| } |
There was a problem hiding this comment.
Creating an "empty but successful" deferred payload by constructing a placeholder error and then clearing Errors is fragile (the placeholder message could leak via logging/diagnostics or future formatter changes). Consider introducing an explicit way to create an incremental-only OperationResult (Completed/Pending/HasNext without Data/Errors) so this code path doesn't rely on a dummy error object.
| @@ -0,0 +1,616 @@ | |||
| using System.Text; | |||
There was a problem hiding this comment.
System.Text is not used in this test file and can be removed to avoid unnecessary usings/build warnings.
| using System.Text; |
| // assert — parse the raw multipart body to verify the incremental delivery format. | ||
| // The transport OperationResult parser drops incremental fields, so we parse raw JSON. | ||
| var rawBody = await result.HttpResponseMessage.Content.ReadAsStringAsync(); | ||
| var payloads = rawBody | ||
| .Split('\n', StringSplitOptions.RemoveEmptyEntries) | ||
| .Select(line => JsonDocument.Parse(line)) | ||
| .ToList(); |
There was a problem hiding this comment.
This assertion block parses the response as newline-delimited JSON (.Split('\n')), but the comment says "raw multipart body". Since the client’s default Accept header includes application/graphql-response+jsonl (JSON Lines) and not multipart/mixed, consider updating the comment (or assert the response Content-Type explicitly) to match the actual format under test.
# Conflicts: # src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.BuildExecutionTree.cs # src/HotChocolate/Fusion/src/Fusion.Execution/Planning/OperationPlanner.cs
# Conflicts: # src/HotChocolate/Fusion/test/Fusion.Execution.Tests/Execution/Types/__snapshots__/SerializeAsTests.SerializeAs_Will_Be_In_The_Schema.graphql
No description provided.