Skip to content

Commit a87e114

Browse files
committed
refactored property mapping
1 parent 905eecb commit a87e114

File tree

11 files changed

+259
-113
lines changed

11 files changed

+259
-113
lines changed

example/Controllers/UserController.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public ActionResult<IEnumerable<User>> Get()
1919
{
2020
var users = _db.Users
2121
.Include(x => x.Company)
22+
.Include(x => x.Addresses)
23+
.ThenInclude(x => x.City)
2224
.Include(x => x.Manager)
2325
.ThenInclude(x => x.Manager)
2426
.Where(x => !x.IsDeleted);

example/Entities/User.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ public record User
1717
[Column(TypeName = "timestamp without time zone")]
1818
public DateTime DateOfBirthTz { get; set; }
1919
public User? Manager { get; set; }
20-
public IEnumerable<Address> Addresses { get; set; } = Array.Empty<Address>();
21-
public IEnumerable<string> Tags { get; set; } = Array.Empty<string>();
20+
public ICollection<Address> Addresses { get; set; } = new List<Address>();
21+
public ICollection<string> Tags { get; set; } = new List<string>();
2222
public Company? Company { get; set; }
2323
}
2424

example/Program.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using System.Reflection;
21
using Bogus;
3-
using Microsoft.AspNetCore.Mvc;
42
using Microsoft.EntityFrameworkCore;
53
using Testcontainers.PostgreSql;
64

@@ -77,8 +75,11 @@
7775
{
7876
var result = db.Users
7977
.Include(x => x.Company)
80-
.Include(x => x.Manager)
81-
.ThenInclude(x => x.Manager)
78+
.Include(x => x.Addresses)
79+
.ThenInclude(x => x.City)
80+
.Include(x => x.Manager)
81+
.ThenInclude(x => x.Manager)
82+
.Where(x => !x.IsDeleted)
8283
.Apply(query);
8384

8485
if (result.IsFailed)

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ public sealed class EnableQueryAttribute<T> : ActionFilterAttribute
55
{
66
private readonly QueryOptions? _options;
77

8-
public EnableQueryAttribute(int maxTop)
8+
public EnableQueryAttribute(int maxTop, int maxPropertyMappingDepth = 5)
99
{
1010
var options = new QueryOptions()
1111
{
12-
MaxTop = maxTop
12+
MaxTop = maxTop,
13+
MaxPropertyMappingDepth = maxPropertyMappingDepth
1314
};
1415

1516
_options = options;

src/GoatQuery/src/Evaluator/FilterEvaluationContext.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
internal class FilterEvaluationContext
66
{
77
public ParameterExpression RootParameter { get; }
8-
public Dictionary<string, string> PropertyMapping { get; }
8+
public PropertyMappingTree PropertyMappingTree { get; }
99
public Stack<LambdaScope> LambdaScopes { get; } = new Stack<LambdaScope>();
1010

11-
public FilterEvaluationContext(ParameterExpression rootParameter, Dictionary<string, string> propertyMapping)
11+
public FilterEvaluationContext(ParameterExpression rootParameter, PropertyMappingTree propertyMappingTree)
1212
{
1313
RootParameter = rootParameter;
14-
PropertyMapping = propertyMapping;
14+
PropertyMappingTree = propertyMappingTree;
1515
}
1616

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

src/GoatQuery/src/Evaluator/FilterEvaluator.cs

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

26-
public static Result<Expression> Evaluate(QueryExpression expression, ParameterExpression parameterExpression, Dictionary<string, string> propertyMapping)
26+
public static Result<Expression> Evaluate(QueryExpression expression, ParameterExpression parameterExpression, PropertyMappingTree propertyMappingTree)
2727
{
2828
if (expression == null) return Result.Fail("Expression cannot be null");
2929
if (parameterExpression == null) return Result.Fail("Parameter expression cannot be null");
30-
if (propertyMapping == null) return Result.Fail("Property mapping cannot be null");
30+
if (propertyMappingTree == null) return Result.Fail("Property mapping tree cannot be null");
3131

32-
var context = new FilterEvaluationContext(parameterExpression, propertyMapping);
32+
var context = new FilterEvaluationContext(parameterExpression, propertyMappingTree);
3333
return EvaluateExpression(expression, context);
3434
}
3535

@@ -52,7 +52,7 @@ private static Result<Expression> EvaluatePropertyPathExpression(
5252
(Expression)context.CurrentLambda.Parameter :
5353
context.RootParameter;
5454

55-
var propertyPathResult = BuildPropertyPath(propertyPath, baseExpression, context.PropertyMapping);
55+
var propertyPathResult = BuildPropertyPath(propertyPath, baseExpression, context.PropertyMappingTree);
5656
if (propertyPathResult.IsFailed) return Result.Fail(propertyPathResult.Errors);
5757

5858
var (finalProperty, nullChecks) = propertyPathResult.Value;
@@ -72,67 +72,62 @@ private static Result<Expression> EvaluatePropertyPathExpression(
7272
private static Result<(MemberExpression Property, List<Expression> NullChecks)> BuildPropertyPath(
7373
PropertyPath propertyPath,
7474
Expression startExpression,
75-
Dictionary<string, string> propertyMapping)
75+
PropertyMappingTree propertyMappingTree)
7676
{
7777
var current = startExpression;
7878
var nullChecks = new List<Expression>();
79-
var currentPropertyMapping = propertyMapping;
79+
var currentMappingTree = propertyMappingTree;
8080

8181
foreach (var (segment, isLast) in propertyPath.Segments.Select((s, i) => (s, i == propertyPath.Segments.Count - 1)))
8282
{
83-
var propertyResult = ResolvePropertySegment(segment, current, currentPropertyMapping);
84-
if (propertyResult.IsFailed) return Result.Fail(propertyResult.Errors);
83+
if (!currentMappingTree.TryGetProperty(segment, out var propertyNode))
84+
return Result.Fail($"Invalid property '{segment}' in path");
8585

86-
current = propertyResult.Value;
86+
current = Expression.Property(current, propertyNode.ActualPropertyName);
8787

8888
// Add null check for intermediate reference types only (not the final property)
8989
if (!isLast && IsNullableReferenceType(current.Type))
9090
{
9191
nullChecks.Add(Expression.NotEqual(current, Expression.Constant(null, current.Type)));
9292
}
9393

94-
// Update property mapping for nested object navigation
94+
// Navigate to nested mapping for next segment
9595
if (!isLast)
9696
{
97-
currentPropertyMapping = PropertyMappingHelper.CreatePropertyMapping(current.Type);
97+
if (!propertyNode.HasNestedMapping)
98+
return Result.Fail($"Property '{segment}' does not support nested navigation");
99+
100+
currentMappingTree = propertyNode.NestedMapping;
98101
}
99102
}
100103

101104
return Result.Ok(((MemberExpression)current, nullChecks));
102105
}
103106

104-
private static Result<MemberExpression> ResolvePropertySegment(
105-
string segment,
106-
Expression parentExpression,
107-
Dictionary<string, string> propertyMapping)
108-
{
109-
if (!propertyMapping.TryGetValue(segment, out var propertyName))
110-
return Result.Fail($"Invalid property '{segment}' in path");
111-
112-
return Expression.Property(parentExpression, propertyName);
113-
}
114-
115107
private static Result<MemberExpression> ResolvePropertyPathForCollection(
116108
PropertyPath propertyPath,
117109
Expression baseExpression,
118-
Dictionary<string, string> propertyMapping)
110+
PropertyMappingTree propertyMappingTree)
119111
{
120112
var current = baseExpression;
121-
var currentPropertyMapping = propertyMapping;
113+
var currentMappingTree = propertyMappingTree;
122114

123115
for (int i = 0; i < propertyPath.Segments.Count; i++)
124116
{
125117
var segment = propertyPath.Segments[i];
126-
var propertyResult = ResolvePropertySegment(segment, current, currentPropertyMapping);
127-
if (propertyResult.IsFailed)
128-
return Result.Fail(propertyResult.Errors);
129118

130-
current = propertyResult.Value;
119+
if (!currentMappingTree.TryGetProperty(segment, out var propertyNode))
120+
return Result.Fail($"Invalid property '{segment}' in lambda expression property path");
131121

132-
// Update property mapping for nested object navigation
122+
current = Expression.Property(current, propertyNode.ActualPropertyName);
123+
124+
// Navigate to nested mapping for next segment
133125
if (i < propertyPath.Segments.Count - 1)
134126
{
135-
currentPropertyMapping = PropertyMappingHelper.CreatePropertyMapping(current.Type);
127+
if (!propertyNode.HasNestedMapping)
128+
return Result.Fail($"Property '{segment}' does not support nested navigation in lambda expression");
129+
130+
currentMappingTree = propertyNode.NestedMapping;
136131
}
137132
}
138133

@@ -146,7 +141,7 @@ private static bool IsNullableReferenceType(Type type)
146141

147142
private static bool IsPrimitiveType(Type type)
148143
{
149-
return type.IsPrimitive || type == typeof(string) || type == typeof(decimal) ||
144+
return type.IsPrimitive || type == typeof(string) || type == typeof(decimal) ||
150145
type == typeof(DateTime) || type == typeof(Guid) ||
151146
Nullable.GetUnderlyingType(type) != null;
152147
}
@@ -313,7 +308,7 @@ private static Result<Expression> EvaluateIdentifierExpression(InfixExpression e
313308
{
314309
var identifier = exp.Left.TokenLiteral();
315310

316-
if (!context.PropertyMapping.TryGetValue(identifier, out var propertyName))
311+
if (!context.PropertyMappingTree.TryGetProperty(identifier, out var propertyNode))
317312
{
318313
return Result.Fail($"Invalid property '{identifier}' within filter");
319314
}
@@ -322,7 +317,7 @@ private static Result<Expression> EvaluateIdentifierExpression(InfixExpression e
322317
(Expression)context.CurrentLambda.Parameter :
323318
context.RootParameter;
324319

325-
var identifierProperty = Expression.Property(baseExpression, propertyName);
320+
var identifierProperty = Expression.Property(baseExpression, propertyNode.ActualPropertyName);
326321
return EvaluateValueComparison(exp, identifierProperty);
327322
}
328323

@@ -374,7 +369,7 @@ private static Result<Expression> EvaluateLambdaExpression(QueryLambdaExpression
374369
(Expression)context.CurrentLambda.Parameter :
375370
context.RootParameter;
376371

377-
var collectionResult = ResolveCollectionProperty(lambdaExp.Property, baseExpression, context.PropertyMapping);
372+
var collectionResult = ResolveCollectionProperty(lambdaExp.Property, baseExpression, context.PropertyMappingTree);
378373
if (collectionResult.IsFailed) return Result.Fail(collectionResult.Errors);
379374

380375
var collectionProperty = collectionResult.Value;
@@ -396,19 +391,19 @@ private static Expression CreateLambdaLinqCall(string function, MemberExpression
396391
: CreateAllExpression(collection, lambda, elementType);
397392
}
398393

399-
private static Result<MemberExpression> ResolveCollectionProperty(QueryExpression property, Expression baseExpression, Dictionary<string, string> propertyMapping)
394+
private static Result<MemberExpression> ResolveCollectionProperty(QueryExpression property, Expression baseExpression, PropertyMappingTree propertyMappingTree)
400395
{
401396
switch (property)
402397
{
403398
case Identifier identifier:
404-
if (!propertyMapping.TryGetValue(identifier.TokenLiteral(), out var propertyName))
399+
if (!propertyMappingTree.TryGetProperty(identifier.TokenLiteral(), out var propertyNode))
405400
{
406401
return Result.Fail($"Invalid property '{identifier.TokenLiteral()}' in lambda expression");
407402
}
408-
return Expression.Property(baseExpression, propertyName);
403+
return Expression.Property(baseExpression, propertyNode.ActualPropertyName);
409404

410405
case PropertyPath propertyPath:
411-
return ResolvePropertyPathForCollection(propertyPath, baseExpression, propertyMapping);
406+
return ResolvePropertyPathForCollection(propertyPath, baseExpression, propertyMappingTree);
412407

413408
default:
414409
return Result.Fail($"Unsupported property type in lambda expression: {property.GetType().Name}");
@@ -459,16 +454,16 @@ private static Result<Expression> EvaluateLambdaBodyIdentifier(InfixExpression e
459454
{
460455
return EvaluateValueComparison(exp, context.CurrentLambda.Parameter);
461456
}
462-
457+
463458
return Result.Fail($"Lambda parameter '{context.CurrentLambda.ParameterName}' cannot be used directly in comparisons for complex types");
464459
}
465460

466-
if (!context.PropertyMapping.TryGetValue(identifierName, out var propertyName))
461+
if (!context.PropertyMappingTree.TryGetProperty(identifierName, out var propertyNode))
467462
{
468463
return Result.Fail($"Invalid property '{identifierName}' within filter");
469464
}
470465

471-
var identifierProperty = Expression.Property(context.RootParameter, propertyName);
466+
var identifierProperty = Expression.Property(context.RootParameter, propertyNode.ActualPropertyName);
472467
return EvaluateValueComparison(exp, identifierProperty);
473468
}
474469

@@ -495,9 +490,9 @@ private static Result<Expression> EvaluateLambdaPropertyPath(InfixExpression exp
495490
var elementType = lambdaParameter.Type;
496491

497492
// Build property path from lambda parameter
498-
var pathResult = BuildLambdaPropertyPath(current, propertyPath.Segments.Skip(1), elementType);
493+
var pathResult = BuildLambdaPropertyPath(current, propertyPath.Segments.Skip(1).ToList(), elementType);
499494
if (pathResult.IsFailed) return pathResult;
500-
495+
501496
current = pathResult.Value;
502497

503498
var finalProperty = (MemberExpression)current;
@@ -531,36 +526,35 @@ private static Expression CreateAllExpression(MemberExpression collection, Lambd
531526
return Expression.AndAlso(hasElements, allMatch);
532527
}
533528

534-
private static Result<Expression> BuildLambdaPropertyPath(Expression startExpression, IEnumerable<string> segments, Type elementType)
529+
private static Result<Expression> BuildLambdaPropertyPath(Expression startExpression, List<string> segments, Type elementType)
535530
{
536531
var current = startExpression;
537-
var lambdaPropertyMapping = PropertyMappingHelper.CreatePropertyMapping(elementType);
532+
var currentMappingTree = PropertyMappingTreeBuilder.BuildMappingTree(elementType, GetDefaultMaxDepth());
538533

539534
foreach (var segment in segments)
540535
{
541-
if (!lambdaPropertyMapping.TryGetValue(segment, out var propertyName))
536+
if (!currentMappingTree.TryGetProperty(segment, out var propertyNode))
542537
{
543-
// If not found in current type, update mapping for nested type and try again
544-
if (current is MemberExpression memberExp)
545-
{
546-
lambdaPropertyMapping = PropertyMappingHelper.CreatePropertyMapping(memberExp.Type);
547-
if (!lambdaPropertyMapping.TryGetValue(segment, out propertyName))
548-
{
549-
return Result.Fail($"Invalid property '{segment}' in lambda property path");
550-
}
551-
}
552-
else
553-
{
554-
return Result.Fail($"Invalid property '{segment}' in lambda property path");
555-
}
538+
return Result.Fail($"Invalid property '{segment}' in lambda property path");
556539
}
557540

558-
current = Expression.Property(current, propertyName);
541+
current = Expression.Property(current, propertyNode.ActualPropertyName);
542+
543+
// Update mapping tree for nested navigation
544+
if (propertyNode.HasNestedMapping)
545+
{
546+
currentMappingTree = propertyNode.NestedMapping;
547+
}
559548
}
560549

561550
return Result.Ok(current);
562551
}
563552

553+
private static int GetDefaultMaxDepth()
554+
{
555+
return new QueryOptions().MaxPropertyMappingDepth;
556+
}
557+
564558
private static Type GetCollectionElementType(Type collectionType)
565559
{
566560
// Handle IEnumerable<T>

src/GoatQuery/src/Evaluator/OrderByEvaluator.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@
77

88
public static class OrderByEvaluator
99
{
10-
public static Result<IQueryable<T>> Evaluate<T>(IEnumerable<OrderByStatement> statements, ParameterExpression parameterExpression, IQueryable<T> queryable, Dictionary<string, string> propertyMapping)
10+
public static Result<IQueryable<T>> Evaluate<T>(IEnumerable<OrderByStatement> statements, ParameterExpression parameterExpression, IQueryable<T> queryable, PropertyMappingTree propertyMappingTree)
1111
{
1212
var isAlreadyOrdered = false;
1313

1414
foreach (var statement in statements)
1515
{
16-
if (!propertyMapping.TryGetValue(statement.TokenLiteral(), out var propertyName))
16+
if (!propertyMappingTree.TryGetProperty(statement.TokenLiteral(), out var propertyNode))
1717
{
1818
return Result.Fail(new Error($"Invalid property '{statement.TokenLiteral()}' within orderby"));
1919
}
2020

21-
var property = Expression.Property(parameterExpression, propertyName);
21+
var property = Expression.Property(parameterExpression, propertyNode.ActualPropertyName);
2222
var lambda = Expression.Lambda(property, parameterExpression);
2323

2424
if (isAlreadyOrdered)

src/GoatQuery/src/Extensions/QueryableExtension.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
using System;
2-
using System.Collections.Generic;
32
using System.Linq;
43
using System.Linq.Expressions;
5-
using System.Reflection;
6-
using System.Text.Json.Serialization;
74
using FluentResults;
85

96
public static class QueryableExtension
@@ -18,7 +15,8 @@ public static Result<QueryResult<T>> Apply<T>(this IQueryable<T> queryable, Quer
1815

1916
var type = typeof(T);
2017

21-
var propertyMappings = PropertyMappingHelper.CreatePropertyMapping<T>();
18+
var maxDepth = options?.MaxPropertyMappingDepth ?? new QueryOptions().MaxPropertyMappingDepth;
19+
var propertyMappingTree = PropertyMappingTreeBuilder.BuildMappingTree<T>(maxDepth);
2220

2321
// Filter
2422
if (!string.IsNullOrEmpty(query.Filter))
@@ -33,7 +31,7 @@ public static Result<QueryResult<T>> Apply<T>(this IQueryable<T> queryable, Quer
3331

3432
ParameterExpression parameter = Expression.Parameter(type);
3533

36-
var expression = FilterEvaluator.Evaluate(statement.Value.Expression, parameter, propertyMappings);
34+
var expression = FilterEvaluator.Evaluate(statement.Value.Expression, parameter, propertyMappingTree);
3735
if (expression.IsFailed)
3836
{
3937
return Result.Fail(expression.Errors);
@@ -75,7 +73,7 @@ public static Result<QueryResult<T>> Apply<T>(this IQueryable<T> queryable, Quer
7573

7674
var parameter = Expression.Parameter(type);
7775

78-
var orderByQuery = OrderByEvaluator.Evaluate<T>(statements, parameter, queryable, propertyMappings);
76+
var orderByQuery = OrderByEvaluator.Evaluate<T>(statements, parameter, queryable, propertyMappingTree);
7977
if (orderByQuery.IsFailed)
8078
{
8179
return Result.Fail(orderByQuery.Errors);

src/GoatQuery/src/QueryOptions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
public sealed class QueryOptions
22
{
33
public int MaxTop { get; set; }
4+
public int MaxPropertyMappingDepth { get; set; } = 5;
45
}

0 commit comments

Comments
 (0)