Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -741,10 +741,11 @@ is not { } providerStep
lifted,
droppedStepIds);

// Record the parent-scope requirements on the descriptor.
// Record the parent-scope requirements on the descriptor. A step without a
// parent dependency is served entirely by other steps in this incremental plan.
foreach (var step in rewrittenIncrementalPlan)
{
if (step is not OperationPlanStep operationStep)
if (step is not OperationPlanStep operationStep || operationStep.ParentDependencies.IsEmpty)
{
continue;
}
Expand Down Expand Up @@ -1541,7 +1542,8 @@ private static (OperationPlanStep? ConsumingStep, OperationRequirement Requireme
/// <summary>
/// Finds every step in <paramref name="incrementalPlanSteps"/> that targets either
/// <paramref name="anchorPath"/> or the operation root and whose every selected field
/// is consumed as a requirement by one of its dependents.
/// is consumed as a requirement at or above <paramref name="anchorPath"/> by one of
/// its dependents.
/// </summary>
private static ImmutableArray<OperationPlanStep> FindDeferRequirementProducers(
ImmutableList<PlanStep> incrementalPlanSteps,
Expand All @@ -1555,7 +1557,7 @@ private static ImmutableArray<OperationPlanStep> FindDeferRequirementProducers(
&& operationStep.SchemaName is not null
&& !operationStep.Dependents.IsEmpty
&& (operationStep.Target.Equals(anchorPath) || operationStep.Target.IsRoot)
&& IsPureRequirementProducer(operationStep, incrementalPlanSteps))
&& IsPureRequirementProducer(operationStep, incrementalPlanSteps, anchorPath))
{
producers.Add(operationStep);
}
Expand All @@ -1566,12 +1568,13 @@ private static ImmutableArray<OperationPlanStep> FindDeferRequirementProducers(

/// <summary>
/// Determines whether every field <paramref name="step"/> selects is consumed as a
/// requirement by one of its dependents, meaning the step carries no output of its
/// own beyond what those dependents need from it.
/// requirement at or above <paramref name="anchorPath"/> by one of its dependents,
/// meaning the step carries no output of its own beyond what the enclosing scope can supply.
/// </summary>
private static bool IsPureRequirementProducer(
OperationPlanStep step,
ImmutableList<PlanStep> incrementalPlanSteps)
ImmutableList<PlanStep> incrementalPlanSteps,
SelectionPath anchorPath)
{
var ownFields = new HashSet<string>(StringComparer.Ordinal);
CollectLeafFieldNames(step.Definition.SelectionSet, ownFields);
Expand All @@ -1591,6 +1594,13 @@ private static bool IsPureRequirementProducer(

foreach (var (_, requirement) in dependentStep.Requirements)
{
// A requirement below the anchor reads a value this step fetches as
// deferred output, which the enclosing scope does not reach.
if (!requirement.Path.IsParentOfOrSame(anchorPath))
{
continue;
}

var fieldName = requirement.InternalAlias ?? ExtractRootFieldName(requirement.Map.ToString());

if (fieldName is not null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public OperationPlanner(
_options = options;
}

public static Version Version { get; } = new(2, 2, 0);
public static Version Version { get; } = new(2, 3, 0);

internal OperationPlannerOptions Options => _options;

Expand Down
93 changes: 93 additions & 0 deletions src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/DeferTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2221,6 +2221,99 @@ ... @defer {
await MatchSnapshotAsync(gateway, request, result, stableStream: true);
}

[Fact]
public async Task Defer_Should_ResolveNestedLookupInDeferredPayload_When_DeferredFieldTraversesSubgraphs()
{
// arrange
using var server1 = CreateSourceSchema(
"A",
b => b.AddQueryType<NestedLookupSourceA.Query>());

using var server2 = CreateSourceSchema(
"B",
b => b.AddQueryType<NestedLookupSourceB.Query>());

using var server3 = CreateSourceSchema(
"C",
b => b.AddQueryType<NestedLookupSourceC.Query>());

using var gateway = await CreateCompositeSchemaAsync(
[
("A", server1),
("B", server2),
("C", server3)
]);

// act
using var client = GraphQLHttpClient.Create(gateway.CreateClient());

var request = new OperationRequest(
"""
{
products {
name
... @defer(label: "deeper") {
related {
name
}
}
}
}
""");

using var result = await client.PostAsync(
request,
new Uri("http://localhost:5000/graphql"),
TestContext.Current.CancellationToken);

// assert
await MatchSnapshotAsync(gateway, request, result, stableStream: true);
}

private static class NestedLookupSourceA
{
public class Query
{
public Product[] GetProducts() => [new(1), new(2)];

[Lookup]
[Internal]
public Product? GetProductById([ID] int id) => new(id);
}

public record Product([property: ID] int Id);
}

private static class NestedLookupSourceB
{
public class Query
{
[Lookup]
[Internal]
public Product? GetProductById([ID] int id) => new(id);
}

public record Product([property: ID] int Id)
{
public Product Related => new(Id + 10);
}
}

private static class NestedLookupSourceC
{
public class Query
{
[Lookup]
[Internal]
public Product? GetProductById([ID] int id) => new(id);
}

public record Product([property: ID] int Id)
{
public string Name => $"Product {Id}";
}
}

private static class MixedRequirementSourceA
{
public class Query
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -514,9 +514,6 @@ operationPlan:
- 1
parentNodeId: 1
requirements:
- name: __fusion_1_id
selectionMap: >-
id
- name: __fusion_2_id
selectionMap: >-
id
Expand Down
Loading
Loading