From 3dfee8de1c2cd95ad1f2c4d3f5198ff343e6e112 Mon Sep 17 00:00:00 2001 From: Kevin Carroll Date: Sun, 10 Mar 2024 10:25:54 -0700 Subject: [PATCH] fixed self referencing complex input object lists --- src/.editorconfig | 6 +-- .../InputValueResolverMethodGenerator.cs | 43 ++++++++++-------- .../Execution/GeneralQueryExecutionTests3.cs | 45 +++++++++++++++++++ .../SelfReferencingInputObject.cs | 20 +++++++++ .../SelfReferencingInputObjectController.cs | 38 ++++++++++++++++ 5 files changed, 130 insertions(+), 22 deletions(-) create mode 100644 src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/SelfReferencingInputObject.cs create mode 100644 src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/SelfReferencingInputObjectController.cs diff --git a/src/.editorconfig b/src/.editorconfig index 55608d4fe..c2d30edfd 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -197,7 +197,7 @@ dotnet_naming_style.begins_with_i.word_separator = dotnet_naming_style.begins_with_i.capitalization = pascal_case - - [*.{cs,vb}] -dotnet_style_prefer_compound_assignment=true:suggestion \ No newline at end of file +dotnet_style_prefer_compound_assignment=true:suggestion + +file_header_template = *************************************************************\n project: graphql-aspnet\n --\n repo: https://github.com/graphql-aspnet\n docs: https://graphql-aspnet.github.io\n --\n License: MIT\n ************************************************************* \ No newline at end of file diff --git a/src/graphql-aspnet/Internal/Resolvers/InputValueResolverMethodGenerator.cs b/src/graphql-aspnet/Internal/Resolvers/InputValueResolverMethodGenerator.cs index a84bc1a3b..2bb8e8399 100644 --- a/src/graphql-aspnet/Internal/Resolvers/InputValueResolverMethodGenerator.cs +++ b/src/graphql-aspnet/Internal/Resolvers/InputValueResolverMethodGenerator.cs @@ -46,7 +46,7 @@ public InputValueResolverMethodGenerator(ISchema schema) public IInputValueResolver CreateResolver(GraphTypeExpression typeExpression) { // used for variable definitions - return this.CreateResolver(typeExpression, null); + return this.CreateResolverInternal(typeExpression, null); } /// @@ -57,7 +57,7 @@ public IInputValueResolver CreateResolver(GraphTypeExpression typeExpression) /// IQueryInputValueResolver. public IInputValueResolver CreateResolver(IInputGraphField field) { - return this.CreateResolver(field.TypeExpression, field); + return this.CreateResolverInternal(field.TypeExpression, field); } /// @@ -68,10 +68,10 @@ public IInputValueResolver CreateResolver(IInputGraphField field) /// IQueryInputValueResolver. public IInputValueResolver CreateResolver(IGraphArgument argument) { - return this.CreateResolver(argument.TypeExpression, argument); + return this.CreateResolverInternal(argument.TypeExpression, argument); } - private IInputValueResolver CreateResolver(GraphTypeExpression typeExpression, IDefaultValueSchemaItem defaultValueProvider) + private IInputValueResolver CreateResolverInternal(GraphTypeExpression typeExpression, IDefaultValueSchemaItem defaultValueProvider) { Validation.ThrowIfNull(typeExpression, nameof(typeExpression)); @@ -79,11 +79,17 @@ private IInputValueResolver CreateResolver(GraphTypeExpression typeExpression, I if (graphType == null) return null; - return this.CreateResolver(graphType, typeExpression, defaultValueProvider); + return this.CreateResolverInternal(graphType, typeExpression, defaultValueProvider); } - private IInputValueResolver CreateResolver(IGraphType graphType, GraphTypeExpression expression, IDefaultValueSchemaItem defaultValueProvider) + private IInputValueResolver CreateResolverInternal(IGraphType graphType, GraphTypeExpression expression, IDefaultValueSchemaItem defaultValueProvider, Dictionary trackedComplexResolvers = null) { + // keep a list of complex value resolvers that were generated in this run + // to prevent infinite loops for self referencing objects + // all instnaces where a complex object needs to be resolved will use + // the same referenced resolver, scoped to this single argument that is being generated + trackedComplexResolvers = trackedComplexResolvers ?? new Dictionary(); + // extract the core resolver for the input type being processed IInputValueResolver coreResolver = null; Type coreType = null; @@ -101,7 +107,7 @@ private IInputValueResolver CreateResolver(IGraphType graphType, GraphTypeExpres else if (graphType is IInputObjectGraphType inputType) { coreType = _schema.KnownTypes.FindConcreteType(inputType); - coreResolver = this.CreateInputObjectResolver(inputType, coreType, defaultValueProvider); + coreResolver = this.CreateInputObjectResolver(inputType, coreType, defaultValueProvider, trackedComplexResolvers); } // wrap any list wrappers around core resolver according to the type expression @@ -117,23 +123,22 @@ private IInputValueResolver CreateResolver(IGraphType graphType, GraphTypeExpres return coreResolver; } - private IInputValueResolver CreateInputObjectResolver(IInputObjectGraphType inputType, Type type, IDefaultValueSchemaItem defaultValueProvider) + private IInputValueResolver CreateInputObjectResolver( + IInputObjectGraphType inputType, + Type type, + IDefaultValueSchemaItem defaultValueProvider, + Dictionary trackedComplexResolvers) { + if (trackedComplexResolvers.TryGetValue(inputType, out var alreadyBuiltResolver)) + return alreadyBuiltResolver; + var inputObjectResolver = new InputObjectValueResolver(inputType, type, _schema, defaultValueProvider); + trackedComplexResolvers.Add(inputType, inputObjectResolver); foreach (var field in inputType.Fields) { - IInputValueResolver childResolver; - if (field.TypeExpression.TypeName == inputType.Name) - { - childResolver = inputObjectResolver; - } - else - { - var graphType = _schema.KnownTypes.FindGraphType(field.TypeExpression.TypeName); - childResolver = this.CreateResolver(graphType, field.TypeExpression, field); - } - + var graphType = _schema.KnownTypes.FindGraphType(field.TypeExpression.TypeName); + var childResolver = this.CreateResolverInternal(graphType, field.TypeExpression, field, trackedComplexResolvers); inputObjectResolver.AddFieldResolver(field.Name, childResolver); } diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/GeneralQueryExecutionTests3.cs b/src/unit-tests/graphql-aspnet-tests/Execution/GeneralQueryExecutionTests3.cs index 8a5b18c00..e14f2a47c 100644 --- a/src/unit-tests/graphql-aspnet-tests/Execution/GeneralQueryExecutionTests3.cs +++ b/src/unit-tests/graphql-aspnet-tests/Execution/GeneralQueryExecutionTests3.cs @@ -45,5 +45,50 @@ public async Task Record_asInputObject_RendersObjectCorrectly() var result = await server.RenderResult(builder); CommonAssertions.AreEqualJsonStrings(expectedOutput, result); } + + [Test] + public async Task Self_Referencing_Nested_List_Input() + { + var server = new TestServerBuilder() + .AddGraphQL(o => + { + o.AddType(); + o.ResponseOptions.ExposeExceptions = true; + }) + .Build(); + + // totalPeople exists on base controller + // totalEmployees exists on the added EmployeeController + var builder = server.CreateQueryContextBuilder() + .AddQueryText(@"query { + countNestings(item: + { + name: ""root"", + children: [ + { + name: ""child1"", + children: [ + { name: ""child1.1""}, + {name: ""child1.2""} + ] + }, + {name: ""child2""} + ] + }) + }"); + + var expectedOutput = + @"{ + ""data"": { + ""countNestings"" : 5 + } + }"; + + var result = await server.ExecuteQuery(builder); + Assert.AreEqual(0, result.Messages.Count); + + var renderedResult = await server.RenderResult(builder); + CommonAssertions.AreEqualJsonStrings(expectedOutput, renderedResult); + } } } \ No newline at end of file diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/SelfReferencingInputObject.cs b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/SelfReferencingInputObject.cs new file mode 100644 index 000000000..13c707eb1 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/SelfReferencingInputObject.cs @@ -0,0 +1,20 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution.TestData.ExecutionPlanTestData +{ + using System.Collections.Generic; + + public class SelfReferencingInputObject + { + public string Name { get; set; } + + public IEnumerable Children { get; set; } + } +} diff --git a/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/SelfReferencingInputObjectController.cs b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/SelfReferencingInputObjectController.cs new file mode 100644 index 000000000..7663a5601 --- /dev/null +++ b/src/unit-tests/graphql-aspnet-tests/Execution/TestData/ExecutionPlanTestData/SelfReferencingInputObjectController.cs @@ -0,0 +1,38 @@ +// ************************************************************* +// project: graphql-aspnet +// -- +// repo: https://github.com/graphql-aspnet +// docs: https://graphql-aspnet.github.io +// -- +// License: MIT +// ************************************************************* + +namespace GraphQL.AspNet.Tests.Execution.TestData.ExecutionPlanTestData +{ + using System.Collections.Generic; + using GraphQL.AspNet.Attributes; + using GraphQL.AspNet.Controllers; + + public class SelfReferencingInputObjectController : GraphController + { + [QueryRoot] + public int CountNestings(SelfReferencingInputObject item) + { + var i = 0; + var stack = new Stack(); + stack.Push(item); + while (stack.Count > 0) + { + var curItem = stack.Pop(); + i++; + if (curItem.Children != null) + { + foreach (var child in curItem.Children) + stack.Push(child); + } + } + + return i; + } + } +}