Skip to content

Commit

Permalink
feat: add support conditional operator with nested collections #81
Browse files Browse the repository at this point in the history
  • Loading branch information
alirezanet committed Jul 22, 2022
1 parent ab8b159 commit 7b57bf1
Show file tree
Hide file tree
Showing 2 changed files with 170 additions and 17 deletions.
55 changes: 38 additions & 17 deletions src/Gridify/Syntax/SyntaxTreeToQueryConvertor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ private static (Expression<Func<T, bool>> Expression, bool IsNested)? ConvertBin
var isNested = ((GMap<T>)gMap).IsNestedCollection();
if (isNested)
{
var result = GenerateNestedExpression(mapper, gMap, right, op);
var result = GenerateNestedExpression(gMap.To.Body, mapper, gMap, right, op);
if (result == null) return null;
return (result, isNested);
}
Expand All @@ -49,29 +49,50 @@ private static LambdaExpression UpdateExpressionIndex(LambdaExpression exp, int
return Expression.Lambda(body, exp.Parameters);
}

private static Expression<Func<T, bool>>? GenerateNestedExpression<T>(
IGridifyMapper<T> mapper,
IGMap<T> gMap,
ValueExpressionSyntax value,
SyntaxNode op)
private static Expression<Func<T, bool>>? GenerateNestedExpression<T>(Expression body, IGridifyMapper<T> mapper, IGMap<T> gMap,
ValueExpressionSyntax value, SyntaxNode op)
{
var body = gMap.To.Body;

if (body is MethodCallExpression selectExp && selectExp.Method.Name == "Select")
while (true)
{
var targetExp = selectExp.Arguments.Single(a => a.NodeType == ExpressionType.Lambda) as LambdaExpression;
var conditionExp = GenerateExpression(targetExp!.Body, targetExp.Parameters[0], value, op, mapper.Configuration.AllowNullSearch,
gMap.Convertor);
switch (body)
{
case MethodCallExpression selectExp when selectExp.Method.Name == "Select":
{
var targetExp = selectExp.Arguments.Single(a => a.NodeType == ExpressionType.Lambda) as LambdaExpression;
var conditionExp = GenerateExpression(targetExp!.Body, targetExp.Parameters[0], value, op, mapper.Configuration.AllowNullSearch,
gMap.Convertor);

if (conditionExp == null) return null;
if (conditionExp == null) return null;

return ParseMethodCallExpression(selectExp, conditionExp) as Expression<Func<T, bool>>;
}
return ParseMethodCallExpression(selectExp, conditionExp) as Expression<Func<T, bool>>;
}
case ConditionalExpression cExp:
{
var ifTrue = GenerateNestedExpression(cExp.IfTrue, mapper, gMap, value, op);
ifTrue = new ReplaceExpressionVisitor(ifTrue!.Parameters[0], gMap.To.Parameters[0]).Visit(ifTrue) as Expression<Func<T, bool>>;
var ifFalse = GenerateNestedExpression(cExp.IfFalse, mapper, gMap, value, op);
ifFalse = new ReplaceExpressionVisitor(ifFalse!.Parameters[0], gMap.To.Parameters[0]).Visit(ifFalse) as Expression<Func<T, bool>>;

// this should never happening
throw new GridifyFilteringException($"The 'Select' method on '{gMap.From}' not found");
var newExp = Expression.Condition(cExp.Test, ifTrue!.Body, ifFalse!.Body);
return Expression.Lambda<Func<T, bool>>(newExp, gMap.To.Parameters[0]);
}
case ConstantExpression constantExpression:
{
return Expression.Lambda<Func<T, bool>>(constantExpression, gMap.To.Parameters[0]);
}
case UnaryExpression uExp:
{
body = uExp.Operand;
continue;
}
default:
// this should never happening
throw new GridifyFilteringException($"The 'Select' method on '{gMap.From}' for type {body.Type} not found");
}
}
}


