Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds expression support for filtering #4877

Merged
merged 10 commits into from
Apr 16, 2022
12 changes: 12 additions & 0 deletions src/HotChocolate/Data/src/Data/DataResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/HotChocolate/Data/src/Data/DataResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,12 @@
<data name="QueryableFiltering_MemberInvalid" xml:space="preserve">
<value>Filtering needs a member of type PropertyInfo or MethodInfo but received {0}! This error occured on {1}.{2}: {3}</value>
</data>
<data name="QueryableFiltering_ExpressionParameterInvalid" xml:space="preserve">
<value>Filtering needs a expression with exactly one parameter of type {0}! This error occured on {1}.{2}: {3}</value>
</data>
<data name="QueryableFilterProvider_ExpressionParameterInvalid" xml:space="preserve">
<value>Filtering needs a expression with exactly one parameter of type {0}! Check the expression on field {1}.</value>
</data>
<data name="QueryableFiltering_NoMemberDeclared" xml:space="preserve">
<value>Filtering needs a member of type PropertyInfo or MethodInfo no member was found! This error occured on {0}.{1}: {2}</value>
</data>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,12 @@ public bool IsOrAllowed()
return false;
}

public IFilterMetadata? CreateMetaData(
ITypeCompletionContext context,
IFilterInputTypeDefinition typeDefinition,
IFilterFieldDefinition fieldDefinition)
=> _provider.CreateMetaData(context, typeDefinition, fieldDefinition);

private bool TryCreateFilterType(
IExtendedType runtimeType,
[NotNullWhen(true)] out Type? type)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,12 @@ TypeSystemObjectBase Factory(IDescriptorContext _)
out IFilterFieldHandler? handler))
{
filterFieldDefinition.Handler = handler;
}
else
}

filterFieldDefinition.Metadata =
convention.CreateMetaData(completionContext, def, filterFieldDefinition);

if (filterFieldDefinition.Handler is null)
{
throw ThrowHelper
.FilterInterceptor_NoHandlerFoundForField(def, filterFieldDefinition);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,14 @@ public interface IFilterConvention : IConvention
IFilterFieldDefinition fieldDefinition,
[NotNullWhen(true)] out IFilterFieldHandler? handler);

/// <summary>
/// Creates metadata for a field that the provider can pick up an use for the translation
/// </summary>
IFilterMetadata? CreateMetaData(
ITypeCompletionContext context,
IFilterInputTypeDefinition typeDefinition,
IFilterFieldDefinition fieldDefinition);

/// <summary>
/// Creates a middleware that represents the filter execution logic
/// for the specified entity type.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Linq.Expressions;

namespace HotChocolate.Data.Filters.Expressions;

/// <summary>
/// Defines meta that for a filter field that the provider can use to build the database query
/// </summary>
public class ExpressionFilterMetadata : IFilterMetadata
{
public ExpressionFilterMetadata(Expression? expression)
{
Expression = expression;
}

public Expression? Expression { get; }
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using HotChocolate.Configuration;
Expand Down Expand Up @@ -28,7 +29,7 @@ public class QueryableDefaultFieldHandler
IFilterInputTypeDefinition typeDefinition,
IFilterFieldDefinition fieldDefinition) =>
fieldDefinition is not FilterOperationFieldDefinition &&
fieldDefinition.Member is not null;
(fieldDefinition.Member is not null || fieldDefinition.Expression is not null);

public override bool TryHandleEnter(
QueryableFilterContext context,
Expand All @@ -51,20 +52,38 @@ public class QueryableDefaultFieldHandler
return false;
}

