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