From 7b57bf114edf67c328c52809e3604d34b6989bd9 Mon Sep 17 00:00:00 2001 From: AliReZa Sabouri Date: Fri, 22 Jul 2022 20:42:22 +0430 Subject: [PATCH] feat: add support conditional operator with nested collections #81 --- .../Syntax/SyntaxTreeToQueryConvertor.cs | 55 +++++--- test/Gridify.Tests/Issue81Tests.cs | 132 ++++++++++++++++++ 2 files changed, 170 insertions(+), 17 deletions(-) create mode 100644 test/Gridify.Tests/Issue81Tests.cs diff --git a/src/Gridify/Syntax/SyntaxTreeToQueryConvertor.cs b/src/Gridify/Syntax/SyntaxTreeToQueryConvertor.cs index 72239b3d..b74a9e22 100644 --- a/src/Gridify/Syntax/SyntaxTreeToQueryConvertor.cs +++ b/src/Gridify/Syntax/SyntaxTreeToQueryConvertor.cs @@ -31,7 +31,7 @@ private static (Expression> Expression, bool IsNested)? ConvertBin var isNested = ((GMap)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); } @@ -49,29 +49,50 @@ private static LambdaExpression UpdateExpressionIndex(LambdaExpression exp, int return Expression.Lambda(body, exp.Parameters); } - private static Expression>? GenerateNestedExpression( - IGridifyMapper mapper, - IGMap gMap, - ValueExpressionSyntax value, - SyntaxNode op) + private static Expression>? GenerateNestedExpression(Expression body, IGridifyMapper mapper, IGMap 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>; - } + return ParseMethodCallExpression(selectExp, conditionExp) as Expression>; + } + 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>; + var ifFalse = GenerateNestedExpression(cExp.IfFalse, mapper, gMap, value, op); + ifFalse = new ReplaceExpressionVisitor(ifFalse!.Parameters[0], gMap.To.Parameters[0]).Visit(ifFalse) as Expression>; - // 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>(newExp, gMap.To.Parameters[0]); + } + case ConstantExpression constantExpression: + { + return Expression.Lambda>(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()) diff --git a/test/Gridify.Tests/Issue81Tests.cs b/test/Gridify.Tests/Issue81Tests.cs new file mode 100644 index 00000000..40a53a8d --- /dev/null +++ b/test/Gridify.Tests/Issue81Tests.cs @@ -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() + .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() + .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() + .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() + { + new() + { + Blog1Id = 1, Blog2Id = 2, + Blog2 = new Blog() { Posts = new List() { new() { Text = "Bye" } } }, + Blog1 = new Blog() { Posts = new List() { new() { Text = "Hello" } } } + }, + } + .AsQueryable(); + + var qb = new QueryBuilder() + .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 Posts { get; set; } +} + +public class Post +{ + public int Id { get; set; } + public string Text { get; set; } +}