Skip to content

Commit 91ee347

Browse files
committed
wip
1 parent d77eee7 commit 91ee347

File tree

6 files changed

+142
-31
lines changed

6 files changed

+142
-31
lines changed

example/Controllers/UserController.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using AutoMapper;
22
using AutoMapper.QueryableExtensions;
33
using Microsoft.AspNetCore.Mvc;
4+
using Microsoft.EntityFrameworkCore;
45

56
[ApiController]
67
[Route("controller/[controller]")]
@@ -22,6 +23,8 @@ public UsersController(ApplicationDbContext db, IMapper mapper)
2223
public ActionResult<IEnumerable<UserDto>> Get()
2324
{
2425
var users = _db.Users
26+
.Include(x => x.Manager)
27+
.ThenInclude(x => x.Manager)
2528
.Where(x => !x.IsDeleted)
2629
.ProjectTo<UserDto>(_mapper.ConfigurationProvider);
2730

example/Dto/UserDto.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ public record UserDto
1313
public int? NullableInt { get; set; }
1414
public DateTime DateOfBirthUtc { get; set; }
1515
public DateTime DateOfBirthTz { get; set; }
16+
public User? Manager { get; set; }
1617
}

example/Entities/User.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ public record User
1616

1717
[Column(TypeName = "timestamp without time zone")]
1818
public DateTime DateOfBirthTz { get; set; }
19+
public User? Manager { get; set; }
1920
}

example/Program.cs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Reflection;
2+
using System.Text.Json;
23
using AutoMapper;
34
using AutoMapper.QueryableExtensions;
45
using Bogus;
@@ -50,7 +51,8 @@
5051

5152
u.DateOfBirthUtc = date;
5253
u.DateOfBirthTz = TimeZoneInfo.ConvertTimeFromUtc(date, timeZone);
53-
});
54+
})
55+
.RuleFor(x => x.Manager, (f, u) => f.CreateManager(3));
5456

