Skip to content

Commit aa50347

Browse files
authored
Add support for expressions on filter fields (#9330)
1 parent 2b13a2b commit aa50347

4 files changed

Lines changed: 169 additions & 23 deletions

File tree

src/HotChocolate/Data/src/Data/Filters/FilterInputTypeDescriptor`1.cs

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Diagnostics.CodeAnalysis;
12
using System.Linq.Expressions;
23
using System.Reflection;
34
using HotChocolate.Language;
@@ -114,31 +115,33 @@ public IFilterFieldDescriptor Field<TField>(Expression<Func<T, TField>> property
114115
return arrayLengthFieldDescriptor;
115116
}
116117

117-
switch (propertyOrMember.TryExtractMember())
118+
if (TryExtractDirectMember(propertyOrMember, out var member))
118119
{
119-
case PropertyInfo m:
120-
var fieldDescriptor =
121-
Fields.FirstOrDefault(t => t.Configuration.Member == m);
122-
123-
if (fieldDescriptor is null)
124-
{
125-
fieldDescriptor = FilterFieldDescriptor.New(Context, Configuration.Scope, m);
126-
Fields.Add(fieldDescriptor);
127-
}
128-
129-
return fieldDescriptor;
130-
131-
case MethodInfo:
132-
throw new ArgumentException(
133-
FilterInputTypeDescriptor_Field_OnlyProperties,
134-
nameof(propertyOrMember));
135-
136-
default:
137-
fieldDescriptor = FilterFieldDescriptor
138-
.New(Context, Configuration.Scope, propertyOrMember);
139-
Fields.Add(fieldDescriptor);
140-
return fieldDescriptor;
120+
switch (member)
121+
{
122+
case PropertyInfo m:
123+
var fieldDescriptor =
124+
Fields.FirstOrDefault(t => t.Configuration.Member == m);
125+
126+
if (fieldDescriptor is null)
127+
{
128+
fieldDescriptor = FilterFieldDescriptor.New(Context, Configuration.Scope, m);
129+
Fields.Add(fieldDescriptor);
130+
}
131+
132+
return fieldDescriptor;
133+
134+
case MethodInfo:
135+
throw new ArgumentException(
136+
FilterInputTypeDescriptor_Field_OnlyProperties,
137+
nameof(propertyOrMember));
138+
}
141139
}
140+
141+
var expressionFieldDescriptor = FilterFieldDescriptor
142+
.New(Context, Configuration.Scope, expression: propertyOrMember);
143+
Fields.Add(expressionFieldDescriptor);
144+
return expressionFieldDescriptor;
142145
}
143146

144147
/// <inheritdoc />
@@ -212,4 +215,29 @@ public IFilterInputTypeDescriptor<T> Ignore(Expression<Func<T, object?>> propert
212215
base.Directive(name, arguments);
213216
return this;
214217
}
218+
219+
private static bool TryExtractDirectMember<TField>(
220+
Expression<Func<T, TField>> propertyOrMember,
221+
[NotNullWhen(true)] out MemberInfo? member)
222+
{
223+
var expression = propertyOrMember.Body;
224+
225+
while (expression is UnaryExpression
226+
{
227+
NodeType: ExpressionType.Convert or ExpressionType.ConvertChecked
228+
} unaryExpression)
229+
{
230+
expression = unaryExpression.Operand;
231+
}
232+
233+
if (expression is MemberExpression { Expression: ParameterExpression }
234+
|| expression is MethodCallExpression { Object: ParameterExpression })
235+
{
236+
member = propertyOrMember.TryExtractMember();
237+
return member is not null;
238+
}
239+
240+
member = null;
241+
return false;
242+
}
215243
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
using HotChocolate.Data.Filters;
2+
using HotChocolate.Execution;
3+
using Microsoft.Extensions.DependencyInjection;
4+
5+
namespace HotChocolate.Data;
6+
7+
public class Issue6258ReproTests
8+
{
9+
[Fact]
10+
public async Task Filter_For_Nested_Inherited_Id_Matches_Author_Id()
11+
{
12+
var executor = await CreateExecutorAsync();
13+
var result = await executor.ExecuteAsync(
14+
"""
15+
{
16+
book(where: { authorId: { eq: 20 } }) {
17+
id
18+
author {
19+
id
20+
}
21+
}
22+
}
23+
""");
24+
25+
result.MatchSnapshot();
26+
}
27+
28+
[Fact]
29+
public async Task Filter_For_Nested_Inherited_Id_Does_Not_Match_Book_Id()
30+
{
31+
var executor = await CreateExecutorAsync();
32+
var result = await executor.ExecuteAsync(
33+
"""
34+
{
35+
book(where: { authorId: { eq: 2 } }) {
36+
id
37+
}
38+
}
39+
""");
40+
41+
result.MatchSnapshot();
42+
}
43+
44+
public abstract class Issue6258Entity
45+
{
46+
public long Id { get; set; }
47+
}
48+
49+
public class Issue6258Book : Issue6258Entity
50+
{
51+
public string Title { get; set; } = default!;
52+
53+
public Issue6258Author Author { get; set; } = default!;
54+
}
55+
56+
public class Issue6258Author : Issue6258Entity
57+
{
58+
public string Name { get; set; } = default!;
59+
}
60+
61+
public sealed class Issue6258BookFilterInputType : FilterInputType<Issue6258Book>
62+
{
63+
protected override void Configure(IFilterInputTypeDescriptor<Issue6258Book> descriptor)
64+
{
65+
descriptor.BindFieldsExplicitly();
66+
descriptor.Field(b => b.Author.Id).Name("authorId");
67+
}
68+
}
69+
70+
public class Issue6258Query
71+
{
72+
[UseFiltering<Issue6258BookFilterInputType>]
73+
public IQueryable<Issue6258Book> GetBook()
74+
=> s_data.AsQueryable();
75+
}
76+
77+
private static readonly Issue6258Book[] s_data =
78+
[
79+
new()
80+
{
81+
Id = 1,
82+
Title = "title 1",
83+
Author = new Issue6258Author { Id = 10, Name = "author 1" }
84+
},
85+
new()
86+
{
87+
Id = 2,
88+
Title = "title 2",
89+
Author = new Issue6258Author { Id = 20, Name = "author 2" }
90+
}
91+
];
92+
93+
private static async Task<IRequestExecutor> CreateExecutorAsync()
94+
{
95+
return await new ServiceCollection()
96+
.AddGraphQL()
97+
.AddFiltering()
98+
.AddQueryType<Issue6258Query>()
99+
.BuildRequestExecutorAsync();
100+
}
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"data": {
3+
"book": []
4+
}
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"data": {
3+
"book": [
4+
{
5+
"id": 2,
6+
"author": {
7+
"id": 20
8+
}
9+
}
10+
]
11+
}
12+
}

0 commit comments

Comments
 (0)