Expression nestedProperty = field.Member switch
Expression nestedProperty;
if (field.Metadata is ExpressionFilterMetadata { Expression: LambdaExpression expression })
{
PropertyInfo propertyInfo =>
Expression.Property(context.GetInstance(), propertyInfo),
if (expression.Parameters.Count != 1 ||
expression.Parameters[0].Type != context.RuntimeTypes.Peek()!.Source)
{
throw ThrowHelper.QueryableFiltering_ExpressionParameterInvalid(
field.RuntimeType.Source,
field);
}

MethodInfo methodInfo =>
Expression.Call(context.GetInstance(), methodInfo),
nestedProperty = ReplaceVariableExpressionVisitor
.ReplaceParameter(expression, expression.Parameters[0], context.GetInstance())
.Body;
}
else
{
nestedProperty = field.Member switch
{
PropertyInfo propertyInfo =>
Expression.Property(context.GetInstance(), propertyInfo),

null =>
throw ThrowHelper.QueryableFiltering_NoMemberDeclared(field),
MethodInfo methodInfo =>
Expression.Call(context.GetInstance(), methodInfo),

_ =>
throw ThrowHelper.QueryableFiltering_MemberInvalid(field.Member, field)
};
null =>
throw ThrowHelper.QueryableFiltering_NoMemberDeclared(field),

_ =>
throw ThrowHelper.QueryableFiltering_MemberInvalid(field.Member, field)
};
}

context.PushInstance(nestedProperty);
context.RuntimeTypes.Push(field.RuntimeType);
Expand Down Expand Up @@ -101,4 +120,34 @@ public class QueryableDefaultFieldHandler
action = SyntaxVisitor.Continue;
return true;
}

private sealed class ReplaceVariableExpressionVisitor : ExpressionVisitor
{
private readonly Expression _replacement;
private readonly ParameterExpression _parameter;

public ReplaceVariableExpressionVisitor(
Expression replacement,
ParameterExpression parameter)
{
_replacement = replacement;
_parameter = parameter;
}

protected override Expression VisitParameter(ParameterExpression node)
{
if (node == _parameter)
{
return _replacement;
}
return base.VisitParameter(node);
}

public static LambdaExpression ReplaceParameter(
LambdaExpression lambda,
ParameterExpression parameter,
Expression replacement)
=> (LambdaExpression)
new ReplaceVariableExpressionVisitor(replacement, parameter).Visit(lambda);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using HotChocolate.Configuration;
using HotChocolate.Language;
using HotChocolate.Resolvers;
using HotChocolate.Types;
Expand Down Expand Up @@ -150,4 +151,27 @@ private static ApplyFiltering CreateApplicator<TEntityType>(NameString argumentN
contextData[ContextVisitFilterArgumentKey] = argumentKey;
contextData[ContextArgumentNameKey] = argumentName;
}

public override IFilterMetadata? CreateMetaData(
ITypeCompletionContext context,
IFilterInputTypeDefinition typeDefinition,
IFilterFieldDefinition fieldDefinition)
{
if (fieldDefinition.Expression is not null)
{
if (fieldDefinition.Expression is not LambdaExpression lambda ||
lambda.Parameters.Count != 1 ||
lambda.Parameters[0].Type != typeDefinition.EntityType)
{
throw ThrowHelper.QueryableFilterProvider_ExpressionParameterInvalid(
context.Type,
typeDefinition,
fieldDefinition);
}

return new ExpressionFilterMetadata(fieldDefinition.Expression);
}

return null;
}
}
7 changes: 7 additions & 0 deletions src/HotChocolate/Data/src/Data/Filters/Fields/FilterField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal FilterField(FilterFieldDefinition definition, int index)
{
Member = definition.Member;
Handler = definition.Handler!;
Metadata = definition.Metadata;
}

public new FilterInputType DeclaringType => (FilterInputType)base.DeclaringType;
Expand All @@ -32,6 +33,8 @@ internal FilterField(FilterFieldDefinition definition, int index)

public IFilterFieldHandler Handler { get; }

public IFilterMetadata? Metadata { get; }

protected override void OnCompleteField(
ITypeCompletionContext context,
ITypeSystemMember declaringMember,
Expand All @@ -43,5 +46,9 @@ internal FilterField(FilterFieldDefinition definition, int index)
{
RuntimeType = context.TypeInspector.GetReturnType(Member, ignoreAttributes: true);
}
else if (base.RuntimeType is { } runtimeType)
{
RuntimeType = context.TypeInspector.GetType(runtimeType);
}
}
}
2 changes: 2 additions & 0 deletions src/HotChocolate/Data/src/Data/Filters/Fields/IFilterField.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ public interface IFilterField : IInputField
new IExtendedType? RuntimeType { get; }

IFilterFieldHandler Handler { get; }

IFilterMetadata? Metadata { get; }
}
12 changes: 9 additions & 3 deletions src/HotChocolate/Data/src/Data/Filters/FilterFieldDefinition.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Linq.Expressions;
using System;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Reflection;
using HotChocolate.Internal;
using HotChocolate.Types;
using HotChocolate.Types.Descriptors;
using HotChocolate.Types.Descriptors.Definitions;
Expand All @@ -18,7 +20,11 @@ public class FilterFieldDefinition

public IFilterFieldHandler? Handler { get; set; }

public string? Scope { get; set; }
public Expression? Expression { get; set; }

internal IFilterMetadata? Metadata { get; set; }

public string? Scope { get; set; }

public List<int> AllowedOperations => _allowedOperations ??= new List<int>();

Expand Down Expand Up @@ -65,5 +71,5 @@ internal void MergeInto(FilterFieldDefinition target)
{
target.CreateFieldTypeDefinition = CreateFieldTypeDefinition;
}
}
}
}
24 changes: 24 additions & 0 deletions src/HotChocolate/Data/src/Data/Filters/FilterFieldDescriptor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Linq.Expressions;
using System.Reflection;
using HotChocolate.Language;
using HotChocolate.Types;
Expand Down Expand Up @@ -38,6 +39,23 @@ public class FilterFieldDescriptor
Definition.Scope = scope;
}

protected FilterFieldDescriptor(
IDescriptorContext context,
string? scope,
Expression expression)
: base(context)
{
IFilterConvention convention = context.GetFilterConvention(scope);

Definition.Expression = expression;
Definition.Scope = scope;
if (Definition.Expression is LambdaExpression lambda)
{
Definition.Type = convention.GetFieldType(lambda.ReturnType);
Definition.RuntimeType = lambda.ReturnType;
}
}

protected internal FilterFieldDescriptor(
IDescriptorContext context,
string? scope)
Expand Down Expand Up @@ -171,4 +189,10 @@ public new IFilterFieldDescriptor Directive<TDirective>()
NameString fieldName,
string? scope) =>
new FilterFieldDescriptor(context, scope, fieldName);

internal static FilterFieldDescriptor New(
IDescriptorContext context,
string? scope,
Expression expression) =>
new FilterFieldDescriptor(context, scope, expression);
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,23 +99,31 @@ public new IFilterInputTypeDescriptor<T> BindFieldsImplicitly()
/// <inheritdoc />
public IFilterFieldDescriptor Field<TField>(Expression<Func<T, TField>> propertyOrMember)
{
if (propertyOrMember.ExtractMember() is PropertyInfo m)
switch (propertyOrMember.TryExtractMember())
{
FilterFieldDescriptor? fieldDescriptor =
Fields.FirstOrDefault(t => t.Definition.Member == m);

if (fieldDescriptor is null)
{
fieldDescriptor = FilterFieldDescriptor.New(Context, Definition.Scope, m);
case PropertyInfo m:
FilterFieldDescriptor? fieldDescriptor =
Fields.FirstOrDefault(t => t.Definition.Member == m);

if (fieldDescriptor is null)
{
fieldDescriptor = FilterFieldDescriptor.New(Context, Definition.Scope, m);
Fields.Add(fieldDescriptor);
}

return fieldDescriptor;

case MethodInfo m:
throw new ArgumentException(
FilterInputTypeDescriptor_Field_OnlyProperties,
nameof(propertyOrMember));

default:
fieldDescriptor = FilterFieldDescriptor
.New(Context, Definition.Scope, propertyOrMember);
Fields.Add(fieldDescriptor);
}

return fieldDescriptor;
return fieldDescriptor;
}

throw new ArgumentException(
FilterInputTypeDescriptor_Field_OnlyProperties,
nameof(propertyOrMember));
}

/// <inheritdoc />
Expand Down