diff --git a/Directory.Build.props b/Directory.Build.props index 44c947a2..7cee1f75 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ - 1.0.9.2 + 1.0.10 diff --git a/src-console/ConsoleAppEF2.0.2_InMemory/Program.cs b/src-console/ConsoleAppEF2.0.2_InMemory/Program.cs index d7b07419..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() @@ -83,6 +102,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; + } } } 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..949aad14 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,79 @@ private void WrapConstantExpressions(ref Expression left, ref Expression right) _constantExpressionWrapper.Wrap(ref right); } } + + public Expression GenerateAndAlsoNotNullExpression(Expression sourceExpression) + { + var expresssions = CollectExpressions(sourceExpression); + if (!expresssions.Any()) + { + return null; + } + + // Reverse the list + expresssions.Reverse(); + + // 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.Length; i++) + { + andAlsoExpression = Expression.AndAlso(andAlsoExpression, binaryExpressions[i]); + } + + return andAlsoExpression; + } + + private static Expression GetMemberExpression(Expression expression) + { + if (expression is ParameterExpression parameterExpression) + { + return parameterExpression; + } + + if (expression is MemberExpression memberExpression) + { + return memberExpression; + } + + if (expression is LambdaExpression lambdaExpression) + { + if (lambdaExpression.Body is MemberExpression bodyAsMemberExpression) + { + return bodyAsMemberExpression; + } + + if (lambdaExpression.Body is UnaryExpression bodyAsunaryExpression) + { + return bodyAsunaryExpression.Operand; + } + } + + return null; + } + + private static List CollectExpressions(Expression sourceExpression) + { + var list = new List(); + Expression expression = GetMemberExpression(sourceExpression); + + while (expression is MemberExpression memberExpression) + { + expression = GetMemberExpression(memberExpression.Expression); + if (expression is 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 66b0280b..2217fb8d 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() { @@ -257,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(); @@ -964,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); @@ -973,12 +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.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(); @@ -1046,10 +1032,12 @@ Expression ParseRoot() return _root; } - Expression ParseIif() + // iif(test, ifTrue, ifFalse) function + Expression ParseFunctionIif() { int errorPos = _textParser.CurrentToken.Pos; _textParser.NextToken(); + Expression[] args = ParseArgumentList(); if (args.Length != 3) { @@ -1059,6 +1047,31 @@ Expression ParseIif() return GenerateConditional(args[0], args[1], args[2], errorPos); } + // np(...) function + Expression ParseFunctionNullPropagation() + { + 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.GenerateAndAlsoNotNullExpression(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)) @@ -1119,6 +1132,7 @@ Expression GenerateConditional(Expression test, Expression expr1, Expression exp return Expression.Condition(test, expr1, expr2); } + // new (...) function Expression ParseNew() { _textParser.NextToken(); @@ -1256,7 +1270,7 @@ private Expression CreateNewExpression(List properties, List(); for (int i = 0; i < expressions.Count; i++) @@ -1275,7 +1289,7 @@ private Expression CreateNewExpression(List properties, List _keywords = new Dictionary(StringComparer.OrdinalIgnoreCase) { @@ -34,9 +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(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/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"; diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs index ba94a7a9..7d592219 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; @@ -1257,57 +1256,48 @@ 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; + 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 != 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(); - string q = "Profile?.UserProfileDetails?.Id > 0 || Profile?.UserProfileDetails?.Id2 > 0"; - string t = X(q); + // Assert + Check.That(resultDynamic).ContainsExactly(result); + } + + [Fact] + public void ExpressionTests_NullPropagating_Value() + { + // Arrange + 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 - // (Profile != null ? (Profile.UserProfileDetails != null ? Profile.UserProfileDetails.Id : null) : null) - var result = testModels.AsQueryable().Where(t); + 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 - 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..7a95a440 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_GenerateAndAlsoNotNullExpression() + { + // Assign + Expression> expression = (x) => x.Relation1.Relation2.Id; + + // Act + Expression result = _expressionHelper.GenerateAndAlsoNotNullExpression(expression); + + // Assert + Check.That(result.ToString()).IsEqualTo("(((x != null) AndAlso (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; } + } } }