Skip to content

Commit fc2da6f

Browse files
committed
wip
1 parent 6059a38 commit fc2da6f

16 files changed

Lines changed: 608 additions & 86 deletions

File tree

goatquery-dotnet.sln

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
Microsoft Visual Studio Solution File, Format Version 12.00
2+
# Visual Studio Version 17
3+
VisualStudioVersion = 17.5.2.0
4+
MinimumVisualStudioVersion = 10.0.40219.1
5+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "example", "example\example.csproj", "{A0FC842C-5103-E242-CA0E-06B13ABAA9ED}"
6+
EndProject
7+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
8+
EndProject
9+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GoatQuery", "GoatQuery", "{94160C38-8879-207F-820A-803BE9D227B4}"
10+
EndProject
11+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "tests", "src\GoatQuery\tests\tests.csproj", "{7BAC3D46-1702-42C0-F16F-9A1BD17B74A6}"
12+
EndProject
13+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{1FEFF0A2-436C-169C-7207-B80A25992048}"
14+
EndProject
15+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoatQuery", "src\GoatQuery\src\GoatQuery.csproj", "{6916B381-5E7F-292D-8CAF-CF88B62E3B32}"
16+
EndProject
17+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GoatQuery.AspNetCore", "GoatQuery.AspNetCore", "{A0FD25A3-A54C-9555-6DAE-55D63D9A1B08}"
18+
EndProject
19+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{9BB897A5-53BC-1E2F-A9E5-833D0C339756}"
20+
EndProject
21+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GoatQuery.AspNetCore", "src\GoatQuery.AspNetCore\src\GoatQuery.AspNetCore.csproj", "{E838C398-8747-11C6-C882-BE86419A0576}"
22+
EndProject
23+
Global
24+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
25+
Debug|Any CPU = Debug|Any CPU
26+
Release|Any CPU = Release|Any CPU
27+
EndGlobalSection
28+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
29+
{A0FC842C-5103-E242-CA0E-06B13ABAA9ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
30+
{A0FC842C-5103-E242-CA0E-06B13ABAA9ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
31+
{A0FC842C-5103-E242-CA0E-06B13ABAA9ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
32+
{A0FC842C-5103-E242-CA0E-06B13ABAA9ED}.Release|Any CPU.Build.0 = Release|Any CPU
33+
{7BAC3D46-1702-42C0-F16F-9A1BD17B74A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
34+
{7BAC3D46-1702-42C0-F16F-9A1BD17B74A6}.Debug|Any CPU.Build.0 = Debug|Any CPU
35+
{7BAC3D46-1702-42C0-F16F-9A1BD17B74A6}.Release|Any CPU.ActiveCfg = Release|Any CPU
36+
{7BAC3D46-1702-42C0-F16F-9A1BD17B74A6}.Release|Any CPU.Build.0 = Release|Any CPU
37+
{6916B381-5E7F-292D-8CAF-CF88B62E3B32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38+
{6916B381-5E7F-292D-8CAF-CF88B62E3B32}.Debug|Any CPU.Build.0 = Debug|Any CPU
39+
{6916B381-5E7F-292D-8CAF-CF88B62E3B32}.Release|Any CPU.ActiveCfg = Release|Any CPU
40+
{6916B381-5E7F-292D-8CAF-CF88B62E3B32}.Release|Any CPU.Build.0 = Release|Any CPU
41+
{E838C398-8747-11C6-C882-BE86419A0576}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
42+
{E838C398-8747-11C6-C882-BE86419A0576}.Debug|Any CPU.Build.0 = Debug|Any CPU
43+
{E838C398-8747-11C6-C882-BE86419A0576}.Release|Any CPU.ActiveCfg = Release|Any CPU
44+
{E838C398-8747-11C6-C882-BE86419A0576}.Release|Any CPU.Build.0 = Release|Any CPU
45+
EndGlobalSection
46+
GlobalSection(SolutionProperties) = preSolution
47+
HideSolutionNode = FALSE
48+
EndGlobalSection
49+
GlobalSection(NestedProjects) = preSolution
50+
{94160C38-8879-207F-820A-803BE9D227B4} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
51+
{7BAC3D46-1702-42C0-F16F-9A1BD17B74A6} = {94160C38-8879-207F-820A-803BE9D227B4}
52+
{1FEFF0A2-436C-169C-7207-B80A25992048} = {94160C38-8879-207F-820A-803BE9D227B4}
53+
{6916B381-5E7F-292D-8CAF-CF88B62E3B32} = {1FEFF0A2-436C-169C-7207-B80A25992048}
54+
{A0FD25A3-A54C-9555-6DAE-55D63D9A1B08} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
55+
{9BB897A5-53BC-1E2F-A9E5-833D0C339756} = {A0FD25A3-A54C-9555-6DAE-55D63D9A1B08}
56+
{E838C398-8747-11C6-C882-BE86419A0576} = {9BB897A5-53BC-1E2F-A9E5-833D0C339756}
57+
EndGlobalSection
58+
GlobalSection(ExtensibilityGlobals) = postSolution
59+
SolutionGuid = {4DE0B5C8-84BA-4D9E-811F-7C4F6553A67A}
60+
EndGlobalSection
61+
EndGlobal

src/GoatQuery.AspNetCore/src/Attributes/EnableQueryAttribute.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using Microsoft.AspNetCore.Mvc;
22
using Microsoft.AspNetCore.Mvc.Filters;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Options;
35

46
public sealed class EnableQueryAttribute<T> : ActionFilterAttribute
57
{
@@ -86,7 +88,25 @@ public override void OnActionExecuted(ActionExecutedContext context)
8688
searchBinder = context.HttpContext.RequestServices.GetService(typeof(ISearchBinder<T>)) as ISearchBinder<T>;
8789
}
8890

89-
var applyResult = queryable.Apply(query, searchBinder, _options);
91+
var applyOptions = _options ?? new QueryOptions();
92+
93+
// Auto-resolve JsonNamingPolicy from DI if not explicitly set
94+
if (applyOptions.PropertyNamingPolicy is null)
95+
{
96+
var jsonOptions = context.HttpContext.RequestServices.GetService<IOptions<JsonOptions>>();
97+
var namingPolicy = jsonOptions?.Value?.JsonSerializerOptions?.PropertyNamingPolicy;
98+
if (namingPolicy is not null)
99+
{
100+
applyOptions = new QueryOptions()
101+
{
102+
MaxTop = applyOptions.MaxTop,
103+
MaxPropertyMappingDepth = applyOptions.MaxPropertyMappingDepth,
104+
PropertyNamingPolicy = namingPolicy
105+
};
106+
}
107+
}
108+
109+
var applyResult = queryable.Apply(query, searchBinder, applyOptions);
90110
if (applyResult.IsFailed)
91111
{
92112
var message = string.Join(", ", applyResult.Errors.Select(x => x.Message));
Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Collections.Generic;
2+
13
public enum OrderByDirection
24
{
35
Ascending = 1,
@@ -7,10 +9,29 @@ public enum OrderByDirection
79
public sealed class OrderByStatement : Node
810
{
911
public OrderByDirection Direction { get; set; }
12+
public List<string> Segments { get; }
13+
14+
public OrderByStatement(Token token) : base(token)
15+
{
16+
Segments = new List<string> { token.Literal };
17+
}
1018

11-
public OrderByStatement(Token token) : base(token) { }
1219
public OrderByStatement(Token token, OrderByDirection direction) : base(token)
1320
{
1421
Direction = direction;
22+
Segments = new List<string> { token.Literal };
1523
}
16-
}
24+
25+
public OrderByStatement(Token token, List<string> segments, OrderByDirection direction) : base(token)
26+
{
27+
Direction = direction;
28+
Segments = segments;
29+
}
30+
31+
public bool IsNestedPath => Segments.Count > 1;
32+
33+
public override string TokenLiteral()
34+
{
35+
return string.Join("/", Segments);
36+
}
37+
}

src/GoatQuery/src/Evaluator/FilterEvaluationContext.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@ internal class FilterEvaluationContext
66
{
77
public ParameterExpression RootParameter { get; }
88
public PropertyMappingTree PropertyMappingTree { get; }
9+
public int MaxPropertyMappingDepth { get; }
910
public Stack<LambdaScope> LambdaScopes { get; } = new Stack<LambdaScope>();
1011

11-
public FilterEvaluationContext(ParameterExpression rootParameter, PropertyMappingTree propertyMappingTree)
12+
public FilterEvaluationContext(ParameterExpression rootParameter, PropertyMappingTree propertyMappingTree, int maxPropertyMappingDepth)
1213
{
1314
RootParameter = rootParameter;
1415
PropertyMappingTree = propertyMappingTree;
16+
MaxPropertyMappingDepth = maxPropertyMappingDepth;
1517
}
1618

1719
public bool IsInLambdaScope => LambdaScopes.Count > 0;

src/GoatQuery/src/Evaluator/FilterEvaluator.cs

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ private static MethodInfo GetEnumerableMethod(string methodName, int parameterCo
2424
private static MethodInfo GetStringMethod(string methodName, params Type[] parameterTypes) =>
2525
typeof(string).GetMethod(methodName, parameterTypes ?? Type.EmptyTypes);
2626

27-
public static Result<Expression> Evaluate(QueryExpression expression, ParameterExpression parameterExpression, PropertyMappingTree propertyMappingTree)
27+
public static Result<Expression> Evaluate(QueryExpression expression, ParameterExpression parameterExpression, PropertyMappingTree propertyMappingTree, int maxPropertyMappingDepth = 5)
2828
{
2929
if (expression == null) return Result.Fail("Expression cannot be null");
3030
if (parameterExpression == null) return Result.Fail("Parameter expression cannot be null");
3131
if (propertyMappingTree == null) return Result.Fail("Property mapping tree cannot be null");
3232

33-
var context = new FilterEvaluationContext(parameterExpression, propertyMappingTree);
33+
var context = new FilterEvaluationContext(parameterExpression, propertyMappingTree, maxPropertyMappingDepth);
3434
return EvaluateExpression(expression, context);
3535
}
3636

@@ -223,13 +223,13 @@ private static Result<ConstantExpression> CreateConstantExpression(QueryExpressi
223223
return literal switch
224224
{
225225
IntegerLiteral intLit => CreateIntegerOrEnumConstant(intLit.Value, expression.Type),
226-
DateLiteral dateLit => Result.Ok(CreateDateConstant(dateLit, expression)),
226+
DateLiteral dateLit => Result.Ok(CreateDateConstant(dateLit, expression.Type)),
227227
GuidLiteral guidLit => Result.Ok(Expression.Constant(guidLit.Value, expression.Type)),
228228
DecimalLiteral decLit => Result.Ok(Expression.Constant(decLit.Value, expression.Type)),
229229
FloatLiteral floatLit => Result.Ok(Expression.Constant(floatLit.Value, expression.Type)),
230230
DoubleLiteral dblLit => Result.Ok(Expression.Constant(dblLit.Value, expression.Type)),
231231
StringLiteral strLit => CreateStringOrEnumConstant(strLit.Value, expression.Type),
232-
DateTimeLiteral dtLit => Result.Ok(Expression.Constant(dtLit.Value, expression.Type)),
232+
DateTimeLiteral dtLit => Result.Ok(CreateDateTimeConstant(dtLit, expression.Type)),
233233
BooleanLiteral boolLit => Result.Ok(Expression.Constant(boolLit.Value, expression.Type)),
234234
NullLiteral _ => Result.Ok(Expression.Constant(null, expression.Type)),
235235
_ => Result.Fail($"Unsupported literal type: {literal.GetType().Name}")
@@ -244,35 +244,66 @@ private static Result<ConstantExpression> CreateConstantExpression(QueryExpressi
244244
return Result.Ok((constantResult.Value, property));
245245
}
246246

247-
private static ConstantExpression CreateDateConstant(DateLiteral dateLiteral, Expression expression)
247+
private static ConstantExpression CreateDateConstant(DateLiteral dateLiteral, Type targetType)
248248
{
249-
if (expression.Type == typeof(DateTime?))
249+
var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType;
250+
251+
if (underlyingType == typeof(DateTimeOffset))
250252
{
251-
return Expression.Constant(dateLiteral.Value.Date, typeof(DateTime));
253+
var value = new DateTimeOffset(dateLiteral.Value.Date, TimeSpan.Zero);
254+
return Expression.Constant(value, targetType);
252255
}
253-
else
256+
257+
return Expression.Constant(dateLiteral.Value.Date, targetType);
258+
}
259+
260+
private static ConstantExpression CreateDateTimeConstant(DateTimeLiteral dtLiteral, Type targetType)
261+
{
262+
var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType;
263+
264+
if (underlyingType == typeof(DateTimeOffset))
254265
{
255-
return Expression.Constant(dateLiteral.Value.Date, expression.Type);
266+
if (DateTimeOffset.TryParse(dtLiteral.TokenLiteral(), System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out var dto))
267+
{
268+
return Expression.Constant(dto, targetType);
269+
}
270+
return Expression.Constant(new DateTimeOffset(dtLiteral.Value), targetType);
256271
}
272+
273+
return Expression.Constant(dtLiteral.Value, targetType);
257274
}
258275

259276
private static Expression CreateContainsExpression(Expression expression, ConstantExpression value)
260277
{
261278
var expressionToLower = Expression.Call(expression, StringToLowerMethod);
262279
var valueToLower = Expression.Call(value, StringToLowerMethod);
263-
return Expression.Call(expressionToLower, StringContainsMethod, valueToLower);
280+
var containsCall = Expression.Call(expressionToLower, StringContainsMethod, valueToLower);
281+
282+
// Guard against null: (expression != null && expression.ToLower().Contains(value.ToLower()))
283+
var nullCheck = Expression.NotEqual(expression, Expression.Constant(null, typeof(string)));
284+
return Expression.AndAlso(nullCheck, containsCall);
264285
}
265286

266287
private static Expression CreateEqualityExpression(Expression expression, ConstantExpression value, bool isEqual)
267288
{
268-
// For string comparisons, make them case-insensitive
289+
// For string comparisons, make them case-insensitive with null safety
269290
if (expression.Type == typeof(string))
270291
{
271292
var expressionToLower = Expression.Call(expression, StringToLowerMethod);
272293
var valueToLower = Expression.Call(value, StringToLowerMethod);
273-
return isEqual
274-
? Expression.Equal(expressionToLower, valueToLower)
275-
: Expression.NotEqual(expressionToLower, valueToLower);
294+
var nullCheck = Expression.NotEqual(expression, Expression.Constant(null, typeof(string)));
295+
296+
if (isEqual)
297+
{
298+
// eq: (expression != null && expression.ToLower() == value.ToLower())
299+
return Expression.AndAlso(nullCheck, Expression.Equal(expressionToLower, valueToLower));
300+
}
301+
else
302+
{
303+
// ne: (expression == null || expression.ToLower() != value.ToLower())
304+
var isNull = Expression.Equal(expression, Expression.Constant(null, typeof(string)));
305+
return Expression.OrElse(isNull, Expression.NotEqual(expressionToLower, valueToLower));
306+
}
276307
}
277308

278309
// For non-string types, use standard equality
@@ -430,7 +461,7 @@ private static Result<Expression> EvaluateLambdaBodyPropertyPath(InfixExpression
430461
propertyPath.Segments[0].Equals(context.CurrentLambda.ParameterName, StringComparison.OrdinalIgnoreCase);
431462

432463
return isLambdaParameterPath
433-
? EvaluateLambdaPropertyPath(exp, propertyPath, context.CurrentLambda.Parameter)
464+
? EvaluateLambdaPropertyPath(exp, propertyPath, context.CurrentLambda.Parameter, context.MaxPropertyMappingDepth)
434465
: EvaluatePropertyPathExpression(exp, propertyPath, context);
435466
}
436467

@@ -474,14 +505,14 @@ private static Result<Expression> EvaluateLambdaBodyLogicalOperator(InfixExpress
474505
};
475506
}
476507

477-
private static Result<Expression> EvaluateLambdaPropertyPath(InfixExpression exp, PropertyPath propertyPath, ParameterExpression lambdaParameter)
508+
private static Result<Expression> EvaluateLambdaPropertyPath(InfixExpression exp, PropertyPath propertyPath, ParameterExpression lambdaParameter, int maxPropertyMappingDepth)
478509
{
479510
// Skip the first segment (lambda parameter name) and build property path from lambda parameter
480511
var current = (Expression)lambdaParameter;
481512
var elementType = lambdaParameter.Type;
482513

483514
// Build property path from lambda parameter
484-
var pathResult = BuildLambdaPropertyPath(current, propertyPath.Segments.Skip(1).ToList(), elementType);
515+
var pathResult = BuildLambdaPropertyPath(current, propertyPath.Segments.Skip(1).ToList(), elementType, maxPropertyMappingDepth);
485516
if (pathResult.IsFailed) return pathResult;
486517

487518
current = pathResult.Value;
@@ -517,10 +548,10 @@ private static Expression CreateAllExpression(MemberExpression collection, Lambd
517548
return Expression.AndAlso(hasElements, allMatch);
518549
}
519550

520-
private static Result<Expression> BuildLambdaPropertyPath(Expression startExpression, List<string> segments, Type elementType)
551+
private static Result<Expression> BuildLambdaPropertyPath(Expression startExpression, List<string> segments, Type elementType, int maxPropertyMappingDepth)
521552
{
522553
var current = startExpression;
523-
var currentMappingTree = PropertyMappingTreeBuilder.BuildMappingTree(elementType, GetDefaultMaxDepth());
554+
var currentMappingTree = PropertyMappingTreeBuilder.BuildMappingTree(elementType, maxPropertyMappingDepth);
524555

525556
foreach (var segment in segments)
526557
{
@@ -541,10 +572,6 @@ private static Result<Expression> BuildLambdaPropertyPath(Expression startExpres
541572
return Result.Ok(current);
542573
}
543574

544-
private static int GetDefaultMaxDepth()
545-
{
546-
return new QueryOptions().MaxPropertyMappingDepth;
547-
}
548575

549576
private static Type GetCollectionElementType(Type collectionType)
550577
{

0 commit comments

Comments
 (0)