graphql-platform icon indicating copy to clipboard operation
graphql-platform copied to clipboard

Sort with Variables tries to instantiate an object from the target collection, fails if no parameterless constructor available

Open ghost opened this issue 2 years ago • 7 comments

Is there an existing issue for this?

  • [X] I have searched the existing issues

Product

Hot Chocolate

Describe the bug

I have a business backend that deals, among other entities, with subjects and their addresses. In the database, handled via Entity Framework Core 7, there is a relation between the subject and its addresses, so that inside the Subject entity there is an "Addresses" navigation property which is defined as an ICollection<Address>. Both classes have no parameterless constructor to fill non-nullable property values.

When returning an IQueryable to HotChocolate, I have the query set up like this:

  [UsePaging(IncludeTotalCount = true, MaxPageSize = 100)]
  [UseFiltering(typeof(SubjectFilterType))]
  [UseSorting(typeof(SubjectSortInputType))]
  public IQueryable<ISubject> ListSubjects([Service] IReadRepository<Subject> repo)
    => repo.AsQueryable(new SubjectsNonDeletedSpec());

The readRepository wraps and ultimately returns the DbSet from Entity Framework, just mixing in a .Where condition on the DeletionDate field.

For the purpose of the bug, I don't think filtering middleware is an issue; the sorting middleware is configured with these settings:

  public class SubjectSortInputType : SortInputType<Subject>
  {
    protected override void Configure(ISortInputTypeDescriptor<Subject> descriptor)
    {
      descriptor.BindFieldsImplicitly();

      descriptor
        .Field(f => f.Addresses.FirstOrDefault(address => address.AddressType == AddressType.LegalResidential))
        .Type<AddressSortInputType>()
        .Name("legalResidentialAddress");
    }
  }

In this way, a virtual "legalResidentialAddress" field is added to the GraphQL schema, and it is supposed to be able to be used for sorting.

Indeed when the query is like this:

  query subsorting {
    subject {
      listSubjects(order: { legalResidentialAddress: { cityName: ASC }})
      {
        nodes {
          shorthandDescription
        }
      }
    }
  }

the expected results are received.

But when the query includes variables, like this:

  query subsorting($order: [SubjectSortInput!]) {
    subject {
      listSubjects(order: $order)
      {
        nodes {
          shorthandDescription
        }
      }
    }
  }

  {
    "order": [
      {
        "legalResidentialAddress": {
          "cityName": "ASC"
        }
      }
    ]
  }

An exception is raised and no errors are returned. Exception details and stacktrace is below.

I don't expect the difference between using and not using variables to produce the instantiation of an object from the target class of the sorting collection; why is it happening? Is a parameterless constructor actually needed?

Steps to reproduce

Due to the complexity of producing a sample showcase github repo, it will be added if the above description is not enough to understand the issue clearly.

Relevant log output

