Skip to content

Commit

Permalink
Adds expression support for filtering (#4877)
Browse files Browse the repository at this point in the history
  • Loading branch information
PascalSenn committed Apr 16, 2022
1 parent 022636f commit ad668d7
Show file tree
Hide file tree
Showing 41 changed files with 673 additions and 31 deletions.
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
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
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
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
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
@@ -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; }
}
@@ -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);
}
}
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
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
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
@@ -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
@@ -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);
}
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

0 comments on commit ad668d7

Please sign in to comment.