From c7d39e6e5952955dfa176d0de97164d480c4d38f Mon Sep 17 00:00:00 2001 From: Daniel Wust Date: Wed, 6 Aug 2025 13:59:45 +0200 Subject: [PATCH 01/13] check if the schema contains a resolver for a type when selecting the best matching subgraph --- .../Fusion/src/Core/Planning/QueryPlannerHelpers.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs index 26f6fc9d6d1..154b13fe42c 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs @@ -54,7 +54,8 @@ private static int EvaluateSubgraphCompatibilityScore( { if (!selection.Field.IsIntrospectionField && currentTypeContext.Fields[selection.Field.Name].Bindings - .ContainsSubgraph(schemaName)) + .ContainsSubgraph(schemaName) && + typeMetadataContext.Resolvers.ContainsResolvers(schemaName)) { score++; From e330961f44ec109266e59c9300a9d2aaea65ee6d Mon Sep 17 00:00:00 2001 From: Daniel Wust Date: Wed, 6 Aug 2025 18:10:33 +0200 Subject: [PATCH 02/13] favor score of "working" subgraphs and add integration test --- .../ExecutionStepDiscoveryMiddleware.cs | 27 +----- .../src/Core/Planning/QueryPlannerHelpers.cs | 35 +++++++- .../test/Core.Tests/DemoIntegrationTests.cs | 75 ++++++++++++++++ ...ata_Is_Available_In_Resolvable_Subgraph.md | 89 +++++++++++++++++++ 4 files changed, 201 insertions(+), 25 deletions(-) create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Unresolvable_Subgraph_Is_Not_Chosen_If_Data_Is_Available_In_Resolvable_Subgraph.md diff --git a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs index fc1cfca7460..1cf81470b5c 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs @@ -123,6 +123,7 @@ parentSelectionPath is null var current = leftovers ?? selections; var subgraph = _config.GetBestMatchingSubgraph( operation, + parentSelectionPath, current, selectionSetTypeMetadata); var executionStep = new SelectionExecutionStep( @@ -232,14 +233,14 @@ parentSelectionPath is null path.RemoveAt(pathIndex); } - // if the current execution step has now way to resolve the data + // if the current execution step has no way to resolve the data // we will try to resolve it from the root. if (executionStep.ParentSelection is not null && executionStep.ParentSelectionPath is not null && executionStep.Resolver is null && executionStep.SelectionResolvers.Count == 0) { - if (!EnsureStepCanBeResolvedFromRoot( + if (!_config.EnsureStepCanBeResolvedFromRoot( executionStep.SubgraphName, executionStep.ParentSelectionPath)) { @@ -628,6 +629,7 @@ private SelectionExecutionStep CreateNodeNestedExecutionSteps( ? availableSubgraphs[0] : _config.GetBestMatchingSubgraph( operation, + null, entityTypeSelectionSet.Selections, entityTypeMetadata, availableSubgraphs); @@ -914,27 +916,6 @@ private static bool IsNodeField(IObjectField field, IOperation operation) && field.DeclaringType.Equals(operation.RootType) && (field.Name.EqualsOrdinal("node") || field.Name.EqualsOrdinal("nodes")); - private bool EnsureStepCanBeResolvedFromRoot( - string subgraphName, - SelectionPath path) - { - var current = path; - - while (current is not null) - { - var typeMetadata = _config.GetType(current.Selection.DeclaringType.Name); - - if (!typeMetadata.Fields[current.Selection.Field.Name].Bindings.ContainsSubgraph(subgraphName)) - { - return false; - } - - current = current.Parent; - } - - return true; - } - private readonly struct BacklogItem( ISelection parentSelection, SelectionPath? selectionPath, diff --git a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs index 154b13fe42c..5888f78d11f 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs @@ -8,6 +8,7 @@ internal static class QueryPlannerHelpers public static string GetBestMatchingSubgraph( this FusionGraphConfiguration configuration, IOperation operation, + SelectionPath? parentSelectionPath, IReadOnlyList selections, ObjectTypeMetadata typeMetadataContext, IReadOnlyList? availableSubgraphs = null) @@ -21,6 +22,7 @@ public static string GetBestMatchingSubgraph( EvaluateSubgraphCompatibilityScore( configuration, operation, + parentSelectionPath, selections, typeMetadataContext, subgraphName); @@ -38,6 +40,7 @@ public static string GetBestMatchingSubgraph( private static int EvaluateSubgraphCompatibilityScore( FusionGraphConfiguration configuration, IOperation operation, + SelectionPath? parentSelectionPath, IReadOnlyList selections, ObjectTypeMetadata typeMetadataContext, string schemaName) @@ -54,11 +57,17 @@ private static int EvaluateSubgraphCompatibilityScore( { if (!selection.Field.IsIntrospectionField && currentTypeContext.Fields[selection.Field.Name].Bindings - .ContainsSubgraph(schemaName) && - typeMetadataContext.Resolvers.ContainsResolvers(schemaName)) + .ContainsSubgraph(schemaName)) { score++; + if (parentSelectionPath is null || + typeMetadataContext.Resolvers.ContainsResolvers(schemaName) || + configuration.EnsureStepCanBeResolvedFromRoot(schemaName, parentSelectionPath)) + { + score++; + } + if (selection.SelectionSet is not null) { foreach (var possibleType in operation.GetPossibleTypes(selection)) @@ -74,4 +83,26 @@ private static int EvaluateSubgraphCompatibilityScore( return score; } + + public static bool EnsureStepCanBeResolvedFromRoot( + this FusionGraphConfiguration configuration, + string subgraphName, + SelectionPath path) + { + var current = path; + + while (current is not null) + { + var typeMetadata = configuration.GetType(current.Selection.DeclaringType.Name); + + if (!typeMetadata.Fields[current.Selection.Field.Name].Bindings.ContainsSubgraph(subgraphName)) + { + return false; + } + + current = current.Parent; + } + + return true; + } } diff --git a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs index 7343a707525..b9821efcb3c 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs @@ -2895,6 +2895,81 @@ query Test($number: Int!) { Assert.Null(result.ExpectOperationResult().Errors); } + [Fact] + public async Task Unresolvable_Subgraph_Is_Not_Chosen_If_Data_Is_Available_In_Resolvable_Subgraph() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + viewer: Viewer + } + + type Viewer { + product: Product! + } + + type Product implements Node { + id: ID! + } + + interface Node { + id: ID! + } + """); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + test: Test! + } + + type Test { + id: ID! + } + + type Product { + id: ID! + name: String! + } + """); + + var subgraphC = await TestSubgraph.CreateAsync( + """ + type Query { + node(id: ID!): Node + } + + type Product implements Node { + id: ID! + name: String! + } + + interface Node { + id: ID! + } + """); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB, subgraphC]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + viewer { + product { + id + name + } + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + MatchMarkdownSnapshot(request, result); + } + public sealed class HotReloadConfiguration : IObservable { private GatewayConfiguration _configuration; diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Unresolvable_Subgraph_Is_Not_Chosen_If_Data_Is_Available_In_Resolvable_Subgraph.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Unresolvable_Subgraph_Is_Not_Chosen_If_Data_Is_Available_In_Resolvable_Subgraph.md new file mode 100644 index 00000000000..2900f5e3a4f --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Unresolvable_Subgraph_Is_Not_Chosen_If_Data_Is_Available_In_Resolvable_Subgraph.md @@ -0,0 +1,89 @@ +# Unresolvable_Subgraph_Is_Not_Chosen_If_Data_Is_Available_In_Resolvable_Subgraph + +## Result + +```json +{ + "data": { + "viewer": { + "product": { + "id": "1", + "name": "string" + } + } + } +} +``` + +## Request + +```graphql +{ + viewer { + product { + id + name + } + } +} +``` + +## QueryPlan Hash + +```text +D7ED84DE24B13D55C6D5F010601B7C7B7F266199 +``` + +## QueryPlan + +```json +{ + "document": "{ viewer { product { id name } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_viewer_1 { viewer { product { id __fusion_exports__1: id } } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Resolve", + "subgraph": "Subgraph_3", + "document": "query fetch_viewer_2($__fusion_exports__1: ID!) { node(id: $__fusion_exports__1) { ... on Product { name } } }", + "selectionSetId": 2, + "path": [ + "node" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 2 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Product_id" + } +} +``` + From 3c4ad2d990d5e1ec34bd7e5295dddf09792b0960 Mon Sep 17 00:00:00 2001 From: Daniel Wust Date: Wed, 6 Aug 2025 18:23:46 +0200 Subject: [PATCH 03/13] fix hash --- ...Is_Not_Chosen_If_Data_Is_Available_In_Resolvable_Subgraph.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Unresolvable_Subgraph_Is_Not_Chosen_If_Data_Is_Available_In_Resolvable_Subgraph.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Unresolvable_Subgraph_Is_Not_Chosen_If_Data_Is_Available_In_Resolvable_Subgraph.md index 2900f5e3a4f..57c5244397f 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Unresolvable_Subgraph_Is_Not_Chosen_If_Data_Is_Available_In_Resolvable_Subgraph.md +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Unresolvable_Subgraph_Is_Not_Chosen_If_Data_Is_Available_In_Resolvable_Subgraph.md @@ -31,7 +31,7 @@ ## QueryPlan Hash ```text -D7ED84DE24B13D55C6D5F010601B7C7B7F266199 +68F7ACC73F0431E50C4145FBB512E15D04D79198 ``` ## QueryPlan From a45c39b507b139e5941bafe86ff5b63f7bfeda83 Mon Sep 17 00:00:00 2001 From: Daniel Wust Date: Wed, 6 Aug 2025 18:44:36 +0200 Subject: [PATCH 04/13] merge condition --- .../src/Core/Planning/QueryPlannerHelpers.cs | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs index 5888f78d11f..41fa4de6aa9 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs @@ -57,25 +57,20 @@ private static int EvaluateSubgraphCompatibilityScore( { if (!selection.Field.IsIntrospectionField && currentTypeContext.Fields[selection.Field.Name].Bindings - .ContainsSubgraph(schemaName)) + .ContainsSubgraph(schemaName) && + (typeMetadataContext.Resolvers.ContainsResolvers(schemaName) || + (parentSelectionPath is not null && configuration.EnsureStepCanBeResolvedFromRoot(schemaName, parentSelectionPath)))) { score++; + } - if (parentSelectionPath is null || - typeMetadataContext.Resolvers.ContainsResolvers(schemaName) || - configuration.EnsureStepCanBeResolvedFromRoot(schemaName, parentSelectionPath)) - { - score++; - } - - if (selection.SelectionSet is not null) + if (selection.SelectionSet is not null) + { + foreach (var possibleType in operation.GetPossibleTypes(selection)) { - foreach (var possibleType in operation.GetPossibleTypes(selection)) - { - var type = configuration.GetType(possibleType.Name); - var selectionSet = operation.GetSelectionSet(selection, possibleType); - stack.Push((selectionSet.Selections, type)); - } + var type = configuration.GetType(possibleType.Name); + var selectionSet = operation.GetSelectionSet(selection, possibleType); + stack.Push((selectionSet.Selections, type)); } } } From 35698225ff478f0fbabc5ca3a4b560d812d4bac2 Mon Sep 17 00:00:00 2001 From: Daniel Wust Date: Wed, 6 Aug 2025 18:59:01 +0200 Subject: [PATCH 05/13] fix bracket --- .../src/Core/Planning/QueryPlannerHelpers.cs | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs index 41fa4de6aa9..477de8afc6d 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs @@ -56,21 +56,24 @@ private static int EvaluateSubgraphCompatibilityScore( foreach (var selection in currentSelections) { if (!selection.Field.IsIntrospectionField && - currentTypeContext.Fields[selection.Field.Name].Bindings + currentTypeContext + .Fields[selection.Field.Name] + .Bindings .ContainsSubgraph(schemaName) && - (typeMetadataContext.Resolvers.ContainsResolvers(schemaName) || - (parentSelectionPath is not null && configuration.EnsureStepCanBeResolvedFromRoot(schemaName, parentSelectionPath)))) + (parentSelectionPath is null || + typeMetadataContext.Resolvers.ContainsResolvers(schemaName) || + configuration.EnsureStepCanBeResolvedFromRoot(schemaName, parentSelectionPath))) { score++; - } - if (selection.SelectionSet is not null) - { - foreach (var possibleType in operation.GetPossibleTypes(selection)) + if (selection.SelectionSet is not null) { - var type = configuration.GetType(possibleType.Name); - var selectionSet = operation.GetSelectionSet(selection, possibleType); - stack.Push((selectionSet.Selections, type)); + foreach (var possibleType in operation.GetPossibleTypes(selection)) + { + var type = configuration.GetType(possibleType.Name); + var selectionSet = operation.GetSelectionSet(selection, possibleType); + stack.Push((selectionSet.Selections, type)); + } } } } From 55868e15fd73974dcdfac1570e0e34bc04a7d216 Mon Sep 17 00:00:00 2001 From: Daniel Wust Date: Wed, 6 Aug 2025 19:28:00 +0200 Subject: [PATCH 06/13] don't return non matching subgraphs --- .../Pipeline/ExecutionStepDiscoveryMiddleware.cs | 14 +++++++++++++- .../src/Core/Planning/QueryPlannerHelpers.cs | 9 +++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs index 1cf81470b5c..c41747fccaa 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs @@ -126,6 +126,12 @@ parentSelectionPath is null parentSelectionPath, current, selectionSetTypeMetadata); + + if (subgraph is null) + { + throw ThrowHelper.NoResolverInContext(); + } + var executionStep = new SelectionExecutionStep( context.NextStepId(), subgraph, @@ -155,7 +161,8 @@ parentSelectionPath is null var field = selection.Field; var fieldInfo = selectionSetTypeMetadata.Fields[field.Name]; - if (!fieldInfo.Bindings.ContainsSubgraph(subgraph)) + if (!fieldInfo.Bindings.ContainsSubgraph(subgraph) + && !fieldInfo.Resolvers.ContainsResolvers(subgraph)) { path.RemoveAt(pathIndex); (leftovers ??= []).Add(selection); @@ -634,6 +641,11 @@ private SelectionExecutionStep CreateNodeNestedExecutionSteps( entityTypeMetadata, availableSubgraphs); + if (subgraph is null) + { + throw ThrowHelper.NoResolverInContext(); + } + var field = nodeSelection.Field; var fieldInfo = queryTypeMetadata.Fields[field.Name]; var executionStep = new SelectionExecutionStep( diff --git a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs index 477de8afc6d..c39c82ce592 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs @@ -5,7 +5,7 @@ namespace HotChocolate.Fusion.Planning; internal static class QueryPlannerHelpers { - public static string GetBestMatchingSubgraph( + public static string? GetBestMatchingSubgraph( this FusionGraphConfiguration configuration, IOperation operation, SelectionPath? parentSelectionPath, @@ -14,7 +14,7 @@ public static string GetBestMatchingSubgraph( IReadOnlyList? availableSubgraphs = null) { var bestScore = 0; - var bestSubgraph = configuration.SubgraphNames[0]; + var bestSubgraph = default(string?); foreach (var subgraphName in availableSubgraphs ?? configuration.SubgraphNames) { @@ -53,6 +53,11 @@ private static int EvaluateSubgraphCompatibilityScore( { var (currentSelections, currentTypeContext) = stack.Pop(); + if (currentSelections.Count == 0) + { + score++; + } + foreach (var selection in currentSelections) { if (!selection.Field.IsIntrospectionField && From fa86c18438c2fd555cae101cb9dcbfca46331572 Mon Sep 17 00:00:00 2001 From: Daniel Wust Date: Wed, 6 Aug 2025 19:57:19 +0200 Subject: [PATCH 07/13] fix common fields --- .../Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs | 3 +-- .../Fusion/src/Core/Planning/QueryPlannerHelpers.cs | 4 ++++ .../Fusion/test/Core.Tests/HotChocolate.Fusion.Tests.csproj | 4 ++++ .../DemoIntegrationTests.Forward_Nested_Node_Variables.md | 4 ++-- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs index c41747fccaa..36bf7a2ea59 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs @@ -161,8 +161,7 @@ parentSelectionPath is null var field = selection.Field; var fieldInfo = selectionSetTypeMetadata.Fields[field.Name]; - if (!fieldInfo.Bindings.ContainsSubgraph(subgraph) - && !fieldInfo.Resolvers.ContainsResolvers(subgraph)) + if (!fieldInfo.Bindings.ContainsSubgraph(subgraph)) { path.RemoveAt(pathIndex); (leftovers ??= []).Add(selection); diff --git a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs index c39c82ce592..e4cbfc38f22 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs @@ -66,6 +66,10 @@ private static int EvaluateSubgraphCompatibilityScore( .Bindings .ContainsSubgraph(schemaName) && (parentSelectionPath is null || + currentTypeContext + .Fields[selection.Field.Name] + .Resolvers + .ContainsResolvers(schemaName) || typeMetadataContext.Resolvers.ContainsResolvers(schemaName) || configuration.EnsureStepCanBeResolvedFromRoot(schemaName, parentSelectionPath))) { diff --git a/src/HotChocolate/Fusion/test/Core.Tests/HotChocolate.Fusion.Tests.csproj b/src/HotChocolate/Fusion/test/Core.Tests/HotChocolate.Fusion.Tests.csproj index 26b96e2be48..b81d995df08 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/HotChocolate.Fusion.Tests.csproj +++ b/src/HotChocolate/Fusion/test/Core.Tests/HotChocolate.Fusion.Tests.csproj @@ -19,4 +19,8 @@ + + + + diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Forward_Nested_Node_Variables.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Forward_Nested_Node_Variables.md index 7b24d4ab105..575e984dc8d 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Forward_Nested_Node_Variables.md +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Forward_Nested_Node_Variables.md @@ -29,7 +29,7 @@ query ProductReviews($id: ID!, $first: Int!) { ## QueryPlan Hash ```text -3EB74A019DB95A4FA68CF9569951EA91659E7186 +90288E30B42792662254D8D4CF683A745331D191 ``` ## QueryPlan @@ -50,7 +50,7 @@ query ProductReviews($id: ID!, $first: Int!) { "type": "User", "node": { "type": "Resolve", - "subgraph": "Accounts", + "subgraph": "Reviews2", "document": "query ProductReviews_1($id: ID!) { node(id: $id) { ... on User { __typename } } }", "selectionSetId": 0, "forwardedVariables": [ From 4d1c1c805169368dc85e7481d35af64db3d31517 Mon Sep 17 00:00:00 2001 From: Daniel Wust Date: Wed, 6 Aug 2025 19:58:12 +0200 Subject: [PATCH 08/13] accidentally added folder --- .../Fusion/test/Core.Tests/HotChocolate.Fusion.Tests.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/HotChocolate/Fusion/test/Core.Tests/HotChocolate.Fusion.Tests.csproj b/src/HotChocolate/Fusion/test/Core.Tests/HotChocolate.Fusion.Tests.csproj index b81d995df08..26b96e2be48 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/HotChocolate.Fusion.Tests.csproj +++ b/src/HotChocolate/Fusion/test/Core.Tests/HotChocolate.Fusion.Tests.csproj @@ -19,8 +19,4 @@ - - - - From 84e71f4d6161a801d8b1cb1707c9203b923ed8f3 Mon Sep 17 00:00:00 2001 From: Daniel Wust Date: Wed, 6 Aug 2025 20:21:59 +0200 Subject: [PATCH 09/13] simplify --- .../Pipeline/ExecutionStepDiscoveryMiddleware.cs | 2 +- .../Fusion/src/Core/Planning/QueryPlannerHelpers.cs | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs index 36bf7a2ea59..8533d79714e 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/Pipeline/ExecutionStepDiscoveryMiddleware.cs @@ -246,7 +246,7 @@ parentSelectionPath is null && executionStep.Resolver is null && executionStep.SelectionResolvers.Count == 0) { - if (!_config.EnsureStepCanBeResolvedFromRoot( + if (!_config.EnsurePathCanBeResolvedFromRoot( executionStep.SubgraphName, executionStep.ParentSelectionPath)) { diff --git a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs index e4cbfc38f22..316583c7362 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs @@ -45,6 +45,8 @@ private static int EvaluateSubgraphCompatibilityScore( ObjectTypeMetadata typeMetadataContext, string schemaName) { + var pathCanBeResolvedFromRoot = configuration.EnsurePathCanBeResolvedFromRoot(schemaName, parentSelectionPath); + var score = 0; var stack = new Stack<(IReadOnlyList selections, ObjectTypeMetadata typeContext)>(); stack.Push((selections, typeMetadataContext)); @@ -65,13 +67,12 @@ private static int EvaluateSubgraphCompatibilityScore( .Fields[selection.Field.Name] .Bindings .ContainsSubgraph(schemaName) && - (parentSelectionPath is null || + (pathCanBeResolvedFromRoot || currentTypeContext .Fields[selection.Field.Name] .Resolvers .ContainsResolvers(schemaName) || - typeMetadataContext.Resolvers.ContainsResolvers(schemaName) || - configuration.EnsureStepCanBeResolvedFromRoot(schemaName, parentSelectionPath))) + typeMetadataContext.Resolvers.ContainsResolvers(schemaName))) { score++; @@ -91,10 +92,10 @@ private static int EvaluateSubgraphCompatibilityScore( return score; } - public static bool EnsureStepCanBeResolvedFromRoot( + public static bool EnsurePathCanBeResolvedFromRoot( this FusionGraphConfiguration configuration, string subgraphName, - SelectionPath path) + SelectionPath? path) { var current = path; From 3fa8127c7e489393abf65db623c18c4cdaf95304 Mon Sep 17 00:00:00 2001 From: Daniel Wust Date: Sun, 10 Aug 2025 12:22:39 +0200 Subject: [PATCH 10/13] move logic to PathCanBeResolved, add test --- .../src/Core/Planning/QueryPlannerHelpers.cs | 38 +++++----- .../test/Core.Tests/DemoIntegrationTests.cs | 69 +++++++++++++++++++ 2 files changed, 89 insertions(+), 18 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs index 316583c7362..abd381e10ea 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs @@ -45,9 +45,13 @@ private static int EvaluateSubgraphCompatibilityScore( ObjectTypeMetadata typeMetadataContext, string schemaName) { - var pathCanBeResolvedFromRoot = configuration.EnsurePathCanBeResolvedFromRoot(schemaName, parentSelectionPath); - var score = 0; + + if (!PathCanBeResolved(configuration, parentSelectionPath, typeMetadataContext, schemaName)) + { + return score; + } + var stack = new Stack<(IReadOnlyList selections, ObjectTypeMetadata typeContext)>(); stack.Push((selections, typeMetadataContext)); @@ -55,24 +59,11 @@ private static int EvaluateSubgraphCompatibilityScore( { var (currentSelections, currentTypeContext) = stack.Pop(); - if (currentSelections.Count == 0) - { - score++; - } - foreach (var selection in currentSelections) { if (!selection.Field.IsIntrospectionField && - currentTypeContext - .Fields[selection.Field.Name] - .Bindings - .ContainsSubgraph(schemaName) && - (pathCanBeResolvedFromRoot || - currentTypeContext - .Fields[selection.Field.Name] - .Resolvers - .ContainsResolvers(schemaName) || - typeMetadataContext.Resolvers.ContainsResolvers(schemaName))) + currentTypeContext.Fields[selection.Field.Name].Bindings + .ContainsSubgraph(schemaName)) { score++; @@ -92,10 +83,21 @@ private static int EvaluateSubgraphCompatibilityScore( return score; } + private static bool PathCanBeResolved( + FusionGraphConfiguration configuration, + SelectionPath? parentSelectionPath, + ObjectTypeMetadata typeMetadataContext, + string schemaName) + { + return parentSelectionPath is null || + typeMetadataContext.Resolvers.ContainsResolvers(schemaName) || + configuration.EnsurePathCanBeResolvedFromRoot(schemaName, parentSelectionPath); + } + public static bool EnsurePathCanBeResolvedFromRoot( this FusionGraphConfiguration configuration, string subgraphName, - SelectionPath? path) + SelectionPath path) { var current = path; diff --git a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs index b9821efcb3c..0773161b997 100644 --- a/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs +++ b/src/HotChocolate/Fusion/test/Core.Tests/DemoIntegrationTests.cs @@ -2970,6 +2970,75 @@ interface Node { MatchMarkdownSnapshot(request, result); } + [Fact] + public async Task Subgraph_Containing_More_Selections_Is_Chosen() + { + // arrange + var subgraphA = await TestSubgraph.CreateAsync( + """ + type Query { + productBySlug: Product + } + + type Product { + id: ID! + } + """); + + var subgraphB = await TestSubgraph.CreateAsync( + """ + type Query { + productById(id: ID!): Product + } + + type Product { + id: ID! + author: Author + } + + type Author { + name: String! + } + """); + + var subgraphC = await TestSubgraph.CreateAsync( + """ + type Query { + productById(id: ID!): Product + } + + type Product { + id: ID! + author: Author + } + + type Author { + name: String! + rating: Int! + } + """); + + using var subgraphs = new TestSubgraphCollection(output, [subgraphA, subgraphB, subgraphC]); + var executor = await subgraphs.GetExecutorAsync(); + var request = """ + query { + productBySlug { + author { + name + rating + } + } + } + """; + + // act + var result = await executor.ExecuteAsync(request); + + // assert + MatchMarkdownSnapshot(request, result); + } + + public sealed class HotReloadConfiguration : IObservable { private GatewayConfiguration _configuration; From eebeb4cb24c3548f8d20aeb9be4ac4477b465e2d Mon Sep 17 00:00:00 2001 From: Daniel Wust Date: Sun, 10 Aug 2025 12:31:51 +0200 Subject: [PATCH 11/13] increase score for empty selection --- .../Fusion/src/Core/Planning/QueryPlannerHelpers.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs index abd381e10ea..907ac31ca1c 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs @@ -59,6 +59,13 @@ private static int EvaluateSubgraphCompatibilityScore( { var (currentSelections, currentTypeContext) = stack.Pop(); + // If there are no selections at the current node, it means the subgraph + // can resolve the path up to this point without requiring any further fields, so we increase the score. + if (currentSelections.Count == 0) + { + score++; + } + foreach (var selection in currentSelections) { if (!selection.Field.IsIntrospectionField && @@ -89,15 +96,14 @@ private static bool PathCanBeResolved( ObjectTypeMetadata typeMetadataContext, string schemaName) { - return parentSelectionPath is null || - typeMetadataContext.Resolvers.ContainsResolvers(schemaName) || + return typeMetadataContext.Resolvers.ContainsResolvers(schemaName) || configuration.EnsurePathCanBeResolvedFromRoot(schemaName, parentSelectionPath); } public static bool EnsurePathCanBeResolvedFromRoot( this FusionGraphConfiguration configuration, string subgraphName, - SelectionPath path) + SelectionPath? path) { var current = path; From 769feebad60b4c2ef1b17e2e947efa71854ae5d2 Mon Sep 17 00:00:00 2001 From: Daniel Wust Date: Sun, 10 Aug 2025 12:45:03 +0200 Subject: [PATCH 12/13] add EnsurePathOrTypeCanBeResolvedFromRoot helper --- .../Fusion/src/Core/Planning/QueryPlannerHelpers.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs index 907ac31ca1c..3381579a964 100644 --- a/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs +++ b/src/HotChocolate/Fusion/src/Core/Planning/QueryPlannerHelpers.cs @@ -47,10 +47,7 @@ private static int EvaluateSubgraphCompatibilityScore( { var score = 0; - if (!PathCanBeResolved(configuration, parentSelectionPath, typeMetadataContext, schemaName)) - { - return score; - } + var pathOrTypeCanBeResolvedFromRoot = EnsurePathOrTypeCanBeResolvedFromRoot(configuration, parentSelectionPath, typeMetadataContext, schemaName); var stack = new Stack<(IReadOnlyList selections, ObjectTypeMetadata typeContext)>(); stack.Push((selections, typeMetadataContext)); @@ -70,7 +67,10 @@ private static int EvaluateSubgraphCompatibilityScore( { if (!selection.Field.IsIntrospectionField && currentTypeContext.Fields[selection.Field.Name].Bindings - .ContainsSubgraph(schemaName)) + .ContainsSubgraph(schemaName) && + (pathOrTypeCanBeResolvedFromRoot || + currentTypeContext.Fields[selection.Field.Name].Resolvers + .ContainsResolvers(schemaName))) { score++; @@ -90,7 +90,7 @@ private static int EvaluateSubgraphCompatibilityScore( return score; } - private static bool PathCanBeResolved( + private static bool EnsurePathOrTypeCanBeResolvedFromRoot( FusionGraphConfiguration configuration, SelectionPath? parentSelectionPath, ObjectTypeMetadata typeMetadataContext, From c0702389c7056752df253ed8bcb4daa1b377b2e1 Mon Sep 17 00:00:00 2001 From: Daniel Wust Date: Sun, 10 Aug 2025 12:46:35 +0200 Subject: [PATCH 13/13] add test --- ...ph_Containing_More_Selections_Is_Chosen.md | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Subgraph_Containing_More_Selections_Is_Chosen.md diff --git a/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Subgraph_Containing_More_Selections_Is_Chosen.md b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Subgraph_Containing_More_Selections_Is_Chosen.md new file mode 100644 index 00000000000..de78f1291da --- /dev/null +++ b/src/HotChocolate/Fusion/test/Core.Tests/__snapshots__/DemoIntegrationTests.Subgraph_Containing_More_Selections_Is_Chosen.md @@ -0,0 +1,89 @@ +# Subgraph_Containing_More_Selections_Is_Chosen + +## Result + +```json +{ + "data": { + "productBySlug": { + "author": { + "name": "string", + "rating": 123 + } + } + } +} +``` + +## Request + +```graphql +{ + productBySlug { + author { + name + rating + } + } +} +``` + +## QueryPlan Hash + +```text +97A88121800B8E021163EA531B156FA2A6CDF11F +``` + +## QueryPlan + +```json +{ + "document": "{ productBySlug { author { name rating } } }", + "rootNode": { + "type": "Sequence", + "nodes": [ + { + "type": "Resolve", + "subgraph": "Subgraph_1", + "document": "query fetch_productBySlug_1 { productBySlug { __fusion_exports__1: id } }", + "selectionSetId": 0, + "provides": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 0 + ] + }, + { + "type": "Resolve", + "subgraph": "Subgraph_3", + "document": "query fetch_productBySlug_2($__fusion_exports__1: ID!) { productById(id: $__fusion_exports__1) { author { name rating } } }", + "selectionSetId": 1, + "path": [ + "productById" + ], + "requires": [ + { + "variable": "__fusion_exports__1" + } + ] + }, + { + "type": "Compose", + "selectionSetIds": [ + 1 + ] + } + ] + }, + "state": { + "__fusion_exports__1": "Product_id" + } +} +``` +