Hotchocolate Scalar For DDD Style Value Types and Filtering

Viewed 277

I’m trying to migrate a scalar field from String to an EmailAddress class The EmailAddress class has a String Property called ‘Value’.

As part of the GraphQL Schema there should be a way to filter the User by EmailAddress.

public class EmailAddress
{
    public string Value { get; }

    public EmailAddress() { }

    public EmailAddress(string value)
    {
        if (value == null)
        {
            return;
        }

        Value = value.ToLowerInvariant();
    }

    public EmailAddress New(string value) => new EmailAddress(value);

    public static readonly ValueConverter<EmailAddress, string> Converter = new
    (
        v => v.Value,
        v => new EmailAddress(v)
    );
}

filter type

export type PersonFilter = {
    and?: Array<PersonFilter> | null | undefined;
    or?: Array<PersonFilter> | null | undefined;
    email?: StringOperationFilterInput | null | undefined;
};

query

{ email: { startsWith: filter } },

Configuration Using StringType

public class PersonFilterType : FilterInputType<Person>
{
    protected override void Configure
    (
        IFilterInputTypeDescriptor<Person> descriptor
    )
    {
        descriptor.Field(f => f.Email).Type<StringOperationFilterInputType>();
    }
}
.BindRuntimeType<EmailAddress, StringType>()
.AddTypeConverter<string, EmailAddress>(x => new EmailAddress(x))
.AddTypeConverter<EmailAddress, string>(x => x.Value);

Translates to Linq Query:

where c.Email**.Value** != null

This fails as that’s not valid SQL.

Configuration Using an EmailAddressType

public class EmailAddressType : ScalarType<EmailAddress, StringValueNode>
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="UrlType"/> class.
        /// </summary>
        public EmailAddressType() : this("Email", bind: BindingBehavior.Implicit)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="UrlType"/> class.
        /// </summary>
        public EmailAddressType
        (
            NameString name,
            string? description = null,
            BindingBehavior bind = BindingBehavior.Explicit
        )
        : base(name, bind)
        {
            Description = description;
        }

        protected override bool IsInstanceOfType(StringValueNode valueSyntax)
        {
            return TryParseUri(valueSyntax.Value, out _);
        }

        protected override EmailAddress ParseLiteral(StringValueNode valueSyntax)
        {
            if (TryParseUri(valueSyntax.Value, out EmailAddress? email))
            {
                return email;
            }

            throw new SerializationException($"Scalar Cannot Parse Email {Name} {valueSyntax.GetType()} {valueSyntax}", this);
        }

        protected override StringValueNode ParseValue(EmailAddress runtimeValue)
        {
            return new(runtimeValue.Value);
        }

        public override IValueNode ParseResult(object? resultValue)
        {
            if (resultValue is null)
            {
                return NullValueNode.Default;
            }

            if (resultValue is string s)
            {
                return new StringValueNode(s);
            }

            if (resultValue is EmailAddress email)
            {
                return ParseValue(email);
            }
            
            throw new SerializationException($"Scalar Cannot Parse Email {Name} {resultValue.GetType()} {resultValue}", this);
        }

        public override bool TrySerialize(object? runtimeValue, out object? resultValue)
        {
            if (runtimeValue is null)
            {
                resultValue = null;
                return true;
            }

            if (runtimeValue is EmailAddress email)
            {
                resultValue = email.ToString();
                return true;
            }

            resultValue = null;
            return false;
        }

        public override bool TryDeserialize(object? resultValue, out object? runtimeValue)
        {
            if (resultValue is null)
            {
                runtimeValue = null;
                return true;
            }

            if (resultValue is string s && TryParseUri(s, out EmailAddress? email))
            {
                runtimeValue = email;
                return true;
            }

            if (resultValue is EmailAddress u)
            {
                runtimeValue = u;
                return true;
            }

            runtimeValue = null;
            return false;
        }

        bool TryParseUri(string value, [NotNullWhen(true)] out EmailAddress? email)
        {
            if (Validations.IsValidEmail(value) == false)
            {
                email = null;
                return false;
            }

            email = new EmailAddress(value);

            return true;
        }
    }```


```csharp
.BindRuntimeType<EmailAddress, EmailAddressType>()
.AddTypeConverter<string, EmailAddress>(x => new EmailAddress(x))
.AddTypeConverter<EmailAddress, string>(x => x.Value);

Fails at runtime

Message:Unexpected Execution Error System.ArgumentException: Method 'Boolean StartsWith(System.String)' declared on type 'System.String' cannot be called with instance of type 'EmailAddress' at System.Linq.Expressions.Expression.ValidateCallInstanceType(Type instanceType, MethodInfo method) at System.Linq.Expressions.Expression.ValidateMethodAndGetParameters(Expression instance, MethodInfo method) at System.Linq.Expressions.Expression.Call(Expression instance, MethodInfo method, Expression arg0) at System.Linq.Expressions.Expression.Call(Expression instance, MethodInfo method, IEnumerable1 arguments) at HotChocolate.Data.Filters.Expressions.FilterExpressionBuilder.StartsWith(Expression property, Object value) at HotChocolate.Data.Filters.Expressions.QueryableStringStartsWithHandler.HandleOperation(QueryableFilterContext context, IFilterOperationField field, IValueNode value, Object parsedValue) at HotChocolate.Data.Filters.Expressions.QueryableOperationHandlerBase.TryHandleOperation(QueryableFilterContext context, IFilterOperationField field, ObjectFieldNode node, Expression& result) at HotChocolate.Data.Filters.FilterOperationHandler2.TryHandleEnter(TContext context, IFilterField field, ObjectFieldNode node, ISyntaxVisitorAction& action) at HotChocolate.Data.Filters.FilterVisitor2.OnFieldEnter(TContext context, IFilterField field, ObjectFieldNode node) at HotChocolate.Data.Filters.FilterVisitorBase2.Enter(ObjectFieldNode node, TContext context)

Its worth noting that I can get it to partially work when i introduce a specific filter

public class EmailAddressOperationFilterInput : StringOperationFilterInputType
    {
        protected override void Configure(IFilterInputTypeDescriptor descriptor)
        {
            descriptor
                .Operation(DefaultFilterOperations.Equals)
                .Type<StringType>();
            descriptor
                .Operation(DefaultFilterOperations.NotEquals)
                .Type<StringType>();
        }
    }

But this limits the filter operations to eq & neq

0 Answers
Related