8000 use custom converters in binary expression · EntityGraphQL/EntityGraphQL@ad52706 · GitHub
[go: up one dir, main page]

Skip to content

Commit ad52706

Browse files
committed
use custom converters in binary expression
1 parent 94a290b commit ad52706

File tree

11 files changed

+52
-107
lines changed

11 files changed

+52
-107
lines changed

docs/docs/field-extensions/filtering.md

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -182,10 +182,14 @@ EntityGraphQL provides a flexible type converter system that enables runtime val
182182

183183
### Using Custom Types in Filters
184184

185-
To use a custom type in filter expressions, you typically need to register both:
185+
To use a custom type in filter expressions, you need to register a type converter:
186186

187-
1. **Type Converter** (`schema.AddCustomTypeConverter`) - For runtime conversion of values (query variables, `isAny` arrays, mutation arguments, etc.). These work throughout EntityGraphQL, not just in filters.
188-
2. **Literal Parser** (`UseFilterExtension.RegisterLiteralParser` or `EntityQueryCompiler.RegisterLiteralParser`) - For compile-time string literal conversion in binary comparisons within filter expressions. This is a global registration that applies to all filters.
187+
**Type Converter** (`schema.AddCustomTypeConverter`) - Enables conversion of string values to custom types. This handles:
188+
- Runtime conversion (query variables, mutation arguments)
189+
- Compile-time conversion (string literals in binary comparisons like `version >= "1.2.3"`)
190+
- Array conversions (`isAny` arrays)
191+
192+
Type converters work throughout EntityGraphQL, not just in filters.
189193

190194
### Example: Filtering by Version
191195

@@ -198,15 +202,9 @@ public class Product
198202

199203
var schema = SchemaBuilder.FromObject<ProductContext>();
200204

201-
// Add type converter for runtime conversion (variables, isAny)
205+
// Add type converter - handles both runtime and compile-time conversion
202206
schema.AddCustomTypeConverter<string, Version>((s, _) => Version.Parse(s));
203207

204-
// Register literal parser globally (applies to all filter expressions)
205-
// Can use either UseFilterExtension.RegisterLiteralParser or EntityQueryCompiler.RegisterLiteralParser
206-
UseFilterExtension.RegisterLiteralParser<Version>(
207-
strExpr => Expression.Call(typeof(Version), nameof(Version.Parse), null, strExpr)
208-
);
209-
210208
// Mark the products field as filterable
211209
schema.ReplaceField("products", ctx => ctx.Products, "List of products")
212210
.UseFilter();