private static LambdaExpression ParseMethodCallExpression(MethodCallExpression exp, LambdaExpression predicate)
{
switch (exp.Arguments.First())
Expand Down
132 changes: 132 additions & 0 deletions test/Gridify.Tests/Issue81Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
using System.Collections.Generic;
using System.Linq;
using Xunit;

namespace Gridify.Tests;

public class Issue81Tests
{
[Fact]
public void Filtering_WithConditionalOperatorAndWithTwoSelect_ShouldGenerateConditionalExpression()
{
// Arrange
var qb = new QueryBuilder<RootClass>()
.UseEmptyMapper()
.AddMap("PostText", r => r.Blog1Id != null
? r.Blog1.Posts.Select(p => p.Text)
: r.Blog2.Posts.Select(p => p.Text))
.AddCondition("PostText = Hello");

const string expected = "r => IIF((r.Blog1Id != null)," +
" ((r.Blog1.Posts != null) AndAlso r.Blog1.Posts.Any(p => (p.Text == \"Hello\")))," +
" ((r.Blog2.Posts != null) AndAlso r.Blog2.Posts.Any(p => (p.Text == \"Hello\"))))";

// Act
var actual = qb.BuildFilteringExpression().ToString();

// Assert
Assert.Equal(expected, actual);
}

[Fact]
public void Filtering_WithConditionalOperatorAndWithSingleSelect_ShouldGenerateConditionalExpression()
{
// Arrange
var qb = new QueryBuilder<RootClass>()
.UseEmptyMapper()
.AddMap("PostText", r => r.Blog1Id != null
? r.Blog1.Posts.Select(p => p.Text)
: true)
.AddCondition("PostText = Hello");

const string expected = "r => IIF((r.Blog1Id != null)," +
" ((r.Blog1.Posts != null) AndAlso r.Blog1.Posts.Any(p => (p.Text == \"Hello\")))," +
" True)";

// Act
var actual = qb.BuildFilteringExpression().ToString();

// Assert
Assert.Equal(expected, actual);
}

[Fact]
public void Filtering_WithConditionalOperatorAndWithSingleSelect2_ShouldGenerateConditionalExpression()
{
// Arrange
var qb = new QueryBuilder<RootClass>()
.UseEmptyMapper()
.AddMap("PostText", r => r.Blog1Id != null
? true
: r.Blog2.Posts.Select(p => p.Text))
.AddCondition("PostText = Hello");

const string expected = "r => IIF((r.Blog1Id != null)," +
" True," +
" ((r.Blog2.Posts != null) AndAlso r.Blog2.Posts.Any(p => (p.Text == \"Hello\"))))";

// Act
var actual = qb.BuildFilteringExpression().ToString();

// Assert
Assert.Equal(expected, actual);
}

[Fact]
public void Filtering_WithConditionalOperator_ShouldGenerateConditionalExpression()
{
// Arrange
var lst = new List<RootClass>()
{
new()
{
Blog1Id = 1, Blog2Id = 2,
Blog2 = new Blog() { Posts = new List<Post>() { new() { Text = "Bye" } } },
Blog1 = new Blog() { Posts = new List<Post>() { new() { Text = "Hello" } } }
},
}
.AsQueryable();

var qb = new QueryBuilder<RootClass>()
.UseEmptyMapper()
.AddMap("PostText", r => r.Blog1Id != null
? r.Blog1.Posts.Select(p => p.Text)
: r.Blog2.Posts.Select(p => p.Text))
.AddCondition("PostText = Hello");

var expected = lst.Where(q => q.Blog1Id != null
? q.Blog1.Posts.Any(w => w.Text == "Hello")
: q.Blog2.Posts.Any(w => w.Text == "Hello")).ToList();

// Act
var actual = qb.Build(lst).ToList();

// Assert
Assert.Equal(expected.Count, actual.Count);
Assert.Equal(expected, actual);
Assert.True(actual.Any());
}
}

public class RootClass
{
public int Id { get; set; }

public int? Blog1Id { get; set; }
public Blog Blog1 { get; set; }

public int? Blog2Id { get; set; }
public Blog Blog2 { get; set; }
}

public class Blog
{
public int Id { get; set; }
public ICollection<Post> Posts { get; set; }
}

public class Post
{
public int Id { get; set; }
public string Text { get; set; }
}

0 comments on commit 7b57bf1

Please sign in to comment.