From 4e50a5f0de4b9695829137a1417872798a449519 Mon Sep 17 00:00:00 2001 From: Michael Staib Date: Sun, 8 Mar 2026 21:03:26 +0000 Subject: [PATCH] Add support for expressions on filter fields --- .../Filters/FilterInputTypeDescriptor`1.cs | 74 +++++++++---- .../test/Data.Tests/Issue6258ReproTests.cs | 101 ++++++++++++++++++ ...d_Inherited_Id_Does_Not_Match_Book_Id.snap | 5 + ...Nested_Inherited_Id_Matches_Author_Id.snap | 12 +++ 4 files changed, 169 insertions(+), 23 deletions(-) create mode 100644 src/HotChocolate/Data/test/Data.Tests/Issue6258ReproTests.cs create mode 100644 src/HotChocolate/Data/test/Data.Tests/__snapshots__/Issue6258ReproTests.Filter_For_Nested_Inherited_Id_Does_Not_Match_Book_Id.snap create mode 100644 src/HotChocolate/Data/test/Data.Tests/__snapshots__/Issue6258ReproTests.Filter_For_Nested_Inherited_Id_Matches_Author_Id.snap diff --git a/src/HotChocolate/Data/src/Data/Filters/FilterInputTypeDescriptor`1.cs b/src/HotChocolate/Data/src/Data/Filters/FilterInputTypeDescriptor`1.cs index 6309030ce69..e5575fd1bfc 100644 --- a/src/HotChocolate/Data/src/Data/Filters/FilterInputTypeDescriptor`1.cs +++ b/src/HotChocolate/Data/src/Data/Filters/FilterInputTypeDescriptor`1.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Reflection; using HotChocolate.Language; @@ -114,31 +115,33 @@ public IFilterFieldDescriptor Field(Expression> property return arrayLengthFieldDescriptor; } - switch (propertyOrMember.TryExtractMember()) + if (TryExtractDirectMember(propertyOrMember, out var member)) { - case PropertyInfo m: - var fieldDescriptor = - Fields.FirstOrDefault(t => t.Configuration.Member == m); - - if (fieldDescriptor is null) - { - fieldDescriptor = FilterFieldDescriptor.New(Context, Configuration.Scope, m); - Fields.Add(fieldDescriptor); - } - - return fieldDescriptor; - - case MethodInfo: - throw new ArgumentException( - FilterInputTypeDescriptor_Field_OnlyProperties, - nameof(propertyOrMember)); - - default: - fieldDescriptor = FilterFieldDescriptor - .New(Context, Configuration.Scope, propertyOrMember); - Fields.Add(fieldDescriptor); - return fieldDescriptor; + switch (member) + { + case PropertyInfo m: + var fieldDescriptor = + Fields.FirstOrDefault(t => t.Configuration.Member == m); + + if (fieldDescriptor is null) + { + fieldDescriptor = FilterFieldDescriptor.New(Context, Configuration.Scope, m); + Fields.Add(fieldDescriptor); + } + + return fieldDescriptor; + + case MethodInfo: + throw new ArgumentException( + FilterInputTypeDescriptor_Field_OnlyProperties, + nameof(propertyOrMember)); + } } + + var expressionFieldDescriptor = FilterFieldDescriptor + .New(Context, Configuration.Scope, expression: propertyOrMember); + Fields.Add(expressionFieldDescriptor); + return expressionFieldDescriptor; } /// @@ -212,4 +215,29 @@ public IFilterInputTypeDescriptor Ignore(Expression> propert base.Directive(name, arguments); return this; } + + private static bool TryExtractDirectMember( + Expression> propertyOrMember, + [NotNullWhen(true)] out MemberInfo? member) + { + var expression = propertyOrMember.Body; + + while (expression is UnaryExpression + { + NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked + } unaryExpression) + { + expression = unaryExpression.Operand; + } + + if (expression is MemberExpression { Expression: ParameterExpression } + || expression is MethodCallExpression { Object: ParameterExpression }) + { + member = propertyOrMember.TryExtractMember(); + return member is not null; + } + + member = null; + return false; + } } diff --git a/src/HotChocolate/Data/test/Data.Tests/Issue6258ReproTests.cs b/src/HotChocolate/Data/test/Data.Tests/Issue6258ReproTests.cs new file mode 100644 index 00000000000..a4bb4a3d16d --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Tests/Issue6258ReproTests.cs @@ -0,0 +1,101 @@ +using HotChocolate.Data.Filters; +using HotChocolate.Execution; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Data; + +public class Issue6258ReproTests +{ + [Fact] + public async Task Filter_For_Nested_Inherited_Id_Matches_Author_Id() + { + var executor = await CreateExecutorAsync(); + var result = await executor.ExecuteAsync( + """ + { + book(where: { authorId: { eq: 20 } }) { + id + author { + id + } + } + } + """); + + result.MatchSnapshot(); + } + + [Fact] + public async Task Filter_For_Nested_Inherited_Id_Does_Not_Match_Book_Id() + { + var executor = await CreateExecutorAsync(); + var result = await executor.ExecuteAsync( + """ + { + book(where: { authorId: { eq: 2 } }) { + id + } + } + """); + + result.MatchSnapshot(); + } + + public abstract class Issue6258Entity + { + public long Id { get; set; } + } + + public class Issue6258Book : Issue6258Entity + { + public string Title { get; set; } = default!; + + public Issue6258Author Author { get; set; } = default!; + } + + public class Issue6258Author : Issue6258Entity + { + public string Name { get; set; } = default!; + } + + public sealed class Issue6258BookFilterInputType : FilterInputType + { + protected override void Configure(IFilterInputTypeDescriptor descriptor) + { + descriptor.BindFieldsExplicitly(); + descriptor.Field(b => b.Author.Id).Name("authorId"); + } + } + + public class Issue6258Query + { + [UseFiltering] + public IQueryable GetBook() + => s_data.AsQueryable(); + } + + private static readonly Issue6258Book[] s_data = + [ + new() + { + Id = 1, + Title = "title 1", + Author = new Issue6258Author { Id = 10, Name = "author 1" } + }, + new() + { + Id = 2, + Title = "title 2", + Author = new Issue6258Author { Id = 20, Name = "author 2" } + } + ]; + + private static async Task CreateExecutorAsync() + { + return await new ServiceCollection() + .AddGraphQL() + .AddFiltering() + .AddQueryType() + .BuildRequestExecutorAsync(); + } +} diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/Issue6258ReproTests.Filter_For_Nested_Inherited_Id_Does_Not_Match_Book_Id.snap b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/Issue6258ReproTests.Filter_For_Nested_Inherited_Id_Does_Not_Match_Book_Id.snap new file mode 100644 index 00000000000..d55455bef82 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/Issue6258ReproTests.Filter_For_Nested_Inherited_Id_Does_Not_Match_Book_Id.snap @@ -0,0 +1,5 @@ +{ + "data": { + "book": [] + } +} diff --git a/src/HotChocolate/Data/test/Data.Tests/__snapshots__/Issue6258ReproTests.Filter_For_Nested_Inherited_Id_Matches_Author_Id.snap b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/Issue6258ReproTests.Filter_For_Nested_Inherited_Id_Matches_Author_Id.snap new file mode 100644 index 00000000000..e0a75adfb12 --- /dev/null +++ b/src/HotChocolate/Data/test/Data.Tests/__snapshots__/Issue6258ReproTests.Filter_For_Nested_Inherited_Id_Matches_Author_Id.snap @@ -0,0 +1,12 @@ +{ + "data": { + "book": [ + { + "id": 2, + "author": { + "id": 20 + } + } + ] + } +}