[19:32:26 INF] Now listening on: http://localhost:5000
   [19:32:26 INF] Application started. Press Ctrl+C to shut down.
   [19:32:26 INF] Hosting environment: Development
   [19:32:26 INF] Content root path: /workspace/backend/src/Web/bin/Debug/net7.0
   [19:32:32 ERR] Error on GraphQL request
   HotChocolate.GraphQLException: Variable `order` got an invalid value.
      at HotChocolate.Execution.Processing.VariableCoercionHelper.CoerceVariableValue(VariableDefinitionNode variableDefinition, IInputType variableType, Object value)
      at HotChocolate.Execution.Processing.VariableCoercionHelper.CoerceVariableValues(ISchema schema, IReadOnlyList`1 variableDefinitions, IReadOnlyDictionary`2 values, IDictionary`2 coercedValues)
      at HotChocolate.Execution.Pipeline.PipelineTools.CoerceVariables(IRequestContext context, VariableCoercionHelper coercionHelper, IReadOnlyList`1 variableDefinitions)
      at HotChocolate.Execution.Pipeline.OperationVariableCoercionMiddleware.InvokeAsync(IRequestContext context)
      at HotChocolate.Execution.Pipeline.OperationResolverMiddleware.InvokeAsync(IRequestContext context)
      at HotChocolate.Execution.Pipeline.OperationComplexityMiddleware.InvokeAsync(IRequestContext context)
      at HotChocolate.Execution.Pipeline.OperationCacheMiddleware.InvokeAsync(IRequestContext context)
      at HotChocolate.Execution.Pipeline.DocumentValidationMiddleware.InvokeAsync(IRequestContext context)
      at HotChocolate.Execution.Pipeline.DocumentParserMiddleware.InvokeAsync(IRequestContext context)
      at HotChocolate.Execution.Pipeline.DocumentCacheMiddleware.InvokeAsync(IRequestContext context)
      at HotChocolate.Execution.Pipeline.TimeoutMiddleware.InvokeAsync(IRequestContext context)
      at HotChocolate.Execution.Pipeline.ExceptionMiddleware.InvokeAsync(IRequestContext context)
   [19:32:32 ERR] Single error details: HC0016 Variable `order` got an invalid value. at path null
   System.MissingMethodException: Cannot dynamically create an instance of type 'XYZ.Core.Anag.SubjectAggregate.Address'. Reason: No parameterless constructor defined.
      at System.RuntimeType.ActivatorCache..ctor(RuntimeType rt)
      at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean wrapExceptions)
      at HotChocolate.Utilities.DictionaryToObjectConverter.VisitObject(IReadOnlyDictionary`2 dictionary, ConverterContext context)
      at HotChocolate.Utilities.DictionaryVisitor`1.Visit(Object value, TContext context)
      at HotChocolate.Utilities.DictionaryToObjectConverter.Convert(Object from, Type to)
      at HotChocolate.Types.InputParser.ConvertValue(Type requestedType, Object value)
      at HotChocolate.Types.InputParser.ParseObject(IValueNode resultValue, InputObjectType type, Path path, Int32 stack, Boolean defaults)
      at HotChocolate.Types.InputParser.ParseLiteralInternal(IValueNode value, IType type, Path path, Int32 stack, Boolean defaults, IInputFieldInfo field)
      at HotChocolate.Types.InputParser.ParseLiteralInternal(IValueNode value, IType type, Path path, Int32 stack, Boolean defaults, IInputFieldInfo field)
      at HotChocolate.Types.InputParser.ParseList(IValueNode resultValue, ListType type, Path path, Int32 stack, Boolean defaults, IInputFieldInfo field)
      at HotChocolate.Types.InputParser.ParseLiteralInternal(IValueNode value, IType type, Path path, Int32 stack, Boolean defaults, IInputFieldInfo field)
      at HotChocolate.Types.InputParser.ParseLiteral(IValueNode value, IType type, Path path)
      at HotChocolate.Execution.Processing.VariableCoercionHelper.CoerceVariableValue(VariableDefinitionNode variableDefinition, IInputType variableType, Object value)

Additional Context?

No response

Version

13.2.1

ghost avatar Sep 20 '23 09:09 ghost

The issue still persists with version 13.5.1; I made a showcase application in https://github.com/alex-mazzariol/hotchocolate-issue-6545.

If anybody can point me in the right direction, since I also noticed some recent work around the classes that digest the variable values, I might be even able to find the problem and propose a fix...

alex-mazzariol avatar Oct 21 '23 10:10 alex-mazzariol

As far as I can gather debugging, it seems like the VariableCoercionHelper asks the InputParser to convert the variables into the appropriate input type, but for some reason, when it comes to mapping the added field (inside InputParser.ParseObject) the type from the Field<T>() lambda is taken into consideration, and not the type specified via the .Type<AddressSortInputType>().

alex-mazzariol avatar Oct 27 '23 14:10 alex-mazzariol

I've been able to find and, apparently, solve the issue - inside src/HotChocolate/Core/src/Types/Types/InputParser.cs around line 260:

var value = ParseLiteralInternal(
    literal,
    field.Type,
    fieldPath,
    stack,
    defaults,
    field);
value = FormatValue(field, value);
value = ConvertValue(field.RuntimeType, value);

if (field.IsOptional)
{
    value = new Optional(value, true);
}

The issue is in the call to ConvertValue(field.RuntimeType, value); where the type parameter is just the RuntimeType of the field, which will be the Address type that cannot be instantiated.

If I replace the parameter with field.Type.RuntimeType, so that the piece of code becomes

var value = ParseLiteralInternal(
    literal,
    field.Type,
    fieldPath,
    stack,
    defaults,
    field);
value = FormatValue(field, value);
value = ConvertValue(field.Type.RuntimeType, value);

if (field.IsOptional)
{
    value = new Optional(value, true);
}

And build HotChocolate again, I am able to sort (correctly) with the input type applied to the correct field, and can continue sorting by the other existing fields as well.

Could someone verify the one-line fix with the tests? I am unable to find instructions on how to run them, and opening a PR for a oneliner might be a little too complex for the result.

alex-mazzariol avatar Oct 30 '23 13:10 alex-mazzariol

+1 - hitting this, as well. Seems like a bug to me!

mattkgross avatar Mar 24 '25 21:03 mattkgross

Hi guys, at first I wanna say that I really appreciate this project and I worked a couple of times with it since my coding journey began in university.

I stumbled on something similar like this bug which was already mentioned above. But the pull request related to this issues doesnt seem to be merged right now @michaelstaib so I am unsure if this error is already fixed or just an error by my side.

.net code i am using:

   public class ArticleQuery : ObjectTypeExtension<Query>
    {
        protected override void Configure(IObjectTypeDescriptor<Query> descriptor)
        {
            descriptor
                .Field("articles")
                .UseOffsetPaging()
                .UseProjection()
                .UseFiltering()
                .UseSorting()
                .ResolveWith<ArticleResolver>(r => r.GetArticles())
                .Type<ListType<NonNullType<ArticleType>>>();
        }
    }

    public class ArticleType : ObjectType<Article>
    {

    }

    public class ArticleResolver
    {
        private readonly IService _baseService;

        public ArticleResolver(IService service)
        {
            _baseService = service;
        }
        public IQueryable<Article> GetArticles()
        {
            return _baseService.GetQueryable<Article, Guid>();
        }
    }

This is a very simple query which orders articles by their creation time:

query articles {
  articles(order: [ {
     scrapeRun:  {
        startedAt: DESC
     }
  }]) {
    pageInfo {
      hasNextPage
      hasPreviousPage
    }
    items {
      title
      isActive
      audios {
        createdAt
      }
      scrapeRun {
        startedAt
      }
    }
  }
}

This request works fine and as it should.

But as soon as I am using basically the same query (just with arguments / variables) the requested data is no longer sorted:

query articles($order: [ArticleSortInput!]) {
  articles(order: $order) {
    pageInfo {
      hasNextPage
      hasPreviousPage
    }
    items {
      title
      isActive
      audios {
        createdAt
      }
      scrapeRun {
        startedAt
      }
    }
  }
}
{
  "order": [
    {
      "scrapeRun": {
        "startedAt": "DESC"
      }
    }
  ]
}
builder.Services.AddGraphQLServer()
    .AddProjections()
    .AddFiltering()
    .AddSorting()
    .AddQueryType<Query>()
    .AddTypeExtension<ArticleQuery>()
    .AddHttpRequestInterceptor<DynamicFieldInterceptor>()
    .ModifyCostOptions(options =>
    {
        // Maximale Feld-Kosten (Default: 1 000)
        options.MaxFieldCost = 5_000;
        // Maximale Typ-Kosten (Default: 1 000)
        options.MaxTypeCost = 5_000;
        // Standardkosten für asynchrone Resolver (Default: 10.0)
        options.DefaultResolverCost = 5.0;
        // Bei Bedarf Limits temporär ausschalten:
        // options.EnforceCostLimits = false;
    });

I had to update my cost options, because at first my backend code would not even have expected the given query with its variables. It returned an error which said that the costs are too high. But that makes no sense to me as well because the code without variables is just working fine.

And I kinda dont get this bevhaviour, I though costs are calculated based on the fields which are requested?

franzfilip avatar Apr 27 '25 20:04 franzfilip

@franzfilip this is a completely different thing and correct. Check out or our youtube episode on cost.

https://www.youtube.com/watch?feature=shared&embeds_referring_euri=https%3A%2F%2Fchillicream.com%2F&source_ve_path=Mjg2NjQsMTY0NTA2&v=R6Rq4kU_GfM&themeRefresh=1

michaelstaib avatar Apr 28 '25 07:04 michaelstaib

Hitting this issue too, HC fails to parse variable of ListFilterInputType type

tsareg avatar Jul 10 '25 10:07 tsareg