src/EntityGraphQL/Compiler/EntityQuery/EntityQueryCompiler.cs

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ namespace EntityGraphQL.Compiler.EntityQuery;
1818
/// not(), !
1919
public static class EntityQueryCompiler
2020
{
21-
private static readonly Dictionary<Type, Func<Expression, Expression>> literalParsers = new();
22-
2321
public static CompiledQueryResult Compile(string query, EqlCompileContext compileContext)
2422
{
2523
return Compile(query, null, compileContext, null);
@@ -85,19 +83,7 @@ private static Expression CompileQuery(
8583
EqlCompileContext compileContext
8684
)
8785
{
88-
var expression = EntityQueryParser.Instance.Parse(query, context, schemaProvider, requestContext, methodProvider, literalParsers, compileContext);
86+
var expression = EntityQueryParser.Instance.Parse(query, context, schemaProvider, requestContext, methodProvider, compileContext);
8987
return expression;
9088
}
91-
92-
/// <summary>
93-
/// Registers a parser for binary comparisons that converts a string operand to TTarget at compile time.
94-
/// Applied when one side is string and the other is TTarget or Nullable<TTarget>. Works for string literals and string-typed variables.
95-
/// </summary>
96-
/// <typeparam name="TTarget">Target type.</typeparam>
97-
/// <param name="makeParseExpression">Factory that turns a string Expression into a TTarget Expression.</param>
98-
/// <returns>The schema provider.</returns>
99-
public static void RegisterLiteralParser<TTarget>(Func<Expression, Expression> makeParseExpression)
100-
{
101-
literalParsers[typeof(TTarget)] = makeParseExpression;
102-
}
10389
}

src/EntityGraphQL/Compiler/EntityQuery/Grammar/Binary.cs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,31 @@ public Expression Compile(Expression? context, EntityQueryParser parser, ISchema
2020
var right = Right.Compile(context, parser, schema, requestContext, methodProvider);
2121
if (left.Type != right.Type)
2222
{
23-
return ConvertLeftOrRight(Op, left, right, parser);
23+
return ConvertLeftOrRight(Op, left, right, parser, schema);
2424
}
2525
return Expression.MakeBinary(Op, left, right);
2626
}
2727

28-
private static BinaryExpression ConvertLeftOrRight(ExpressionType op, Expression left, Expression right, EntityQueryParser parser)
28+
private static BinaryExpression ConvertLeftOrRight(ExpressionType op, Expression left, Expression right, EntityQueryParser parser, ISchemaProvider? schema)
2929
{
3030
// Defer nullable promotion and numeric width alignment until after we attempt literal parsing and specific conversions
3131
if (left.Type != right.Type)
3232
{
33-
// Allow registered literal parsers to handle string → target conversions first
34-
if (right.Type == typeof(string) && left.Type != typeof(string) && parser.TryGetLiteralParser(left.Type, out var leftParser))
35-
right = leftParser(right);
36-
else if (left.Type == typeof(string) && right.Type != typeof(string) && parser.TryGetLiteralParser(right.Type, out var rightParser))
37-
left = rightParser(left);
33+
// Try to use type converters for constant string expressions first
34+
if (schema != null && right is ConstantExpression { Value: string str } && left.Type != typeof(string))
35+
{
36+
if (schema.TryConvertCustom(str, left.Type, out var converted))
37+
{
38+
right = Expression.Constant(converted, left.Type);
39+
}
40+
}
41+
else if (schema != null && left is ConstantExpression { Value: string str2 } && right.Type != typeof(string))
42+
{
43+
if (schema.TryConvertCustom(str2, right.Type, out var converted))
44+
{
45+
left = Expression.Constant(converted, right.Type);
46+
}
47+
}
3848

3949
if (left.Type.IsEnum && right.Type.IsEnum)
4050
throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, $"Cannot compare enums of different types '{left.Type.Name}' and '{right.Type.Name}'");

src/EntityGraphQL/Compiler/EntityQuery/Grammar/EntityQueryParser.cs

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ namespace EntityGraphQL.Compiler.EntityQuery.Grammar;
1010

1111
public sealed class EntityQueryParser
1212
{
13-
private Dictionary<Type, Func<Expressio 58D6 n, Expression>> literalParsers = new();
1413
public static readonly EntityQueryParser Instance;
1514
private const string MultiplyChar = "*";
1615
private const string DivideChar = "/";
@@ -257,33 +256,11 @@ public EntityQueryParseContext(string query, Expression? context, ISchemaProvide
257256
public EqlCompileContext CompileContext { get; }
258257
}
259258

260-
public Expression Parse(
261-
string query,
262-
Expression? context,
263-
ISchemaProvider? schema,
264-
QueryRequestContext requestContext,
265-
IMethodProvider methodProvider,
266-
Dictionary<Type, Func<Expression, Expression>> literalParsers,
267-
EqlCompileContext compileContext
268-
)
259+
public Expression Parse(string query, Expression? context, ISchemaProvider? schema, QueryRequestContext requestContext, IMethodProvider methodProvider, EqlCompileContext compileContext)
269260
{
270-
this.literalParsers = literalParsers;
271261
var parseContext = new EntityQueryParseContext(query, context, schema, requestContext, methodProvider, compileContext);
272262

273263
var result = grammar.Parse(parseContext) ?? throw new EntityGraphQLException(GraphQLErrorCategory.DocumentError, "Failed to parse query");
274264
return result.Compile(parseContext.Context, Instance, parseContext.Schema, parseContext.RequestContext, parseContext.MethodProvider);
275265
}
276-
277-
/// <summary>
278-
/// Gets the registered parser for a target type used by binary comparisons when the other operand is string.
279-
/// </summary>
280-
/// <param name="toType">Target type (nullable allowed).</param>
281-
/// <param name="makeParseExpression">Parser factory (string Expression → target Expression).</param>
282-
/// <returns>True if found; otherwise false.</returns>
283-
public bool TryGetLiteralParser(Type toType, out Func<Expression, Expression> makeParseExpression)
284-
{
285-
// Unwrap nullable types to find the underlying type's parser
286-
var targetType = Nullable.GetUnderlyingType(toType) ?? toType;
287-
return literalParsers.TryGetValue(targetType, out makeParseExpression!);
288-
}
289266
}

src/EntityGraphQL/Schema/FieldExtensions/Filter/UseFilterExtension.cs

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
using System;
2-
using System.Linq.Expressions;
3-
using EntityGraphQL.Compiler.EntityQuery;
4-
51
namespace EntityGraphQL.Schema.FieldExtensions;
62

73
public static class UseFilterExtension
@@ -17,18 +13,6 @@ public static IField UseFilter(this IField field)
1713
field.AddExtension(new FilterExpressionExtension());
1814
return field;
1915
}
20-
21-
/// <summary>
22-
/// Registers a parser for binary comparisons in filter expressions that converts a string operand to TTarget at compile time.
23-
/// Applied when one side is string and the other is TTarget or Nullable&lt;TTarget&gt;.
24-
/// This is a global registration that applies to all filter expressions.
25-
/// </summary>
26-
/// <typeparam name="TTarget">Target type to parse string literals into.</typeparam>
27-
/// <param name="makeParseExpression">Factory that turns a string Expression into a TTarget Expression.</param>
28-
public static void RegisterLiteralParser<TTarget>(Func<Expression, Expression> makeParseExpression)
29-
{
30-
EntityQueryCompiler.RegisterLiteralParser<TTarget>(makeParseExpression);
31-
}
3216
}
3317

