From a13bba6e61e36863773d019a250c15298d2f3f18 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 14 Nov 2018 14:49:07 +0100 Subject: [PATCH 1/6] GenerateAndAlsoMemberExpression --- .../ConsoleAppEF2.1.1_InMemory/Program.cs | 148 ++---------------- .../Parser/ExpressionHelper.cs | 63 ++++++++ .../Parser/ExpressionParser.cs | 75 +++++---- .../Parser/IExpressionHelper.cs | 2 + .../Parser/KeywordsHelper.cs | 2 + src/System.Linq.Dynamic.Core/Res.cs | 2 + 6 files changed, 126 insertions(+), 166 deletions(-) diff --git a/src-console/ConsoleAppEF2.1.1_InMemory/Program.cs b/src-console/ConsoleAppEF2.1.1_InMemory/Program.cs index 18dad64c..63b4dd91 100644 --- a/src-console/ConsoleAppEF2.1.1_InMemory/Program.cs +++ b/src-console/ConsoleAppEF2.1.1_InMemory/Program.cs @@ -7,7 +7,6 @@ using System.Reflection; using ConsoleAppEF2.Database; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace ConsoleAppEF2 { @@ -18,11 +17,17 @@ public class NestedDto public string Name { get; set; } public NestedDto2 NestedDto2 { get; set; } + } + public class NestedDto2 + { + public string Name2 { get; set; } + public int Id { get; set; } + public NestedDto3 NestedDto3 { get; set; } } - public class NestedDto2 + public class NestedDto3 { public string Name2 { get; set; } @@ -50,129 +55,18 @@ public Type ResolveType(string typeName) } } - private static object GetObj() - { - return new - { - Id = 5, - Value = 400 - }; - } - - class X : DynamicClass - { - - } - - private static IQueryable GetQueryable() - { - var random = new Random((int)DateTime.Now.Ticks); - - var jt = typeof(JToken); - - var em = jt.GetTypeInfo().GetDeclaredMethods("op_Explicit"); - var im = jt.GetTypeInfo().GetDeclaredMethods("op_Explicit"); - - var j = new JObject - { - { "Id", new JValue(9) }, - { "Name", new JValue("Test") } - }; - - //(j["Id"] as JValue).Value - - IQueryable jarray = new[] { j }.AsQueryable(); - var jresult = jarray.Select("new (int(Id) as Id, string(Name) as Name)"); - - var an = jresult.Any("Id > 4"); - - - var dx = new X(); - dx["Id"] = 5; - - IQueryable srcDX = new[] { dx }.AsQueryable(); - var b = srcDX.Select("new (Id.ToString() as Id)"); - var anyDX = b.Any("int.Parse(Id) > 4"); - - var x = Enumerable.Range(0, 10).Select(i => new - { - Id = i, - Value = random.Next() - }).AsQueryable(); - - //var any = x.Any("Id > 4"); - - //var obj = new - //{ - // Id = 5, - // Value = random.Next() - //}; - //var x2 = Enumerable.Range(0, 1).Select(_ => obj).AsQueryable(); - //var any2 = x.Any("Id > 4"); - - //var o = GetObj(); - //var t = o.GetType(); - //IQueryable source = new[] { o }.AsQueryable(); - //// source.ElementType = t; - - //var x2b = new[] { o }.AsQueryable(); - //var any2function = x2b.Any(null, "Id > 4", t); - - //var any2b = x2b.Any("Id > 4"); - - //var x3 = new[] { obj }.AsQueryable(); - //var any3 = x3.Any("Id > 4"); - - return x.Select("new (it as Id, @0 as Value)", random.Next()); - // return x.AsQueryable(); //x.AsQueryable().Select("new (Id, Value)"); - } - - public static IQueryable Transform(this IQueryable source, Type resultType) + static void Main(string[] args) { - var resultProperties = resultType.GetProperties().Where(p => p.CanWrite); + var q = new[] { new NestedDto(), new NestedDto { NestedDto2 = new NestedDto2 { NestedDto3 = new NestedDto3 { Id = 42 } } } }.AsQueryable(); - ParameterExpression s = Expression.Parameter(source.ElementType, "s"); + var np1 = q.Select("np(it.NestedDto2.NestedDto3.Id, 0)"); + var npResult1 = np1.ToDynamicList(); + Console.WriteLine("npResult1 {0}", JsonConvert.SerializeObject(npResult1, Formatting.Indented)); - var memberBindings = - resultProperties.Select(p => - Expression.Bind(resultType.GetMember(p.Name)[0], Expression.Property(s, p.Name))).OfType(); + var np2 = q.Select("np(it.NestedDto2.NestedDto3.Id)"); + var npResult2 = np2.ToDynamicList(); + Console.WriteLine("npResult2 {0}", JsonConvert.SerializeObject(npResult2, Formatting.Indented)); - Expression memberInit = Expression.MemberInit( - Expression.New(resultType), - memberBindings - ); - - var memberInitLambda = Expression.Lambda(memberInit, s); - - var typeArgs = new[] - { - source.ElementType, - memberInit.Type - }; - - var mc = Expression.Call(typeof(Queryable), "Select", typeArgs, source.Expression, memberInitLambda); - - var query = source.Provider.CreateQuery(mc); - - return query; - } - - public static IQueryable EmptyQueryByExample(this T _) => Enumerable.Empty().AsQueryable(); - - - private static TResult Execute(MethodInfo operatorMethodInfo, IQueryable source, Expression expression, Type t = null) - { - operatorMethodInfo = operatorMethodInfo.GetGenericArguments().Length == 2 - ? operatorMethodInfo.MakeGenericMethod(t == null ? source.ElementType : t, typeof(TResult)) - : operatorMethodInfo.MakeGenericMethod(t == null ? source.ElementType : t); - - var optimized = Expression.Call(null, operatorMethodInfo, source.Expression, expression); - return source.Provider.Execute(optimized); - } - - static void Main(string[] args) - { - var q = new[] { new NestedDto(), new NestedDto { NestedDto2 = new NestedDto2 { Id = 42 } } }.AsQueryable(); var r1 = q.Select("it != null && it.NestedDto2 != null ? it.NestedDto2.Id : null"); var list1 = r1.ToDynamicList(); @@ -191,18 +85,6 @@ static void Main(string[] args) Console.WriteLine(projectedData.First().Name); Console.WriteLine(projectedData.Last().Name); - IQueryable qry = GetQueryable(); - - var result = qry.Select("it").OrderBy("Value"); - try - { - Console.WriteLine("result {0}", JsonConvert.SerializeObject(result, Formatting.Indented)); - } - catch (Exception) - { - // Console.WriteLine(e); - } - var all = new { test1 = new List { 1, 2, 3 }.ToDynamicList(typeof(int)), diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs index 529c67af..1ec8f6d6 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs @@ -1,4 +1,5 @@ using JetBrains.Annotations; +using System.Collections.Generic; using System.Globalization; using System.Linq.Dynamic.Core.Validation; using System.Linq.Expressions; @@ -234,5 +235,67 @@ private void WrapConstantExpressions(ref Expression left, ref Expression right) _constantExpressionWrapper.Wrap(ref right); } } + + public Expression GenerateAndAlsoMemberExpression(Expression expression) + { + var memberExpresssions = GetAllMemberExpressions(expression); + if (!memberExpresssions.Any()) + { + return null; + } + + memberExpresssions.Reverse(); + + var binaryExpressions = new List(); + foreach (var memberExpresssion in memberExpresssions) + { + binaryExpressions.Add(Expression.NotEqual(memberExpresssion, Constants.NullLiteral)); + } + + var andAlsoExpression = binaryExpressions[0]; + for (int i = 1; i < binaryExpressions.Count; i++) + { + andAlsoExpression = Expression.AndAlso(andAlsoExpression, binaryExpressions[i]); + } + + return andAlsoExpression; + } + + private static MemberExpression GetMemberExpression(Expression expression) + { + if (expression is MemberExpression) + { + return (MemberExpression)expression; + } + else if (expression is LambdaExpression lambdaExpression) + { + if (lambdaExpression.Body is MemberExpression memberExpression) + { + return memberExpression; + } + else if (lambdaExpression.Body is UnaryExpression unaryExpression) + { + return (MemberExpression)unaryExpression.Operand; + } + } + return null; + } + + private static List GetAllMemberExpressions(Expression expression) + { + var list = new List(); + var memberExpression = GetMemberExpression(expression); + + while (memberExpression != null) + { + memberExpression = GetMemberExpression(memberExpression.Expression); + if (memberExpression != null) + { + list.Add(memberExpression); + } + } + + return list; + } } } diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index a4341b54..94a061ef 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -210,23 +210,6 @@ Expression ParseConditionalOperator() return expr; } - // ?. (null-propagating) operator - //Expression ParseNullPropagatingOperator() - //{ - // int errorPos = _textParser.CurrentToken.Pos; - // Expression expr = ParseNullCoalescingOperator(); - // if (_textParser.CurrentToken.Id == TokenId.Question) - // { - // _textParser.NextToken(); - // Expression expr1 = ParseConditionalOperator(); - // _textParser.ValidateToken(TokenId.Dot, Res.DotExpected); - // _textParser.NextToken(); - // Expression expr2 = ParseConditionalOperator(); - // expr = GenerateConditional(expr, expr1, expr2, errorPos); - // } - // return expr; - //} - // ?? (null-coalescing) operator Expression ParseNullCoalescingOperator() { @@ -979,6 +962,7 @@ Expression ParseIdentifier() if (value == (object)KeywordsHelper.KEYWORD_IIF) return ParseIif(); if (value == (object)KeywordsHelper.KEYWORD_NEW) return ParseNew(); if (value == (object)KeywordsHelper.KEYWORD_ISNULL) return ParseIsNull(); + if (value == (object)KeywordsHelper.KEYWORD_NULLPROPAGATION) return ParseNullPropagation(); _textParser.NextToken(); @@ -1050,6 +1034,7 @@ Expression ParseIif() { int errorPos = _textParser.CurrentToken.Pos; _textParser.NextToken(); + Expression[] args = ParseArgumentList(); if (args.Length != 3) { @@ -1059,6 +1044,30 @@ Expression ParseIif() return GenerateConditional(args[0], args[1], args[2], errorPos); } + Expression ParseNullPropagation() + { + int errorPos = _textParser.CurrentToken.Pos; + _textParser.NextToken(); + + Expression[] args = ParseArgumentList(); + + if (args.Length != 1 && args.Length != 2) + { + throw ParseError(errorPos, Res.NullPropagationRequiresCorrectArgs); + } + + if (args[0] is MemberExpression memberExpression) + { + var expressionTest = _expressionHelper.GenerateAndAlsoMemberExpression(memberExpression); + var expressionIfTrue = memberExpression; + var expressionIfFalse = args.Length == 2 ? args[1] : Constants.NullLiteral; + + return GenerateConditional(expressionTest, expressionIfTrue, expressionIfFalse, errorPos); + } + + throw ParseError(errorPos, Res.NullPropagationRequiresMemberExpression); + } + Expression GenerateConditional(Expression test, Expression expr1, Expression expr2, int errorPos) { if (test.Type != typeof(bool)) @@ -1251,33 +1260,33 @@ private Expression CreateNewExpression(List properties, List); + type = typeof(DynamicClass); + Type typeForKeyValuePair = typeof(KeyValuePair); #if NET35 || NET40 ConstructorInfo constructorForKeyValuePair = typeForKeyValuePair.GetConstructors().First(); #else - ConstructorInfo constructorForKeyValuePair = typeForKeyValuePair.GetTypeInfo().DeclaredConstructors.First(); + ConstructorInfo constructorForKeyValuePair = typeForKeyValuePair.GetTypeInfo().DeclaredConstructors.First(); #endif - var arrayIndexParams = new List(); - for (int i = 0; i < expressions.Count; i++) - { - // Just convert the expression always to an object expression. - UnaryExpression boxingExpression = Expression.Convert(expressions[i], typeof(object)); - NewExpression parameter = Expression.New(constructorForKeyValuePair, (Expression)Expression.Constant(properties[i].Name), boxingExpression); + var arrayIndexParams = new List(); + for (int i = 0; i < expressions.Count; i++) + { + // Just convert the expression always to an object expression. + UnaryExpression boxingExpression = Expression.Convert(expressions[i], typeof(object)); + NewExpression parameter = Expression.New(constructorForKeyValuePair, (Expression)Expression.Constant(properties[i].Name), boxingExpression); - arrayIndexParams.Add(parameter); - } + arrayIndexParams.Add(parameter); + } - // Create an expression tree that represents creating and initializing a one-dimensional array of type KeyValuePair. - NewArrayExpression newArrayExpression = Expression.NewArrayInit(typeof(KeyValuePair), arrayIndexParams); + // Create an expression tree that represents creating and initializing a one-dimensional array of type KeyValuePair. + NewArrayExpression newArrayExpression = Expression.NewArrayInit(typeof(KeyValuePair), arrayIndexParams); - // Get the "public DynamicClass(KeyValuePair[] propertylist)" constructor + // Get the "public DynamicClass(KeyValuePair[] propertylist)" constructor #if NET35 || NET40 ConstructorInfo constructor = type.GetConstructors().First(); #else - ConstructorInfo constructor = type.GetTypeInfo().DeclaredConstructors.First(); + ConstructorInfo constructor = type.GetTypeInfo().DeclaredConstructors.First(); #endif - return Expression.New(constructor, newArrayExpression); + return Expression.New(constructor, newArrayExpression); #if !UAP10_0 } diff --git a/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs index c4ac4c4d..e51170e8 100644 --- a/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs @@ -27,5 +27,7 @@ internal interface IExpressionHelper void OptimizeForEqualityIfPossible(ref Expression left, ref Expression right); Expression OptimizeStringForEqualityIfPossible(string text, Type type); + + Expression GenerateAndAlsoMemberExpression(Expression expression); } } diff --git a/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs b/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs index bad40994..bd6b26ff 100644 --- a/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs @@ -14,6 +14,7 @@ internal class KeywordsHelper public const string KEYWORD_IIF = "iif"; public const string KEYWORD_NEW = "new"; public const string KEYWORD_ISNULL = "isnull"; + public const string KEYWORD_NULLPROPAGATION = "np"; private readonly IDictionary _keywords = new Dictionary(StringComparer.OrdinalIgnoreCase) { @@ -37,6 +38,7 @@ public KeywordsHelper(ParsingConfig config) _keywords.Add(KEYWORD_IIF, KEYWORD_IIF); _keywords.Add(KEYWORD_NEW, KEYWORD_NEW); _keywords.Add(KEYWORD_ISNULL, KEYWORD_ISNULL); + _keywords.Add(KEYWORD_NULLPROPAGATION, KEYWORD_NULLPROPAGATION); foreach (Type type in PredefinedTypesHelper.PredefinedTypes.OrderBy(kvp => kvp.Value).Select(kvp => kvp.Key)) { diff --git a/src/System.Linq.Dynamic.Core/Res.cs b/src/System.Linq.Dynamic.Core/Res.cs index fd0d475b..cff1a949 100644 --- a/src/System.Linq.Dynamic.Core/Res.cs +++ b/src/System.Linq.Dynamic.Core/Res.cs @@ -49,6 +49,8 @@ internal static class Res public const string NoMatchingConstructor = "No matching constructor in type '{0}'"; public const string NoParentInScope = "No 'parent' is in scope"; public const string NoRootInScope = "No 'root' is in scope"; + public const string NullPropagationRequiresCorrectArgs = "The 'np' (null-propagation) function requires 1 or 2 arguments"; + public const string NullPropagationRequiresMemberExpression = "The 'np' (null-propagation) function requires the first argument to be a MemberExpression"; public const string OpenBracketExpected = "'[' expected"; public const string OpenCurlyParenExpected = "'{' expected"; public const string OpenParenExpected = "'(' expected"; From 675a60eae98e7bb547c55e9a3e4aa3a0ff703cda Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 14 Nov 2018 15:53:32 +0100 Subject: [PATCH 2/6] tests --- .../ExpressionTests.cs | 66 ++++++++----------- .../Parser/ExpressionHelperTests.cs | 33 +++++++++- 2 files changed, 58 insertions(+), 41 deletions(-) diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs index 255f8135..baa05c8d 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs @@ -4,7 +4,6 @@ using System.Linq.Dynamic.Core.Exceptions; using System.Linq.Dynamic.Core.Tests.Helpers; using System.Linq.Dynamic.Core.Tests.Helpers.Models; -using System.Text; using Newtonsoft.Json.Linq; using Xunit; using NFluent; @@ -1258,57 +1257,46 @@ public void ExpressionTests_NullCoalescing() Assert.Equal(expectedResult3, result3b.ToDynamicArray()); } - // [Fact] - public void ExpressionTests_NullPropagating() + [Fact] + public void ExpressionTests_NullPropagating_Null() { // Arrange var testModels = User.GenerateSampleModels(1, true); testModels[0].Profile = null; - string q = "Profile?.UserProfileDetails?.Id > 0 || Profile?.UserProfileDetails?.Id2 > 0"; - string t = X(q); + // Act + var result = testModels.AsQueryable().Select(t => t.Profile != null && t.Profile.UserProfileDetails != null ? (long?)t.Profile.UserProfileDetails.Id : null).ToArray(); + var resultDynamic = testModels.AsQueryable().Select("np(it.Profile.UserProfileDetails.Id)").ToDynamicArray(); + + // Assert + Check.That(resultDynamic).ContainsExactly(result); + } + + [Fact] + public void ExpressionTests_NullPropagating_Value() + { + // Arrange + var testModels = User.GenerateSampleModels(1, true); + testModels[0].Profile = null; // Act - // (Profile != null ? (Profile.UserProfileDetails != null ? Profile.UserProfileDetails.Id : null) : null) - var result = testModels.AsQueryable().Where(t); + var result = testModels.AsQueryable().Select(t => t.Profile != null && t.Profile.UserProfileDetails != null ? t.Profile.UserProfileDetails.Id : 100).ToArray(); + var resultDynamic = testModels.AsQueryable().Select("np(it.Profile.UserProfileDetails.Id, 100)").ToDynamicArray(); // Assert - Check.That(result).IsNull(); + Check.That(resultDynamic).ContainsExactly(result); } - private string X(string text) + [Fact] + public void ExpressionTests_NullPropagating_ThrowsException() { - var newText = new List(); - string[] spaceParts = text.Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries); - foreach (string spacePart in spaceParts) - { - string[] parts = spacePart.Split(new[] { "?." }, StringSplitOptions.RemoveEmptyEntries); - if (parts.Length > 1) - { - var list = new List(); - for (int i = 0; i < parts.Length; i++) - { - list.Add(string.Join(".", parts.Take(parts.Length - i))); - } - - var stringBuilder = new StringBuilder(); - list.Reverse(); - for (int i = 0; i < list.Count - 1; i++) - { - stringBuilder.Append($"({list[i]} != null ? "); - } - - stringBuilder.Append($"({list.Last()}) {string.Concat(Enumerable.Repeat(" : null)", parts.Length - 1))}"); - - newText.Add(stringBuilder.ToString()); - } - else - { - newText.Add(spacePart); - } - } + // Arrange + var q = User.GenerateSampleModels(1, true).AsQueryable(); - return string.Join(" ", newText); + // Act + Check.ThatCode(() => q.Select("np()")).Throws(); + Check.ThatCode(() => q.Select("np(it.Profile.UserProfileDetails.Id, 1, 2)")).Throws(); + Check.ThatCode(() => q.Select("np(1)")).Throws(); } [Fact] diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionHelperTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionHelperTests.cs index c29ef4c6..88edfad2 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionHelperTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionHelperTests.cs @@ -1,5 +1,4 @@ -using System.Linq.Dynamic.Core.Parser; -using System.Linq.Expressions; +using System.Linq.Expressions; using NFluent; using Xunit; @@ -40,5 +39,35 @@ public void ExpressionHelper_OptimizeStringForEqualityIfPossible_Guid_Invalid() // Assert Check.That(result).IsNull(); } + + [Fact] + public void ExpressionHelper_GenerateAndAlsoMemberExpression() + { + // Assign + Expression> expression = (x) => x.Relation1.Relation2.Id; + + // Act + Expression result = _expressionHelper.GenerateAndAlsoMemberExpression(expression); + + // Assert + Check.That(result.ToString()).IsEqualTo("((x.Relation1 != null) AndAlso (x.Relation1.Relation2 != null))"); + } + + class Item + { + public int Id { get; set; } + public Relation1 Relation1 { get; set; } + } + + class Relation1 + { + public int Id { get; set; } + public Relation2 Relation2 { get; set; } + } + + class Relation2 + { + public int Id { get; set; } + } } } From 1ec053e3a402330fc9272dee9502bbfc625debe6 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Thu, 15 Nov 2018 12:13:59 +0100 Subject: [PATCH 3/6] refactor --- .../Parser/ExpressionHelper.cs | 60 +++++++++++-------- .../Parser/ExpressionParser.cs | 53 ++++++++-------- .../Parser/IExpressionHelper.cs | 2 +- .../Parser/KeywordsHelper.cs | 18 +++--- .../ExpressionTests.cs | 14 +++-- .../Parser/ExpressionHelperTests.cs | 6 +- 6 files changed, 87 insertions(+), 66 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs index 1ec8f6d6..949aad14 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs @@ -236,24 +236,23 @@ private void WrapConstantExpressions(ref Expression left, ref Expression right) } } - public Expression GenerateAndAlsoMemberExpression(Expression expression) + public Expression GenerateAndAlsoNotNullExpression(Expression sourceExpression) { - var memberExpresssions = GetAllMemberExpressions(expression); - if (!memberExpresssions.Any()) + var expresssions = CollectExpressions(sourceExpression); + if (!expresssions.Any()) { return null; } - memberExpresssions.Reverse(); + // Reverse the list + expresssions.Reverse(); - var binaryExpressions = new List(); - foreach (var memberExpresssion in memberExpresssions) - { - binaryExpressions.Add(Expression.NotEqual(memberExpresssion, Constants.NullLiteral)); - } + // Convert all expressions into '!= null' + var binaryExpressions = expresssions.Select(expression => Expression.NotEqual(expression, Constants.NullLiteral)).ToArray(); + // Convert all binary expressions into `AndAlso(...)` var andAlsoExpression = binaryExpressions[0]; - for (int i = 1; i < binaryExpressions.Count; i++) + for (int i = 1; i < binaryExpressions.Length; i++) { andAlsoExpression = Expression.AndAlso(andAlsoExpression, binaryExpressions[i]); } @@ -261,40 +260,53 @@ public Expression GenerateAndAlsoMemberExpression(Expression expression) return andAlsoExpression; } - private static MemberExpression GetMemberExpression(Expression expression) + private static Expression GetMemberExpression(Expression expression) { - if (expression is MemberExpression) + if (expression is ParameterExpression parameterExpression) + { + return parameterExpression; + } + + if (expression is MemberExpression memberExpression) { - return (MemberExpression)expression; + return memberExpression; } - else if (expression is LambdaExpression lambdaExpression) + + if (expression is LambdaExpression lambdaExpression) { - if (lambdaExpression.Body is MemberExpression memberExpression) + if (lambdaExpression.Body is MemberExpression bodyAsMemberExpression) { - return memberExpression; + return bodyAsMemberExpression; } - else if (lambdaExpression.Body is UnaryExpression unaryExpression) + + if (lambdaExpression.Body is UnaryExpression bodyAsunaryExpression) { - return (MemberExpression)unaryExpression.Operand; + return bodyAsunaryExpression.Operand; } } + return null; } - private static List GetAllMemberExpressions(Expression expression) + private static List CollectExpressions(Expression sourceExpression) { var list = new List(); - var memberExpression = GetMemberExpression(expression); + Expression expression = GetMemberExpression(sourceExpression); - while (memberExpression != null) + while (expression is MemberExpression memberExpression) { - memberExpression = GetMemberExpression(memberExpression.Expression); - if (memberExpression != null) + expression = GetMemberExpression(memberExpression.Expression); + if (expression is MemberExpression) { - list.Add(memberExpression); + list.Add(expression); } } + if (expression is ParameterExpression) + { + list.Add(expression); + } + return list; } } diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 94a061ef..f7354132 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -240,8 +240,8 @@ Expression ParseLambdaOperator() return expr; } - // isnull(a,b) operator - Expression ParseIsNull() + // isnull(a,b) function + Expression ParseFunctionIsNull() { int errorPos = _textParser.CurrentToken.Pos; _textParser.NextToken(); @@ -947,7 +947,7 @@ Expression ParseIdentifier() if (_keywordsHelper.TryGetValue(_textParser.CurrentToken.Text, out object value)) { - var typeValue = value as Type; + Type typeValue = value as Type; if (typeValue != null) { return ParseTypeAccess(typeValue); @@ -956,13 +956,15 @@ Expression ParseIdentifier() if (value == (object)KeywordsHelper.KEYWORD_IT) return ParseIt(); if (value == (object)KeywordsHelper.KEYWORD_PARENT) return ParseParent(); if (value == (object)KeywordsHelper.KEYWORD_ROOT) return ParseRoot(); + if (value == (object)KeywordsHelper.SYMBOL_IT) return ParseIt(); if (value == (object)KeywordsHelper.SYMBOL_PARENT) return ParseParent(); if (value == (object)KeywordsHelper.SYMBOL_ROOT) return ParseRoot(); - if (value == (object)KeywordsHelper.KEYWORD_IIF) return ParseIif(); - if (value == (object)KeywordsHelper.KEYWORD_NEW) return ParseNew(); - if (value == (object)KeywordsHelper.KEYWORD_ISNULL) return ParseIsNull(); - if (value == (object)KeywordsHelper.KEYWORD_NULLPROPAGATION) return ParseNullPropagation(); + + if (value == (object)KeywordsHelper.FUNCTION_IIF) return ParseFunctionIif(); + if (value == (object)KeywordsHelper.FUNCTION_ISNULL) return ParseFunctionIsNull(); + if (value == (object)KeywordsHelper.FUNCTION_NEW) return ParseNew(); + if (value == (object)KeywordsHelper.FUNCTION_NULLPROPAGATION) return ParseFunctionNullPropagation(); _textParser.NextToken(); @@ -1030,7 +1032,8 @@ Expression ParseRoot() return _root; } - Expression ParseIif() + // iif(test, ifTrue, ifFalse) function + Expression ParseFunctionIif() { int errorPos = _textParser.CurrentToken.Pos; _textParser.NextToken(); @@ -1044,7 +1047,8 @@ Expression ParseIif() return GenerateConditional(args[0], args[1], args[2], errorPos); } - Expression ParseNullPropagation() + // np(...) function + Expression ParseFunctionNullPropagation() { int errorPos = _textParser.CurrentToken.Pos; _textParser.NextToken(); @@ -1058,7 +1062,7 @@ Expression ParseNullPropagation() if (args[0] is MemberExpression memberExpression) { - var expressionTest = _expressionHelper.GenerateAndAlsoMemberExpression(memberExpression); + var expressionTest = _expressionHelper.GenerateAndAlsoNotNullExpression(memberExpression); var expressionIfTrue = memberExpression; var expressionIfFalse = args.Length == 2 ? args[1] : Constants.NullLiteral; @@ -1128,6 +1132,7 @@ Expression GenerateConditional(Expression test, Expression expr1, Expression exp return Expression.Condition(test, expr1, expr2); } + // new (...) function Expression ParseNew() { _textParser.NextToken(); @@ -1260,33 +1265,33 @@ private Expression CreateNewExpression(List properties, List); + type = typeof(DynamicClass); + Type typeForKeyValuePair = typeof(KeyValuePair); #if NET35 || NET40 ConstructorInfo constructorForKeyValuePair = typeForKeyValuePair.GetConstructors().First(); #else ConstructorInfo constructorForKeyValuePair = typeForKeyValuePair.GetTypeInfo().DeclaredConstructors.First(); #endif - var arrayIndexParams = new List(); - for (int i = 0; i < expressions.Count; i++) - { - // Just convert the expression always to an object expression. - UnaryExpression boxingExpression = Expression.Convert(expressions[i], typeof(object)); - NewExpression parameter = Expression.New(constructorForKeyValuePair, (Expression)Expression.Constant(properties[i].Name), boxingExpression); + var arrayIndexParams = new List(); + for (int i = 0; i < expressions.Count; i++) + { + // Just convert the expression always to an object expression. + UnaryExpression boxingExpression = Expression.Convert(expressions[i], typeof(object)); + NewExpression parameter = Expression.New(constructorForKeyValuePair, (Expression)Expression.Constant(properties[i].Name), boxingExpression); - arrayIndexParams.Add(parameter); - } + arrayIndexParams.Add(parameter); + } - // Create an expression tree that represents creating and initializing a one-dimensional array of type KeyValuePair. - NewArrayExpression newArrayExpression = Expression.NewArrayInit(typeof(KeyValuePair), arrayIndexParams); + // Create an expression tree that represents creating and initializing a one-dimensional array of type KeyValuePair. + NewArrayExpression newArrayExpression = Expression.NewArrayInit(typeof(KeyValuePair), arrayIndexParams); - // Get the "public DynamicClass(KeyValuePair[] propertylist)" constructor + // Get the "public DynamicClass(KeyValuePair[] propertylist)" constructor #if NET35 || NET40 ConstructorInfo constructor = type.GetConstructors().First(); #else ConstructorInfo constructor = type.GetTypeInfo().DeclaredConstructors.First(); #endif - return Expression.New(constructor, newArrayExpression); + return Expression.New(constructor, newArrayExpression); #if !UAP10_0 } diff --git a/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs index e51170e8..a43a9aa2 100644 --- a/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs @@ -28,6 +28,6 @@ internal interface IExpressionHelper Expression OptimizeStringForEqualityIfPossible(string text, Type type); - Expression GenerateAndAlsoMemberExpression(Expression expression); + Expression GenerateAndAlsoNotNullExpression(Expression sourceExpression); } } diff --git a/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs b/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs index bd6b26ff..01b8e716 100644 --- a/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs @@ -11,10 +11,11 @@ internal class KeywordsHelper public const string KEYWORD_IT = "it"; public const string KEYWORD_PARENT = "parent"; public const string KEYWORD_ROOT = "root"; - public const string KEYWORD_IIF = "iif"; - public const string KEYWORD_NEW = "new"; - public const string KEYWORD_ISNULL = "isnull"; - public const string KEYWORD_NULLPROPAGATION = "np"; + + public const string FUNCTION_IIF = "iif"; + public const string FUNCTION_ISNULL = "isnull"; + public const string FUNCTION_NEW = "new"; + public const string FUNCTION_NULLPROPAGATION = "np"; private readonly IDictionary _keywords = new Dictionary(StringComparer.OrdinalIgnoreCase) { @@ -35,10 +36,11 @@ public KeywordsHelper(ParsingConfig config) _keywords.Add(SYMBOL_IT, SYMBOL_IT); _keywords.Add(SYMBOL_PARENT, SYMBOL_PARENT); _keywords.Add(SYMBOL_ROOT, SYMBOL_ROOT); - _keywords.Add(KEYWORD_IIF, KEYWORD_IIF); - _keywords.Add(KEYWORD_NEW, KEYWORD_NEW); - _keywords.Add(KEYWORD_ISNULL, KEYWORD_ISNULL); - _keywords.Add(KEYWORD_NULLPROPAGATION, KEYWORD_NULLPROPAGATION); + + _keywords.Add(FUNCTION_IIF, FUNCTION_IIF); + _keywords.Add(FUNCTION_ISNULL, FUNCTION_ISNULL); + _keywords.Add(FUNCTION_NEW, FUNCTION_NEW); + _keywords.Add(FUNCTION_NULLPROPAGATION, FUNCTION_NULLPROPAGATION); foreach (Type type in PredefinedTypesHelper.PredefinedTypes.OrderBy(kvp => kvp.Value).Select(kvp => kvp.Key)) { diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs index baa05c8d..f8ec17c5 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs @@ -1261,11 +1261,12 @@ public void ExpressionTests_NullCoalescing() public void ExpressionTests_NullPropagating_Null() { // Arrange - var testModels = User.GenerateSampleModels(1, true); - testModels[0].Profile = null; + var testModels = User.GenerateSampleModels(2, true).ToList(); + testModels.Add(null); // Add null User + testModels[0].Profile = null; // Set the Profile to null for first User // Act - var result = testModels.AsQueryable().Select(t => t.Profile != null && t.Profile.UserProfileDetails != null ? (long?)t.Profile.UserProfileDetails.Id : null).ToArray(); + var result = testModels.AsQueryable().Select(t => t != null && t.Profile != null && t.Profile.UserProfileDetails != null ? (long?)t.Profile.UserProfileDetails.Id : null).ToArray(); var resultDynamic = testModels.AsQueryable().Select("np(it.Profile.UserProfileDetails.Id)").ToDynamicArray(); // Assert @@ -1276,11 +1277,12 @@ public void ExpressionTests_NullPropagating_Null() public void ExpressionTests_NullPropagating_Value() { // Arrange - var testModels = User.GenerateSampleModels(1, true); - testModels[0].Profile = null; + var testModels = User.GenerateSampleModels(2, true).ToList(); + testModels.Add(null); // Add null User + testModels[0].Profile = null; // Set the Profile to null for first User // Act - var result = testModels.AsQueryable().Select(t => t.Profile != null && t.Profile.UserProfileDetails != null ? t.Profile.UserProfileDetails.Id : 100).ToArray(); + var result = testModels.AsQueryable().Select(t => t != null && t.Profile != null && t.Profile.UserProfileDetails != null ? t.Profile.UserProfileDetails.Id : 100).ToArray(); var resultDynamic = testModels.AsQueryable().Select("np(it.Profile.UserProfileDetails.Id, 100)").ToDynamicArray(); // Assert diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionHelperTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionHelperTests.cs index 88edfad2..7a95a440 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionHelperTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionHelperTests.cs @@ -41,16 +41,16 @@ public void ExpressionHelper_OptimizeStringForEqualityIfPossible_Guid_Invalid() } [Fact] - public void ExpressionHelper_GenerateAndAlsoMemberExpression() + public void ExpressionHelper_GenerateAndAlsoNotNullExpression() { // Assign Expression> expression = (x) => x.Relation1.Relation2.Id; // Act - Expression result = _expressionHelper.GenerateAndAlsoMemberExpression(expression); + Expression result = _expressionHelper.GenerateAndAlsoNotNullExpression(expression); // Assert - Check.That(result.ToString()).IsEqualTo("((x.Relation1 != null) AndAlso (x.Relation1.Relation2 != null))"); + Check.That(result.ToString()).IsEqualTo("(((x != null) AndAlso (x.Relation1 != null)) AndAlso (x.Relation1.Relation2 != null))"); } class Item From 37649ede05d1ee4942fc69e2aa3addfd98f578f6 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 5 Jan 2019 13:11:10 +0100 Subject: [PATCH 4/6] 1.0.10 --- Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index caaa013e..7cee1f75 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ - 1.0.9.1 + 1.0.10 @@ -16,4 +16,4 @@ - \ No newline at end of file + From f80a3d6ecb2481524ff35c84cf25bcb60215fd2c Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Tue, 22 Jan 2019 11:32:07 +0100 Subject: [PATCH 5/6] update test --- src-console/ConsoleAppEF2.0.2_InMemory/Program.cs | 3 +++ src-console/ConsoleAppEF2.0/Database/Car.cs | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/src-console/ConsoleAppEF2.0.2_InMemory/Program.cs b/src-console/ConsoleAppEF2.0.2_InMemory/Program.cs index d7b07419..e603e1b8 100644 --- a/src-console/ConsoleAppEF2.0.2_InMemory/Program.cs +++ b/src-console/ConsoleAppEF2.0.2_InMemory/Program.cs @@ -83,6 +83,9 @@ static void Main(string[] args) context.Cars.Add(new Car { Brand = "Alfa", Color = "Black", Vin = "a%bc", Year = "1979", DateLastModified = dateLastModified.AddDays(3) }); context.SaveChanges(); + var methodTest = context.Cars.Select("it.X(true, \"tst\").Contains(\"Blue\")"); + Console.WriteLine("methodTest {0}", JsonConvert.SerializeObject(methodTest, Formatting.Indented)); + var carSingleOrDefault = context.Cars.SingleOrDefault(config, "Brand = \"Ford\""); Console.WriteLine("carSingleOrDefault {0}", JsonConvert.SerializeObject(carSingleOrDefault, Formatting.Indented)); diff --git a/src-console/ConsoleAppEF2.0/Database/Car.cs b/src-console/ConsoleAppEF2.0/Database/Car.cs index 64dc218b..34599543 100644 --- a/src-console/ConsoleAppEF2.0/Database/Car.cs +++ b/src-console/ConsoleAppEF2.0/Database/Car.cs @@ -23,5 +23,10 @@ public class Car [Required] public DateTime DateLastModified { get; set; } + + public string X(bool b, string s) + { + return b + s + Color; + } } } From 7c4bfafe6301553b5b915a1dc54256d777a40ff5 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Tue, 22 Jan 2019 12:48:45 +0100 Subject: [PATCH 6/6] UserShares --- .../ConsoleAppEF2.0.2_InMemory/Program.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src-console/ConsoleAppEF2.0.2_InMemory/Program.cs b/src-console/ConsoleAppEF2.0.2_InMemory/Program.cs index e603e1b8..5dbd40ea 100644 --- a/src-console/ConsoleAppEF2.0.2_InMemory/Program.cs +++ b/src-console/ConsoleAppEF2.0.2_InMemory/Program.cs @@ -11,6 +11,16 @@ namespace ConsoleAppEF2 { class Program { + class User + { + public string Name { get; set; } + + public string GetDisplayName(bool a, bool b, bool c) + { + return Name + "GetDisplayName"; + } + } + class C : AbstractDynamicLinqCustomTypeProvider, IDynamicLinkCustomTypeProvider { public HashSet GetCustomTypes() @@ -69,6 +79,15 @@ static void Main(string[] args) }; Console.WriteLine("all {0}", JsonConvert.SerializeObject(all, Formatting.Indented)); + var projects = new[] + { + new { UserShares = new [] { new User { Name = "John" } } } + }.AsQueryable(); + + var filter = "UserShares.Any(GetDisplayName(true,true,false).Contains(\"John\"))"; + var filtered = projects.Where(filter); + Console.WriteLine("filtered {0}", JsonConvert.SerializeObject(filtered, Formatting.Indented)); + var config = new ParsingConfig { CustomTypeProvider = new C()