5557
context.Users.AddRange(users.Generate(1_000));
5658
context.SaveChanges();
@@ -62,6 +64,8 @@
6264
app.MapGet("/minimal/users", (ApplicationDbContext db, [FromServices] IMapper mapper, [AsParameters] Query query) =>
6365
{
6466
var result = db.Users
67+
.Include(x => x.Manager)
68+
.ThenInclude(x => x.Manager)
6569
.Where(x => !x.IsDeleted)
6670
.ProjectTo<UserDto>(mapper.ConfigurationProvider)
6771
.Apply(query);
@@ -79,3 +83,27 @@
7983
app.MapControllers();
8084

8185
app.Run();
86+
87+
public static class FakerExtensions
88+
{
89+
public static User? CreateManager(this Faker f, int depth)
90+
{
91+
if (depth <= 0 || !f.Random.Bool(0.6f)) // 60% chance of having manager, stop at depth 0
92+
return null;
93+
94+
return new User
95+
{
96+
Id = f.Random.Guid(),
97+
Firstname = f.Person.FirstName,
98+
Lastname = f.Person.LastName,
99+
Age = f.Random.Int(0, 100),
100+
IsDeleted = f.Random.Bool(),
101+
Test = f.Random.Double(),
102+
NullableInt = f.Random.Bool() ? f.Random.Int(1, 100) : null,
103+
IsEmailVerified = f.Random.Bool(),
104+
DateOfBirthUtc = f.Date.Past().ToUniversalTime(),
105+
DateOfBirthTz = TimeZoneInfo.ConvertTimeFromUtc(f.Date.Past().ToUniversalTime(), TimeZoneInfo.FindSystemTimeZoneById("America/New_York")),
106+
Manager = f.CreateManager(depth - 1) // Recursive call with reduced depth
107+
};
108+
}
109+
}

src/GoatQuery/src/Evaluator/FilterEvaluator.cs

Lines changed: 90 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -88,20 +88,12 @@ private static Result<Expression> BuildPropertyExpression(ParameterExpression pa
8888
var nullCheck = Expression.NotEqual(current, Expression.Constant(null));
8989
var propertyAccess = Expression.Property(current, segment);
9090

91-
// For the final segment, we'll handle null checks in the comparison
92-
if (segment == segments.Last())
93-
{
94-
current = propertyAccess;
95-
}
96-
else
97-
{
98-
// For intermediate segments, we need to ensure they're not null
99-
current = Expression.Condition(
100-
nullCheck,
101-
propertyAccess,
102-
Expression.Constant(null, propertyInfo.PropertyType)
103-
);
104-
}
91+
// Create null-safe navigation for all segments
92+
current = Expression.Condition(
93+
nullCheck,
94+
propertyAccess,
95+
Expression.Constant(null, propertyInfo.PropertyType)
96+
);
10597
}
10698
else
10799
{
@@ -223,15 +215,92 @@ private static Result<Expression> CreatePropertyPathComparison(Expression proper
223215

224216
private static Expression CreateNullCheckExpression(Expression property, List<string> segments)
225217
{
226-
// Work backwards from the property expression to create null checks for each level
227-
// For now, we'll use a simple approach - if any level is null, the comparison is false
218+
// For property paths, we need to ensure that all intermediate reference types are not null
219+
// The BuildPropertyExpression already handles null-safe navigation with conditional expressions,
220+
// but we need to create an additional check for the final comparison
221+
222+
if (segments.Count <= 1)
223+
{
224+
// Single property, no intermediate null checks needed
225+
return Expression.Constant(true);
226+
}
227+
228+
// For a path like "manager.firstName", we need to check that "manager" is not null
229+
// We need to traverse the property path and create null checks for each reference type level
230+
231+
var parameterExpression = GetParameterFromProperty(property);
232+
if (parameterExpression == null)
233+
{
234+
return Expression.Constant(true);
235+
}
236+
237+
Expression current = parameterExpression;
238+
Expression nullCheckExpression = null;
239+
240+
// Check all segments except the last one (since the last one is handled in the comparison)
241+
for (int i = 0; i < segments.Count - 1; i++)
242+
{
243+
var segment = segments[i];
244+
var propertyInfo = current.Type.GetProperty(segment);
245+
246+
if (propertyInfo != null)
247+
{
248+
var propertyAccess = Expression.Property(current, segment);
249+
250+
// Only add null checks for reference types (not value types)
251+
if (!propertyInfo.PropertyType.IsValueType)
252+
{
253+
var nullCheck = Expression.NotEqual(propertyAccess, Expression.Constant(null));
254+
255+
if (nullCheckExpression == null)
256+
{
257+
nullCheckExpression = nullCheck;
258+
}
259+
else
260+
{
261+
nullCheckExpression = Expression.AndAlso(nullCheckExpression, nullCheck);
262+
}
263+
}
264+
265+
current = propertyAccess;
266+
}
267+
}
228268

229-
// This is a simplified implementation. In a more robust version, you'd want to
230-
// traverse the expression tree and create proper null checks for each level.
231-
// For the current implementation, we'll rely on the property mapping and expression building
232-
// to handle the null safety.
269+
return nullCheckExpression ?? Expression.Constant(true);
270+
}
233271

234-
return Expression.Constant(true); // Placeholder - will be enhanced based on testing
272+
private static ParameterExpression GetParameterFromProperty(Expression expression)
273+
{
274+
// Traverse the expression tree to find the root parameter
275+
while (expression != null)
276+
{
277+
if (expression is ParameterExpression parameter)
278+
{
279+
return parameter;
280+
}
281+
else if (expression is MemberExpression memberExpression)
282+
{
283+
expression = memberExpression.Expression;
284+
}
285+
else if (expression is ConditionalExpression conditionalExpression)
286+
{
287+
// Handle the case where we have conditional expressions from BuildPropertyExpression
288+
expression = conditionalExpression.Test;
289+
if (expression is BinaryExpression binaryExpression && binaryExpression.Left is MemberExpression memberExpr)
290+
{
291+
expression = memberExpr.Expression;
292+
}
293+
else
294+
{
295+
break;
296+
}
297+
}
298+
else
299+
{
300+
break;
301+
}
302+
}
303+
return null;
235304
}
236305

237306
private static Result<Expression> CreateStandardComparison(Expression property, ConstantExpression value, string operatorKeyword, QueryExpression leftExpression)

src/GoatQuery/src/Extensions/QueryableExtension.cs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,32 @@ public static class QueryableExtension
1111
private static Dictionary<string, string> CreatePropertyMapping<T>()
1212
{
1313
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
14-
var visitedTypes = new HashSet<Type>();
14+
var typeMappings = new Dictionary<Type, Dictionary<string, string>>();
1515

16-
BuildPropertyMapping(typeof(T), "", result, visitedTypes);
16+
BuildPropertyMapping(typeof(T), "", result, typeMappings);
1717

1818
return result;
1919
}
2020

21-
private static void BuildPropertyMapping(Type type, string prefix, Dictionary<string, string> result, HashSet<Type> visitedTypes)
21+
private static void BuildPropertyMapping(Type type, string prefix, Dictionary<string, string> result, Dictionary<Type, Dictionary<string, string>> typeMappings)
2222
{
23-
// Prevent circular reference loops
24-
if (visitedTypes.Contains(type))
23+
// Check if we've already mapped this type
24+
if (typeMappings.ContainsKey(type))
2525
{
26+
// Reuse the existing mappings for this type
27+
var existingMappings = typeMappings[type];
28+
foreach (var mapping in existingMappings)
29+
{
30+
var prefixedJsonPath = string.IsNullOrEmpty(prefix) ? mapping.Key : $"{prefix}/{mapping.Key}";
31+
var prefixedPropertyPath = string.IsNullOrEmpty(prefix) ? mapping.Value : $"{prefix}.{mapping.Value}";
32+
result[prefixedJsonPath] = prefixedPropertyPath;
33+
}
2634
return;
2735
}
2836

29-
visitedTypes.Add(type);
37+
// Create a new mapping for this type
38+
var currentTypeMappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
39+
typeMappings[type] = currentTypeMappings;
3040

3141
var properties = type.GetProperties();
3242

@@ -42,15 +52,14 @@ private static void BuildPropertyMapping(Type type, string prefix, Dictionary<st
4252

4353
// Add the mapping for this property
4454
result[fullJsonPath] = fullPropertyPath;
55+
currentTypeMappings[jsonName] = property.Name;
4556

4657
// Recursively process nested object properties (but not primitive types, collections, etc.)
4758
if (IsComplexType(property.PropertyType))
4859
{
49-
BuildPropertyMapping(property.PropertyType, fullJsonPath, result, visitedTypes);
60+
BuildPropertyMapping(property.PropertyType, fullJsonPath, result, typeMappings);
5061
}
5162
}
52-
53-
visitedTypes.Remove(type);
5463
}
5564

5665
private static bool IsComplexType(Type type)

0 commit comments

Comments
 (0)