3418
public class UseFilterAttribute : ExtensionAttribute

src/EntityGraphQL/Schema/ISchemaProvider.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Linq.Expressions;
43
using EntityGraphQL.Compiler.EntityQuery;
54
using EntityGraphQL.Directives;
65

src/tests/EntityGraphQL.Tests/BinaryComparisonTests.cs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4-
using System.Linq.Expressions;
54
using EntityGraphQL.Compiler;
65
using EntityGraphQL.Compiler.EntityQuery;
76
using EntityGraphQL.Schema;
@@ -147,9 +146,9 @@ public void Nullable_Int_Equals_Constant()
147146
}
148147

149148
[Fact]
150-
public void Binary_Version_All_Operators_Work_With_LiteralParser()
149+
public void Binary_Version_All_Operators_Work_With_CustomType()
151150
{
152-
var schema = MakeSchemaWithVersionLiteralParser();
151+
var schema = MakeSchemaWithVersionCustomConverter();
153152
var data = new List<WithVersion> { new(new Version(1, 2, 2), "A"), new(new Version(1, 2, 3), "B"), new(new Version(2, 0, 0), "C") };
154153

155154
// ==
@@ -184,9 +183,9 @@ public void Binary_Version_All_Operators_Work_With_LiteralParser()
184183
}
185184

