Skip to content

Commit ae3bdc8

Browse files
author
Glacor Soul
committed
chore: #286 Gridify should allow intermediate null values
1 parent ef8a077 commit ae3bdc8

File tree

5 files changed

+211
-39
lines changed

5 files changed

+211
-39
lines changed

src/Gridify/Builder/BaseQueryBuilder.cs

Lines changed: 107 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66

77
namespace Gridify.Builder;
88

9+
internal static class BaseQueryBuilder
10+
{
11+
internal static MemberNullPropogationVisitor MemberNullPropagatorVisitor { get; } = new();
12+
}
13+
914
public abstract class BaseQueryBuilder<TQuery, T>(IGridifyMapper<T> mapper)
1015
where TQuery : class
1116
{
@@ -38,55 +43,59 @@ public TQuery Build(ExpressionSyntax expression)
3843

3944
protected abstract TQuery CombineWithOrOperator(TQuery left, TQuery right);
4045

41-
private (TQuery Query, bool IsNested) BuildQuery(ExpressionSyntax expression, bool isParenthesisOpen = false)
46+
private (TQuery Query, bool IsNested) BuildQueryFromBinaryExpression(BinaryExpressionSyntax? bExp, bool isParenthesisOpen = false)
4247
{
43-
while (true)
44-
switch (expression.Kind)
48+
if (bExp!.Left is FieldExpressionSyntax && bExp.Right is ValueExpressionSyntax)
49+
try
4550
{
46-
case SyntaxKind.BinaryExpression:
47-
{
48-
var bExp = expression as BinaryExpressionSyntax;
51+
return ConvertBinaryExpressionSyntaxToQuery(bExp)
52+
?? throw new GridifyFilteringException("Invalid expression");
53+
}
54+
catch (GridifyMapperException)
55+
{
56+
if (mapper.Configuration.IgnoreNotMappedFields)
57+
return (BuildAlwaysTrueQuery(), false);
4958

50-
if (bExp!.Left is FieldExpressionSyntax && bExp.Right is ValueExpressionSyntax)
51-
try
52-
{
53-
return ConvertBinaryExpressionSyntaxToQuery(bExp)
54-
?? throw new GridifyFilteringException("Invalid expression");
55-
}
56-
catch (GridifyMapperException)
57-
{
58-
if (mapper.Configuration.IgnoreNotMappedFields)
59-
return (BuildAlwaysTrueQuery(), false);
59+
throw;
60+
}
6061

61-
throw;
62-
}
62+
(TQuery query, bool isNested) leftQuery;
63+
(TQuery query, bool isNested) rightQuery;
6364

64-
(TQuery query, bool isNested) leftQuery;
65-
(TQuery query, bool isNested) rightQuery;
65+
if (bExp.Left is ParenthesizedExpressionSyntax lpExp)
66+
leftQuery = BuildQuery(lpExp.Expression, true);
67+
else
68+
leftQuery = BuildQuery(bExp.Left);
6669

67-
if (bExp.Left is ParenthesizedExpressionSyntax lpExp)
68-
leftQuery = BuildQuery(lpExp.Expression, true);
69-
else
70-
leftQuery = BuildQuery(bExp.Left);
7170

71+
if (bExp.Right is ParenthesizedExpressionSyntax rpExp)
72+
rightQuery = BuildQuery(rpExp.Expression, true);
73+
else
74+
rightQuery = BuildQuery(bExp.Right);
7275

73-
if (bExp.Right is ParenthesizedExpressionSyntax rpExp)
74-
rightQuery = BuildQuery(rpExp.Expression, true);
75-
else
76-
rightQuery = BuildQuery(bExp.Right);
76+
// check for nested collections
77+
if (isParenthesisOpen &&
78+
CheckIfCanMergeQueries(leftQuery, rightQuery, bExp.OperatorToken.Kind) is { } mergedResult)
79+
return (mergedResult, true);
7780

78-
// check for nested collections
79-
if (isParenthesisOpen &&
80-
CheckIfCanMergeQueries(leftQuery, rightQuery, bExp.OperatorToken.Kind) is { } mergedResult)
81-
return (mergedResult, true);
81+
var result = bExp.OperatorToken.Kind switch
82+
{
83+
SyntaxKind.And => CombineWithAndOperator(leftQuery.query, rightQuery.query),
84+
SyntaxKind.Or => CombineWithOrOperator(leftQuery.query, rightQuery.query),
85+
_ => throw new GridifyFilteringException($"Invalid expression Operator '{bExp.OperatorToken.Kind}'")
86+
};
87+
return (result, false);
88+
}
8289

83-
var result = bExp.OperatorToken.Kind switch
84-
{
85-
SyntaxKind.And => CombineWithAndOperator(leftQuery.query, rightQuery.query),
86-
SyntaxKind.Or => CombineWithOrOperator(leftQuery.query, rightQuery.query),
87-
_ => throw new GridifyFilteringException($"Invalid expression Operator '{bExp.OperatorToken.Kind}'")
88-
};
89-
return (result, false);
90+
private (TQuery Query, bool IsNested) BuildQuery(ExpressionSyntax expression, bool isParenthesisOpen = false)
91+
{
92+
while (true)
93+
switch (expression.Kind)
94+
{
95+
case SyntaxKind.BinaryExpression:
96+
{
97+
var bExp = expression as BinaryExpressionSyntax;
98+
return BuildQueryFromBinaryExpression(bExp, isParenthesisOpen);
9099
}
91100
case SyntaxKind.ParenthesizedExpression: // first entrypoint only
92101
{
@@ -129,10 +138,69 @@ public TQuery Build(ExpressionSyntax expression)
129138

130139
if (hasIndexer)
131140
query = AddIndexerNullCheck(mapTarget, query);
141+
else
142+
query = AddNullPropagator(mapTarget, query);
132143

133144
return ((TQuery)query, false);
134145
}
135146

147+
148+
private object AddNullPropagator(LambdaExpression mapTarget, object query)
149+
{
150+
if (!mapper.Configuration.AvoidNullReference)
151+
return query;
152+
153+
var mainQuery = query as LambdaExpression;
154+
if (mainQuery == null)
155+
return query;
156+
157+
var nullPropagatedExpression = BaseQueryBuilder.MemberNullPropagatorVisitor.Visit(mapTarget) as LambdaExpression;
158+
if (mainQuery.Body is MethodCallExpression methodExp
159+
&& nullPropagatedExpression!.Body.Type != typeof(object)
160+
&& methodExp.Arguments.Count == 1)
161+
{
162+
var call = Expression.Call(methodExp.Method!, nullPropagatedExpression!.Body);
163+
return Expression.Lambda(call, mainQuery.Parameters);
164+
}
165+
166+
var body = mainQuery.Body;
167+
if (body is not BinaryExpression bExp)
168+
return query;
169+
170+
if (bExp.Method == null
171+
|| nullPropagatedExpression!.Body.Type == typeof(object)
172+
|| nullPropagatedExpression.Body is MethodCallExpression)
173+
{
174+
return query;
175+
}
176+
var newExp = Expression.Call(bExp.Method!, nullPropagatedExpression.Body, bExp.Right);
177+
return Expression.Lambda(newExp, mainQuery.Parameters);
178+
}
179+
180+
private object AddNullPropagator2(LambdaExpression mapTarget, object query)
181+
{
182+
if (!mapper.Configuration.AvoidNullReference)
183+
return query;
184+
185+
var mainQuery = query as LambdaExpression;
186+
if (mainQuery == null)
187+
return query;
188+
189+
var body = mainQuery.Body;
190+
if (body is not BinaryExpression bExp)
191+
return query;
192+
193+
var nullPropagatedExpression = new MemberNullPropogationVisitor().Visit(mapTarget) as LambdaExpression;
194+
if (bExp.Method == null
195+
|| nullPropagatedExpression.Body.Type == typeof(object)
196+
|| nullPropagatedExpression.Body is MethodCallExpression)
197+
{
198+
return query;
199+
}
200+
var newExp = Expression.Call(bExp.Method!, nullPropagatedExpression.Body, bExp.Right);
201+
return Expression.Lambda(newExp, mainQuery.Parameters);
202+
}
203+
136204
private object AddIndexerNullCheck(LambdaExpression mapTarget, object query)
137205
{
138206
if (mapper.Configuration.DisableCollectionNullChecks || mapper.Configuration.EntityFrameworkCompatibilityLayer)

src/Gridify/GridifyGlobalConfiguration.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ public static class GridifyGlobalConfiguration
2424
/// </summary>
2525
public static bool CaseSensitiveMapper { get; set; }
2626

27+
/// <summary>
28+
/// This option enables allows for an intermediate object to be null i.e in obj.PropA.Prob PropA can be null
29+
/// Default is false
30+
/// </summary>
31+
public static bool AvoidNullReference { get; set; } = true;
32+
2733
/// <summary>
2834
/// This option enables the 'null' keyword in filtering operations
2935
/// Default is true

src/Gridify/GridifyMapperConfiguration.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ public record GridifyMapperConfiguration
1616
/// </summary>
1717
public bool AllowNullSearch { get; set; } = GridifyGlobalConfiguration.AllowNullSearch;
1818

19+
/// <summary>
20+
/// This option enables allows for an intermediate object to be null i.e in obj.PropA.Prob PropA can be null
21+
/// Default is false
22+
/// </summary>
23+
public bool AvoidNullReference { get; set; } = GridifyGlobalConfiguration.AvoidNullReference;
24+
1925
/// <summary>
2026
/// If true, in filtering and ordering operations,
2127
/// gridify doesn't return any exceptions when a mapping
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System;
2+
using System.Diagnostics.CodeAnalysis;
3+
using System.Linq.Expressions;
4+
5+
namespace Gridify.Reflection;
6+
7+
//https://stackoverflow.com/a/30489160/1698682
8+
public class MemberNullPropogationVisitor : ExpressionVisitor
9+
{
10+
protected override Expression VisitMember(MemberExpression node)
11+
{
12+
if (node.Expression == null || !IsNullable(node.Expression.Type))
13+
return base.VisitMember(node);
14+
15+
if (node.Type.IsValueType && node.Type != typeof(Nullable<>))
16+
{
17+
return base.VisitMember(node);
18+
}
19+
else
20+
{
21+
var expression = base.Visit(node.Expression);
22+
var nullBaseExpression = Expression.Constant(null, expression.Type);
23+
var test = Expression.Equal(expression, nullBaseExpression);
24+
var memberAccess = Expression.MakeMemberAccess(expression, node.Member);
25+
var nullMemberExpression = Expression.Constant(null, node.Type);
26+
return Expression.Condition(test, nullMemberExpression, node);
27+
}
28+
}
29+
30+
protected override Expression VisitMethodCall(MethodCallExpression node)
31+
{
32+
if (node.Object == null || !IsNullable(node.Object.Type))
33+
return base.VisitMethodCall(node);
34+
35+
var expression = base.Visit(node.Object);
36+
var nullBaseExpression = Expression.Constant(null, expression.Type);
37+
var test = Expression.Equal(expression, nullBaseExpression);
38+
var memberAccess = Expression.Call(expression, node.Method);
39+
var nullMemberExpression = Expression.Constant(null, MakeNullable(node.Type));
40+
return Expression.Condition(test, nullMemberExpression, node);
41+
}
42+
43+
private static Type MakeNullable(Type type)
44+
{
45+
if (IsNullable(type))
46+
return type;
47+
48+
return typeof(Nullable<>).MakeGenericType(type);
49+
}
50+
51+
private static bool IsNullable(Type type)
52+
{
53+
if (type.IsClass)
54+
return true;
55+
return type.IsGenericType &&
56+
type.GetGenericTypeDefinition() == typeof(Nullable<>);
57+
}
58+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
2+
using System.Linq;
3+
using AutoBogus;
4+
using Xunit;
5+
6+
namespace Gridify.Tests.IssueTests;
7+
8+
public class TestClassGridifyMapper : GridifyMapper<TestClass>
9+
{
10+
public TestClassGridifyMapper()
11+
{
12+
GenerateMappings()
13+
.AddMap("ChildName", e => e.ChildClass.Name);
14+
}
15+
}
16+
17+
public class Issue286Tests
18+
{
19+
[Fact]
20+
public void Mapping_ShouldAllowNullProperty()
21+
{
22+
GridifyGlobalConfiguration.AvoidNullReference = true;
23+
var fakeList = AutoFaker.Generate<TestClass>(10);
24+
fakeList.Add(new TestClass() { ChildClass = null });
25+
fakeList.Add(new TestClass() { ChildClass = new TestClass() });
26+
fakeList.Add(new TestClass() { ChildClass = new TestClass() { Name = "glacor" } });
27+
var mapper = new TestClassGridifyMapper();
28+
29+
var result = fakeList.AsQueryable().ApplyFiltering("ChildName=glacor", mapper).Distinct().ToList();
30+
var result2 = fakeList.AsQueryable().ApplyFiltering("ChildName=", mapper).Distinct().ToList();
31+
Assert.Single(result);
32+
Assert.Equal(2, result2.Count);
33+
}
34+
}

0 commit comments

Comments
 (0)