Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/pages/guide/gridifyGlobalConfiguration.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ some ORMs like NHibernate don't support this. You can disable this behavior by s
- type: `bool`
- default: `false`

### AvoidNullReference

This option allows for an intermediate object to be null

e.g on `obj.PropA.Prob`, `PropA` can be null

This configuration is specific for properties and was introduced after DisableNullChecks.
Hence it has its own property.

### CaseInsensitiveFiltering

If true, string comparison operations are case insensitive by default.
Expand Down
13 changes: 13 additions & 0 deletions docs/pages/guide/gridifyMapper.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,19 @@ This setting is similar to [`DisableNullChecks`](./gridifyGlobalConfiguration.md
var mapper = new GridifyMapper<Person>(q => q.DisableCollectionNullChecks = true);
```

### AvoidNullReference


This setting is similar to [`AvoidNullReference`](./gridifyGlobalConfiguration.md#avoidnullreference) in the global configuration, but it allows you to enable this setting on a per-query basis instead of globally. When set to true, Gridify won't check for null values in collections during filtering operations.

- **Type:** `bool`
- **Default:** `false`

```csharp
var mapper = new GridifyMapper<Person>(q => q.AvoidNullReference = true);
```


### EntityFrameworkCompatibilityLayer

This setting is the same as [`EntityFrameworkCompatibilityLayer`](./extensions/entityframework.md#compatibility-layer) in the global configuration, but it allows you to enable this setting on a per-query basis instead of globally. When set to true, the EntityFramework Compatibility layer is enabled, making the generated expressions compatible with Entity Framework.
Expand Down
122 changes: 83 additions & 39 deletions src/Gridify/Builder/BaseQueryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

namespace Gridify.Builder;

internal abstract class BaseQueryBuilder
{
internal static MemberNullPropagationVisitor MemberNullPropagatorVisitor { get; } = new();
}

public abstract class BaseQueryBuilder<TQuery, T>(IGridifyMapper<T> mapper)
where TQuery : class
{
Expand Down Expand Up @@ -38,55 +43,59 @@ public TQuery Build(ExpressionSyntax expression)

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

private (TQuery Query, bool IsNested) BuildQuery(ExpressionSyntax expression, bool isParenthesisOpen = false)
private (TQuery Query, bool IsNested) BuildQueryFromBinaryExpression(BinaryExpressionSyntax? bExp, bool isParenthesisOpen = false)
{
while (true)
switch (expression.Kind)
if (bExp!.Left is FieldExpressionSyntax && bExp.Right is ValueExpressionSyntax)
try
{
case SyntaxKind.BinaryExpression:
{
var bExp = expression as BinaryExpressionSyntax;
return ConvertBinaryExpressionSyntaxToQuery(bExp)
?? throw new GridifyFilteringException("Invalid expression");
}
catch (GridifyMapperException)
{
if (mapper.Configuration.IgnoreNotMappedFields)
return (BuildAlwaysTrueQuery(), false);

if (bExp!.Left is FieldExpressionSyntax && bExp.Right is ValueExpressionSyntax)
try
{
return ConvertBinaryExpressionSyntaxToQuery(bExp)
?? throw new GridifyFilteringException("Invalid expression");
}
catch (GridifyMapperException)
{
if (mapper.Configuration.IgnoreNotMappedFields)
return (BuildAlwaysTrueQuery(), false);
throw;
}

throw;
}
(TQuery query, bool isNested) leftQuery;
(TQuery query, bool isNested) rightQuery;

(TQuery query, bool isNested) leftQuery;
(TQuery query, bool isNested) rightQuery;
if (bExp.Left is ParenthesizedExpressionSyntax lpExp)
leftQuery = BuildQuery(lpExp.Expression, true);
else
leftQuery = BuildQuery(bExp.Left);

if (bExp.Left is ParenthesizedExpressionSyntax lpExp)
leftQuery = BuildQuery(lpExp.Expression, true);
else
leftQuery = BuildQuery(bExp.Left);

if (bExp.Right is ParenthesizedExpressionSyntax rpExp)
rightQuery = BuildQuery(rpExp.Expression, true);
else
rightQuery = BuildQuery(bExp.Right);

if (bExp.Right is ParenthesizedExpressionSyntax rpExp)
rightQuery = BuildQuery(rpExp.Expression, true);
else
rightQuery = BuildQuery(bExp.Right);
// check for nested collections
if (isParenthesisOpen &&
CheckIfCanMergeQueries(leftQuery, rightQuery, bExp.OperatorToken.Kind) is { } mergedResult)
return (mergedResult, true);

// check for nested collections
if (isParenthesisOpen &&
CheckIfCanMergeQueries(leftQuery, rightQuery, bExp.OperatorToken.Kind) is { } mergedResult)
return (mergedResult, true);
var result = bExp.OperatorToken.Kind switch
{
SyntaxKind.And => CombineWithAndOperator(leftQuery.query, rightQuery.query),
SyntaxKind.Or => CombineWithOrOperator(leftQuery.query, rightQuery.query),
_ => throw new GridifyFilteringException($"Invalid expression Operator '{bExp.OperatorToken.Kind}'")
};
return (result, false);
}

var result = bExp.OperatorToken.Kind switch
{
SyntaxKind.And => CombineWithAndOperator(leftQuery.query, rightQuery.query),
SyntaxKind.Or => CombineWithOrOperator(leftQuery.query, rightQuery.query),
_ => throw new GridifyFilteringException($"Invalid expression Operator '{bExp.OperatorToken.Kind}'")
};
return (result, false);
private (TQuery Query, bool IsNested) BuildQuery(ExpressionSyntax expression, bool isParenthesisOpen = false)
{
while (true)
switch (expression.Kind)
{
case SyntaxKind.BinaryExpression:
{
var bExp = expression as BinaryExpressionSyntax;
return BuildQueryFromBinaryExpression(bExp, isParenthesisOpen);
}
case SyntaxKind.ParenthesizedExpression: // first entrypoint only
{
Expand Down Expand Up @@ -129,10 +138,45 @@ public TQuery Build(ExpressionSyntax expression)

if (hasIndexer)
query = AddIndexerNullCheck(mapTarget, query);
else
query = AddNullPropagator(mapTarget, query);

return ((TQuery)query, false);
}


private object AddNullPropagator(LambdaExpression mapTarget, object query)
{
if (!mapper.Configuration.AvoidNullReference)
return query;

var mainQuery = query as LambdaExpression;
if (mainQuery == null)
return query;

var nullPropagatedExpression = BaseQueryBuilder.MemberNullPropagatorVisitor.Visit(mapTarget) as LambdaExpression;
if (mainQuery.Body is MethodCallExpression methodExp
&& nullPropagatedExpression!.Body.Type != typeof(object)
&& methodExp.Arguments.Count == 1)
{
var call = Expression.Call(methodExp.Method!, nullPropagatedExpression!.Body);
return Expression.Lambda(call, mainQuery.Parameters);
}

var body = mainQuery.Body;
if (body is not BinaryExpression bExp)
return query;

if (bExp.Method == null
|| nullPropagatedExpression!.Body.Type == typeof(object)
|| nullPropagatedExpression.Body is MethodCallExpression)
{
return query;
}
var newExp = Expression.Call(bExp.Method!, nullPropagatedExpression.Body, bExp.Right);
return Expression.Lambda(newExp, mainQuery.Parameters);
}

private object AddIndexerNullCheck(LambdaExpression mapTarget, object query)
{
if (mapper.Configuration.DisableCollectionNullChecks || mapper.Configuration.EntityFrameworkCompatibilityLayer)
Expand Down
6 changes: 6 additions & 0 deletions src/Gridify/GridifyGlobalConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ public static class GridifyGlobalConfiguration
/// </summary>
public static bool IgnoreNotMappedFields { get; set; }

/// This option allows for an intermediate object to be null i.e in obj.PropA.Prob PropA can be null
/// This configuration is specific for properties and was introduced after DisableNullChecks.
/// Hence it has its own property.
/// </summary>
public static bool AvoidNullReference { get; set; } = true;

/// <summary>
/// On nested collections by default gridify adds null check condition
/// to prevent null reference exceptions, e.g () => field != null && field....
Expand Down
8 changes: 8 additions & 0 deletions src/Gridify/GridifyMapperConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ public record GridifyMapperConfiguration
/// </summary>
public bool AllowNullSearch { get; set; } = GridifyGlobalConfiguration.AllowNullSearch;

/// <summary>
/// This option allows for an intermediate object to be null i.e in obj.PropA.Prob PropA can be null
/// This configuration is specific for properties and was introduced after DisableCollectionNullChecks.
/// Hence it has its own property.
/// Default is false
/// </summary>
public bool AvoidNullReference { get; set; } = GridifyGlobalConfiguration.AvoidNullReference;

/// <summary>
/// If true, in filtering and ordering operations,
/// gridify doesn't return any exceptions when a mapping
Expand Down
53 changes: 53 additions & 0 deletions src/Gridify/Reflection/MemberNullPropagationVisitor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;

namespace Gridify.Reflection;

//https://stackoverflow.com/a/30489160/1698682
public class MemberNullPropagationVisitor : ExpressionVisitor
{
protected override Expression VisitMember(MemberExpression node)
{
if (node.Expression == null || !IsNullable(node.Expression.Type))
return base.VisitMember(node);

if (node.Type.IsValueType && node.Type != typeof(Nullable<>))
{
return base.VisitMember(node);
}
var expression = base.Visit(node.Expression);
var nullBaseExpression = Expression.Constant(null, expression.Type);
var test = Expression.Equal(expression, nullBaseExpression);
var nullMemberExpression = Expression.Constant(null, node.Type);
return Expression.Condition(test, nullMemberExpression, node);
}

protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Object == null || !IsNullable(node.Object.Type))
return base.VisitMethodCall(node);

var expression = base.Visit(node.Object);
var nullBaseExpression = Expression.Constant(null, expression.Type);
var test = Expression.Equal(expression, nullBaseExpression);
var nullMemberExpression = Expression.Constant(null, MakeNullable(node.Type));
return Expression.Condition(test, nullMemberExpression, node);
}

private static Type MakeNullable(Type type)
{
if (IsNullable(type))
return type;

return typeof(Nullable<>).MakeGenericType(type);
}

private static bool IsNullable(Type type)
{
if (type.IsClass)
return true;
return type.IsGenericType &&
type.GetGenericTypeDefinition() == typeof(Nullable<>);
}
}
34 changes: 34 additions & 0 deletions test/Gridify.Tests/IssueTests/Issue268Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

using System.Linq;
using AutoBogus;
using Xunit;

namespace Gridify.Tests.IssueTests;

public class TestClassGridifyMapper : GridifyMapper<TestClass>
{
public TestClassGridifyMapper()
{
GenerateMappings()
.AddMap("ChildName", e => e.ChildClass!.Name);
}
}

public class Issue268Tests
{
[Fact]
public void Mapping_ShouldAllowNullProperty()
{
var fakeList = AutoFaker.Generate<TestClass>(10);
fakeList.Add(new TestClass() { ChildClass = null });
fakeList.Add(new TestClass() { ChildClass = new TestClass() });
fakeList.Add(new TestClass() { ChildClass = new TestClass() { Name = "glacor" } });
var mapper = new TestClassGridifyMapper();
mapper.Configuration.AvoidNullReference = true;

var result = fakeList.AsQueryable().ApplyFiltering("ChildName=glacor", mapper).Distinct().ToList();
var result2 = fakeList.AsQueryable().ApplyFiltering("ChildName=", mapper).Distinct().ToList();
Assert.Single(result);
Assert.Equal(2, result2.Count);
}
}
Loading