ChilliCream / ChilliCream/graphql-platform

Using records as a projection fails on QueryableProjectionScopeExtensions

Open
#6,487 2 comments 5 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

🌶️ hot chocolate Area: Data
Dominant language
C#
Stars
5.8k
Forks
810
Avg merge
15h 39m
Merged PRs (30d)
98

Description

Is there an existing issue for this?
  • I have searched the existing issues
Product

Hot Chocolate

Describe the bug

If you use a record in a projection, it will fail because of trying to use the default constructor on it.
Type 'PriceData' does not have a default constructor (Parameter 'type')

Steps to reproduce
  1. Create a record
  2. Create a query
  3. Mark query as projection
  4. Call the query
  5. Response has a result with error node
Relevant log output
message
: 
"Type 'PriceData' does not have a default constructor (Parameter 'type')"
stackTrace
: 
"   at System.Linq.Expressions.Expression.New(Type type)\r\n   at HotChocolate.Data.Projections.Expressions.QueryableProjectionScopeExtensions.CreateMemberInit(QueryableProjectionScope scope)\r\n   at HotChocolate.Data.Projections.Expressions.QueryableProjectionScopeExtensions.CreateMemberInitLambda(QueryableProjection 
Additional Context?

You can create records with Expressions and turn them in a MemberInitExpression. Still I don't know if this safe and sound.
Below is an attempt to patch CreateMemberInit but this is untested

private static Expression GetMemberInit(Object val)
    {
        var isRecord = ((TypeInfo) val.Key).DeclaredProperties.Any(x => x.Name == "EqualityContract");
        if (isRecord)
        {
            var ctor = val.Key.GetConstructors()[0];
            return Expression.MemberInit(Expression.New(ctor, val.Value.Select(x => x.Expression)));
        }
        else
        {
            var ctor = Expression.New(val.Key);
            return Expression.MemberInit(ctor, val.Value);
        }
    }

public static Expression CreateMemberInit(this QueryableProjectionScope scope)
    {
        if (scope.HasAbstractTypes())
        {
            Expression lastValue = Expression.Default(scope.RuntimeType);

            foreach (var val in scope.GetAbstractTypes())
            {
                Expression memberInit = GetMemberInit(val);

                lastValue = Expression.Condition(
                    Expression.TypeIs(scope.Instance.Peek(), val.Key),
                    Expression.Convert(memberInit, scope.RuntimeType),
                    lastValue);
            }

            return lastValue;
        }
        else
        {
            var ctor = Expression.New(scope.RuntimeType);
            return Expression.MemberInit(ctor, scope.Level.Peek());
        }
    }
Version

13.5.0

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in QueryableProjectionScopeExtensions.CreateMemberInit, using the stack trace and reproduction steps. Reproduce the failure with a C# record used in a projection, then inspect how expressions are constructed for types without default constructors. Done means the projection query returns its record result without the default-constructor error.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp, graphql
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.