Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,17 @@ public IObjectFieldDescriptor Field<TResolver, TPropertyType>(
{
ArgumentNullException.ThrowIfNull(propertyOrMethod);

if (propertyOrMethod.Body is UnaryExpression { NodeType: ExpressionType.ArrayLength })
{
var fieldDescriptor = ObjectFieldDescriptor.New(
Context,
propertyOrMethod,
Configuration.RuntimeType,
typeof(TResolver));
_fields.Add(fieldDescriptor);
return fieldDescriptor;
}

var member = propertyOrMethod.TryExtractMember();

if (member is PropertyInfo or MethodInfo)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,25 @@ public async Task UseMiddleware()
result.ToJson().MatchSnapshot();
}

[Fact]
public void Field_ArrayLengthExpression_Uses_ExpressionConfiguration()
{
// arrange
var descriptor = new ObjectTypeDescriptor<ArrayHolder>(Context);

// act
IObjectTypeDescriptor<ArrayHolder> desc = descriptor;
desc.BindFieldsExplicitly();
desc.Field(t => t.Buffer.Length).Name("bufferLength");

var field = descriptor.CreateConfiguration().Fields.Single(t => t.Name == "bufferLength");

// assert
Assert.Null(field.Member);
Assert.NotNull(field.Expression);
Assert.Equal(typeof(int), field.ResultType);
}

public class Foo : FooBase
{
public required string A { get; set; }
Expand All @@ -199,6 +218,11 @@ public class FooBase
public virtual required string B { get; set; }
}

public class ArrayHolder
{
public byte[] Buffer { get; set; } = [];
}

public class BarType : ObjectType
{
protected override void Configure(IObjectTypeDescriptor descriptor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,17 @@ protected override void OnCompleteFields(
/// <inheritdoc />
public IFilterFieldDescriptor Field<TField>(Expression<Func<T, TField>> propertyOrMember)
{
if (propertyOrMember.Body is UnaryExpression { NodeType: ExpressionType.ArrayLength })
{
var arrayLengthFieldDescriptor =
FilterFieldDescriptor.New(
Context,
Configuration.Scope,
propertyOrMember);
Fields.Add(arrayLengthFieldDescriptor);
return arrayLengthFieldDescriptor;
}

switch (propertyOrMember.TryExtractMember())
{
case PropertyInfo m:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using HotChocolate.Execution.Processing;
Expand All @@ -9,14 +10,17 @@ public class QueryableProjectionScalarHandler
: QueryableProjectionHandlerBase
{
public override bool CanHandle(Selection selection)
=> selection.IsLeaf && CanProjectMember(selection);
=> selection.IsLeaf
&& (CanProjectMember(selection)
|| selection.Field.ResolverExpression is LambdaExpression);

public override bool TryHandleEnter(
QueryableProjectionContext context,
Selection selection,
[NotNullWhen(true)] out ISelectionVisitorAction? action)
{
if (selection.Field.Member is PropertyInfo { CanWrite: true })
if (selection.Field.Member is PropertyInfo { CanWrite: true }
|| selection.Field.ResolverExpression is LambdaExpression)
{
action = SelectionVisitor.SkipAndLeave;
return true;
Expand All @@ -34,24 +38,142 @@ public override bool TryHandleLeave(
var field = selection.Field;

if (context.Scopes.Count > 0
&& context.Scopes.Peek() is QueryableProjectionScope closure
&& field.Member is PropertyInfo member)
&& context.Scopes.Peek() is QueryableProjectionScope closure)
{
var instance = closure.Instance.Peek();

closure.Level.Peek()
.Enqueue(
Expression.Bind(
member,
Expression.Property(instance, member)));
if (field.Member is PropertyInfo member)
{
EnqueueBinding(
closure,
member,
Expression.Property(instance, member));

action = SelectionVisitor.Continue;
return true;
action = SelectionVisitor.Continue;
return true;
}

if (field.Member is null
&& field.ResolverExpression is LambdaExpression expression
&& expression.Parameters.Count == 1
&& expression.Parameters[0].Type.IsAssignableFrom(instance.Type))
{
var properties = TopLevelPropertyExtractor.Extract(expression);

foreach (var property in properties)
{
if (!property.CanWrite
|| !(property.DeclaringType?.IsAssignableFrom(instance.Type) ?? false))
{
continue;
}

EnqueueBinding(
closure,
property,
Expression.Property(instance, property));
}

action = SelectionVisitor.Continue;
return true;
}
}

action = SelectionVisitor.Skip;
return true;
}

private static void EnqueueBinding(
QueryableProjectionScope scope,
PropertyInfo member,
Expression value)
{
if (scope.Level.Peek().Any(t => t.Member == member))
{
return;
}

scope.Level.Peek().Enqueue(Expression.Bind(member, value));
}

private sealed class TopLevelPropertyExtractor(ParameterExpression parameter) : ExpressionVisitor
{
private readonly ParameterExpression _parameter = parameter;
private readonly HashSet<PropertyInfo> _seen = [];
private readonly List<PropertyInfo> _properties = [];

public static IReadOnlyList<PropertyInfo> Extract(LambdaExpression expression)
{
var visitor = new TopLevelPropertyExtractor(expression.Parameters[0]);
visitor.Visit(expression.Body);
return visitor._properties;
}

protected override Expression VisitExtension(Expression node) => node.CanReduce ? base.VisitExtension(node) : node;

protected override Expression VisitMember(MemberExpression node)
{
if (TryGetTopLevelProperty(node, _parameter, out var property)
&& _seen.Add(property))
{
_properties.Add(property);
}

return base.VisitMember(node);
}

private static bool TryGetTopLevelProperty(
Expression expression,
ParameterExpression parameter,
[NotNullWhen(true)] out PropertyInfo? property)
{
Expression? current = expression;

while (current is not null)
{
current = UnwrapConvert(current);

if (current is not MemberExpression memberExpression)
{
break;
}

var parent = UnwrapConvert(memberExpression.Expression);

if (parent == parameter)
{
property = memberExpression.Member as PropertyInfo;
return property is not null;
}

if (parent is null)
{
break;
}

current = parent;
}

property = null;
return false;
}

private static Expression? UnwrapConvert(Expression? expression)
{
while (expression is UnaryExpression
{
NodeType:
ExpressionType.Convert
or ExpressionType.ConvertChecked
or ExpressionType.TypeAs
} unary)
{
expression = unary.Operand;
}

return expression;
}
}

public static QueryableProjectionScalarHandler Create(ProjectionProviderContext context) => new();
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@ protected override void OnCompleteFields(
/// <inheritdoc />
public ISortFieldDescriptor Field<TField>(Expression<Func<T, TField>> propertyOrMember)
{
if (propertyOrMember.Body is UnaryExpression { NodeType: ExpressionType.ArrayLength })
{
var arrayLengthFieldDescriptor =
SortFieldDescriptor.New(
Context,
Configuration.Scope,
propertyOrMember);
Fields.Add(arrayLengthFieldDescriptor);
return arrayLengthFieldDescriptor;
}

switch (propertyOrMember.TryExtractMember())
{
case PropertyInfo m:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,32 @@ public void FilterInputType_Should_InfereType_When_ItIsAInterface()
schema.MatchSnapshot();
}

[Fact]
public void FilterInputType_Field_ArrayLengthExpression_Infers_IntOperationType()
{
// arrange
var schema = SchemaBuilder.New()
.AddFiltering()
.AddQueryType(
d => d
.Name("Query")
.Field("cardReaders")
.Resolve(new List<CardReader>())
.UseFiltering<CardReaderFilterInputType>())
.Create();

// act
Assert.True(
schema.Types.TryGetType<CardReaderFilterInputType>(
"CardReaderFilterInput",
out var filterType));

// assert
Assert.NotNull(filterType);
var lengthField = Assert.IsType<FilterField>(filterType.Fields["cardReaderUidLength"]);
Assert.IsType<IntOperationFilterInputType>(lengthField.Type);
}

[Fact]
public void FilterInputType_WithGlobalObjectIdentification_AppliesGlobalIdFormatter()
{
Expand Down Expand Up @@ -566,6 +592,11 @@ public class IgnoreTest
public string Name { get; set; } = null!;
}

public class CardReader
{
public byte[] CardReaderUid { get; set; } = [];
}

public class IgnoreTestFilterInputType
: FilterInputType<IgnoreTest>
{
Expand All @@ -583,6 +614,16 @@ protected override void Configure(IFilterInputTypeDescriptor<User> descriptor)
}
}

public class CardReaderFilterInputType : FilterInputType<CardReader>
{
protected override void Configure(IFilterInputTypeDescriptor<CardReader> descriptor)
{
descriptor.BindFieldsExplicitly();
descriptor.Name("CardReaderFilterInput");
descriptor.Field(x => x.CardReaderUid.Length).Name("cardReaderUidLength");
}
}

public class UserQueryType : ObjectType<User>
{
protected override void Configure(IObjectTypeDescriptor<User> descriptor)
Expand Down
Loading
Loading