186185
[Fact]
187-
public void Binary_Version_Literal_On_Left_Uses_LiteralParser()
186+
public void Binary_Version_Literal_On_Left_Uses_CustomConverter()
188187
{
189-
var schema = MakeSchemaWithVersionLiteralParser();
188+
var schema = MakeSchemaWithVersionCustomConverter();
190189
var data = new[] { new WithVersion(new Version(1, 2, 3), "B"), new WithVersion(new Version(2, 0, 0), "C") };
191190
var compiled = EntityQueryCompiler.Compile("\"1.2.3\" <= v", schema, compileContext);
192191
var res = data.Where((Func<WithVersion, bool>)compiled.LambdaExpression.Compile()).Select(d => d.Name).ToArray();
@@ -197,7 +196,7 @@ public void Binary_Version_Literal_On_Left_Uses_LiteralParser()
197196
public void Binary_NullableVersion_Handles_Nulls_Correctly()
198197
{
199198
var schema = SchemaBuilder.FromObject<WithNullableVersion>();
200-
EntityQueryCompiler.RegisterLiteralParser<Version>(strExpr => Expression.Call(typeof(Version), nameof(Version.Parse), null, strExpr));
199+
schema.AddCustomTypeConverter<string, Version>((s, _) => Version.Parse(s));
201200

202201
var compiled = EntityQueryCompiler.Compile("v >= \"1.2.3\"", schema, compileContext);
203202
var data = new[] { new WithNullableVersion(null, "N"), new WithNullableVersion(new Version(1, 2, 3), "B"), new WithNullableVersion(new Version(2, 0, 0), "C") };
@@ -208,18 +207,21 @@ public void Binary_NullableVersion_Handles_Nulls_Correctly()
208207
[Fact]
209208
public void Binary_Version_Invalid_Literal_Shows_Parser_Error()
210209
{
211-
var schema = MakeSchemaWithVersionLiteralParser();
212-
var compiled = EntityQueryCompiler.Compile("v >= \"not-a-version\"", schema, compileContext);
213-
var pred = (Func<WithVersion, bool>)compiled.LambdaExpression.Compile();
210+
var schema = MakeSchemaWithVersionCustomConverter();
214211
// Evaluate on a single row to trigger Version.Parse of the literal
215-
var ex = Assert.ThrowsAny<Exception>(() => pred(new WithVersion(new Version(0, 0), "X")));
212+
var ex = Assert.ThrowsAny<ArgumentException>(() =>
213+
{
214+
var compiled = EntityQueryCompiler.Compile("v >= \"not-a-version\"", schema, compileContext);
215+
var pred = (Func<WithVersion, bool>)compiled.LambdaExpression.Compile();
216+
pred(new WithVersion(new Version(0, 0), "X"));
217+
});
216218
Assert.Contains("Version", ex.Message, StringComparison.OrdinalIgnoreCase);
217219
}
218220

219-
private static SchemaProvider<WithVersion> MakeSchemaWithVersionLiteralParser()
221+
private static SchemaProvider<WithVersion> MakeSchemaWithVersionCustomConverter()
220222
{
221223
var schema = SchemaBuilder.FromObject<WithVersion>();
222-
EntityQueryCompiler.RegisterLiteralParser<Version>(strExpr => Expression.Call(typeof(Version), nameof(Version.Parse), null, strExpr));
224+
schema.AddCustomTypeConverter<string, Version>((s, _) => Version.Parse(s));
223225
return schema;
224226
}
225227

src/tests/EntityGraphQL.Tests/CustomConvertersWithBinaryAndIsAnyTests.cs

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,10 @@ public WithVersion(Version v, string name)
2727
}
2828

2929
[Fact]
30-
public void Binary_Version_Uses_LiteralParser_For_String_Literals()
30+
public void Binary_Version_Uses_CustomConverter_For_String_Literals()
3131
{
3232
var schema = SchemaBuilder.FromObject<WithVersion>();
33-
// Enable parsing string literals to Version inside Binary comparisons
34-
EntityQueryCompiler.RegisterLiteralParser<Version>(strExpr => Expression.Call(typeof(Version), nameof(Version.Parse), null, strExpr));
33+
schema.AddCustomTypeConverter<string, Version>((s, _) => Version.Parse(s));
3534

3635
var compiled = EntityQueryCompiler.Compile("v >= \"1.2.3\"", schema, compileContext);
3736
var data = new List<WithVersion> { new(new Version(1, 2, 2), "A"), new(new Version(1, 2, 3), "B"), new(new Version(2, 0, 0), "C") };
@@ -43,12 +42,10 @@ public void Binary_Version_Uses_LiteralParser_For_String_Literals()
4342
}
4443

4544
[Fact]
46-
public void IsAny_On_String_And_Binary_On_Version_With_LiteralParser()
45+
public void IsAny_On_String_And_Binary_On_Version_With_CustomConverter()
4746
{
4847
var schema = SchemaBuilder.FromObject<WithVersion>();
49-
// isAny will be used on Name (string) which is supported by default
50-
// Version will use literal parser for binary comparison
51-
EntityQueryCompiler.RegisterLiteralParser<Version>(strExpr => Expression.Call(typeof(Version), nameof(Version.Parse), null, strExpr));
48+
schema.AddCustomTypeConverter<string, Version>((s, _) => Version.Parse(s));
5249

5350
var compiled = EntityQueryCompiler.Compile("name.isAny([\"B\", \"C\"]) && v >= \"1.2.3\"", schema, compileContext, schema.MethodProvider);
5451
var data = new List<WithVersion> { new(new Version(1, 2, 2), "A"), new(new Version(1, 2, 3), "B"), new(new Version(2, 0, 0), "C"), new(new Version(3, 0, 0), "D") };
@@ -60,11 +57,10 @@ public void IsAny_On_String_And_Binary_On_Version_With_LiteralParser()
6057
}
6158

6259
[Fact]
63-
public void IsAny_On_String_And_Binary_On_Version_With_LiteralParser_And_ToOnly_Converter()
60+
public void IsAny_On_String_And_Binary_On_Version_With_ToOnly_Converter()
6461
{
6562
var schema = SchemaBuilder.FromObject<WithVersion>();
66-
// String isAny supported by default; add literal parser for Version and also a to-only converter
67-
EntityQueryCompiler.RegisterLiteralParser<Version>(strExpr => Expression.Call(typeof(Version), nameof(Version.Parse), null, strExpr));
63+
// String isAny supported by default; add custom type converter for Version and also a to-only converter
6864
schema.AddCustomTypeConverter<Version>(
6965
(obj, _) =>
7066
{
@@ -125,10 +121,7 @@ public void Combining_Extensions_In_Single_Query_And_Execution_Path()
125121
{
126122
// Arrange
127123
var schema = SchemaBuilder.FromObject<WithVersion>();
128-
// Register literal parser so Binary can parse string literals to Version
129-
EntityQueryCompiler.RegisterLiteralParser<Version>(strExpr => Expression.Call(typeof(Version), nameof(Version.Parse), null, strExpr));
130124
// Register custom converters for Version (from-to and to-only). This automatically enables isAny for Version
131-
schema.AddCustomTypeConverter<string, Version>((s, _) => Version.Parse(s));
132125
schema.AddCustomTypeConverter<Version>((obj, _) => obj is Version v ? v : Version.Parse(obj!.ToString()!));
133126
Assert.True(schema.MethodProvider.EntityTypeHasMethod(typeof(Version), "isAny"));
134127

src/tests/EntityGraphQL.Tests/EntityQuery/EntityQueryCompilerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ public void CompilesEnumSimple()
364364
var schema = SchemaBuilder.FromObject<TestSchema>();
365365
var param = Expression.Parameter(typeof(Person));
366366

367-
var exp = EntityQueryParser.Instance.Parse("gender == Female", param, schema, new QueryRequestContext(null, null), new EqlMethodProvider(), [], compileContext);
367+
var exp = EntityQueryParser.Instance.Parse("gender == Female", param, schema, new QueryRequestContext(null, null), new EqlMethodProvider(), compileContext);
368368

369369
var res = (bool?) 2742 Expression.Lambda(exp, param).Compile().DynamicInvoke(new Person { Gender = Gender.Female });
370370
Assert.NotNull(res);

src/tests/EntityGraphQL.Tests/LiteralParserExtensionTests.cs renamed to src/tests/EntityGraphQL.Tests/FilterExtensionBinaryCustomTypeTests.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4-
using System.Linq.Expressions;
54
using EntityGraphQL.Compiler;
65
using EntityGraphQL.Compiler.EntityQuery;
76
using EntityGraphQL.Schema;
87
using Xunit;
98

109
namespace EntityGraphQL.Tests;
1110

12-
public class LiteralParserExtensionTests
11+
public class FilterExtensionBinaryCustomTypeTests
1312
{
1413
private readonly EqlCompileContext compileContext = new(new CompileContext(new ExecutionOptions(), null, new QueryRequestContext(null, null), null, null));
1514

@@ -26,11 +25,10 @@ public WithVersion(Version v, string name)
2625
}
2726

2827
[Fact]
29-
public void Binary_Uses_Custom_Literal_Parser_For_Target_Type()
28+
public void Binary_Uses_Custom_Custom_Converter_For_Target_Type()
3029
{
3130
var schema = SchemaBuilder.FromObject<WithVersion>();
32-
// Register a literal parser for Version so string literals can be parsed at compile-time
33-
EntityQueryCompiler.RegisterLiteralParser<Version>(strExpr => Expression.Call(typeof(Version), nameof(Version.Parse), null, strExpr));
31+
schema.AddCustomTypeConverter<string, Version>((s, _) => Version.Parse(s));
3432

3533
var compiled = EntityQueryCompiler.Compile("v >= \"1.2.3\"", schema, compileContext);
3634
var data = new List<WithVersion> { new(new Version(1, 2, 2), "A"), new(new Version(1, 2, 3), "B"), new(new Version(2, 0, 0), "C") };

0 commit comments

Comments
 (0)
0