Skip to content

List of union type inside object type causes an exception when using fluent API #5528

Description

@dotarj

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

I've ran into an exception ("Argument types do not match") when I have a class which has a property which is a list of a union type. If I configure this object structure using the fluent API, UnionTypeDescriptor.RuntimeType will always be set to object:

public static UnionTypeDescriptor FromSchemaType(
    IDescriptorContext context,
    Type schemaType)
{
    var descriptor = New(context, schemaType);
    descriptor.Definition.RuntimeType = typeof(object);
    return descriptor;
}

The method QueryableProjectionListHandler.TryHandleLeave executes this code: Expression.Bind(field.Member, select). In case of the list of union type, field.Member is a list of the actual CLR type of the union object (List<FileOrFolder in the sample project), but the select variable is a MethodCallExpression with type List<object>, which causes the exception.

I cannot change the generic type of the list to object instead of the actual type, as the ORM will not understand the association when I use object.

Is there a way to work around this problem (with the fluent API)?

Steps to reproduce

This repository can be used to reproduce the error. There is a SQL script to setup a database.

Relevant log output

{
  "errors": [
    {
      "message": "Unexpected Execution Error",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "tenants"
      ],
      "extensions": {
        "message": "Argument types do not match",
        "stackTrace": "   at System.Linq.Expressions.Expression.Bind(MemberInfo member, Expression expression)\r
   at HotChocolate.Data.Projections.Expressions.Handlers.QueryableProjectionListHandler.TryHandleLeave(QueryableProjectionContext context, ISelection selection, ISelectionVisitorAction& action)\r
   at HotChocolate.Data.Projections.ProjectionVisitor`1.Leave(ISelection selection, TContext context)\r
   at HotChocolate.Data.Projections.SelectionVisitor`1.Visit(ISelection selection, TContext context)\r
   at HotChocolate.Data.Projections.ProjectionVisitor`1.Visit(ISelection selection, TContext context)\r
   at HotChocolate.Data.Projections.SelectionVisitor`1.VisitObjectType(IOutputField field, ObjectType objectType, ISelection selection, TContext context)\r
   at HotChocolate.Data.Projections.Expressions.QueryableProjectionVisitor.VisitObjectType(IOutputField field, ObjectType objectType, ISelection selection, QueryableProjectionContext context)\r
   at HotChocolate.Data.Projections.SelectionVisitor`1.VisitChildren(IOutputField field, TContext context)\r
   at HotChocolate.Data.Projections.SelectionVisitor`1.Visit(IOutputField field, TContext context)\r
   at HotChocolate.Data.Projections.ProjectionVisitor`1.Visit(IOutputField field, TContext context)\r
   at HotChocolate.Data.Projections.ProjectionVisitor`1.Visit(TContext context, ISelection selection)\r
   at HotChocolate.Data.Projections.ProjectionVisitor`1.Visit(TContext context)\r
   at HotChocolate.Data.Projections.Expressions.QueryableProjectionProvider.<>c__5`1.<CreateApplicator>b__5_0(IResolverContext context, Object input)\r
   at HotChocolate.Data.Projections.Expressions.QueryableProjectionProvider.<>c__DisplayClass4_0`1.<<CreateExecutor>g__ExecuteAsync|1>d.MoveNext()\r
--- End of stack trace from previous location ---\r
   at HotChocolate.Types.Pagination.PagingMiddleware.InvokeAsync(IMiddlewareContext context)\r
   at HotChocolate.Utilities.MiddlewareCompiler`1.ExpressionHelper.AwaitTaskHelper(Task task)\r
   at HotChocolate.Execution.Processing.Tasks.ResolverTask.ExecuteResolverPipelineAsync(CancellationToken cancellationToken)\r
   at HotChocolate.Execution.Processing.Tasks.ResolverTask.TryExecuteAsync(CancellationToken cancellationToken)"
      }
    }
  ],
  "data": {
    "tenants": null
  }
}

Additional Context?

No response

Product

Hot Chocolate

Version

13.0.0-preview.75

Activity

  1. Bery0za commented on Aug 4, 2023

    @Bery0za

    I'm experiencing the same issue, any usage of the lists of the union types with UseProjection() causes the errors or incorrect behavior.

    I've tried to change the generic type of the collection property to object as a workaround mentioned by @dotarj:

    class ContainingEntity {
        /*List<EntityWithSubtypes> ListOfUnionEntities { get; set; }*/
        List<object> ListOfUnionEntities { get; set; }
    }

    It helps to solve the Argument types do not match error, but if I query entities in the collection by a concrete type then I get null instead of any array:

    {
        containingEntities {
            listOfUnionEntities {
                ... on SubtypeA {
                    someProperty
                }
            }
        }
    }
    "containingEntities": {
        "listOfUnionEntities": null
    }

    If I query union entities by some common interface or when there are only one instance of EntityWithSubtypes in the database then the solution works.

    {
        containingEntities {
            listOfUnionEntities {
                ... on EntityWithSubtypesInterface {
                    someProperty
                }
            }
        }
    }
    "containingEntities": {
        "listOfUnionEntities": [
            ...actual entities...
        ]
    }

    I also tried to check if I can query those entities from a separate repository.

    public class Query
    {
       public IQueryable<EntityWithSubtypes> UnionEntities(IRepository repository) => repository.EntitiesWithSubtypes;
    }
    
    public class QueryType : ObjectType<Query>
    {
        protected override void Configure(IObjectTypeDescriptor<Query> descriptor)
        {
            descriptor
                .Field(q => q.UnionEntities)
                .Type<ListType<NonNullType<EntityWithSubtypesUnion>>>
                .UseProjection();
        }
    }
    {
        unionEntities {
            ... on EntityWithSubtypesInterface {
               someProperty
            }
        }
    }

    But I'm getting the error Unable to cast object of type…, no matter if I query entities by a concrete type or by the common interface.

    {
        "errors": [
            {
                "message": "Unexpected Execution Error",
                "locations": [
                    {
                        "line": 2,
                        "column": 5
                    }
                ],
                "path": [
                    "unionEntities"
                ],
                "extensions": {
                    "message": "Unable to cast object of type 'System.Linq.Expressions.Expression1`1[System.Func`2[System.Object,System.Object]]' to type 'System.Linq.Expressions.Expression`1[System.Func`2[ListOfUnionTest.EntityWithSubtypes,ListOfUnionTest.EntityWithSubtypes]]'.",
                    "stackTrace": "   at HotChocolate.Data.Projections.Expressions.QueryableProjectionScopeExtensions.Project[T](QueryableProjectionScope scope)
       at HotChocolate.Data.Projections.Expressions.QueryableProjectionContextExtensions.Project[T](QueryableProjectionContext context)
       at HotChocolate.Data.Projections.Expressions.QueryableProjectionProvider.<CreateApplicator>b__7_0[TEntityType](IResolverContext context, Object input)
       at HotChocolate.Data.Projections.Expressions.QueryableProjectionProvider.<>c__DisplayClass4_0`1.<<CreateExecutor>g__ExecuteAsync|1>d.MoveNext()
    --- End of stack trace from previous location ---
       at HotChocolate.Execution.Processing.Tasks.ResolverTask.ExecuteResolverPipelineAsync(CancellationToken cancellationToken)
       at HotChocolate.Execution.Processing.Tasks.ResolverTask.TryExecuteAsync(CancellationToken cancellationToken)"
                }
            }
        ],
        "data": {
            "unionEntities ": null
        }
    }

    If I change the return type of the query to the collection of objects, then I'm getting the same behavior as in the first part of this comment, i. e. queries only work when I try to get the union entities by the common interface.

    public class Query
    {
       public /*IQueryable<EntityWithSubtypes>*/ IQueryable<object> UnionEntities(IRepository repository) => repository.EntitiesWithSubtypes;
    }
  2. Bery0za commented on Aug 5, 2023

    @Bery0za

    Well, I found a solution to the described problem and it is quite simple and straigtforward. UnionType class has a generic extension UnionType<T> which is supposed to be used with some marker interface, but it's also possible to use the base class of the union types as a generic parameter. It helps to get rid of Argument types do not match and Unable to cast object of type… errors with the collections which include entities of the various subtypes (and there is no need to change the generic type of the collections to object).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions