diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs index 338b82d1..8e175065 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs @@ -270,21 +270,16 @@ public bool TryGenerateAndAlsoNotNullExpression(Expression sourceExpression, boo return true; } - private static Expression GetMemberExpression(Expression expression) + public bool ExpressionQualifiesForNullPropagation(Expression expression) { - if (expression is MemberExpression memberExpression) - { - return memberExpression; - } - - if (expression is ParameterExpression parameterExpression) - { - return parameterExpression; - } + return expression is MemberExpression || expression is ParameterExpression || expression is MethodCallExpression; + } - if (expression is MethodCallExpression methodCallExpression) + private Expression GetMemberExpression(Expression expression) + { + if (ExpressionQualifiesForNullPropagation(expression)) { - return methodCallExpression; + return expression; } if (expression is LambdaExpression lambdaExpression) @@ -303,7 +298,7 @@ private static Expression GetMemberExpression(Expression expression) return null; } - private static List CollectExpressions(bool addSelf, Expression sourceExpression) + private List CollectExpressions(bool addSelf, Expression sourceExpression) { Expression expression = GetMemberExpression(sourceExpression); @@ -317,24 +312,31 @@ private static List CollectExpressions(bool addSelf, Expression sour } } - while (expression is MemberExpression memberExpression) + bool expressionRecognized; + do { - expression = GetMemberExpression(memberExpression.Expression); - if (expression is MemberExpression) + switch (expression) { - list.Add(expression); + case MemberExpression memberExpression: + expression = GetMemberExpression(memberExpression.Expression); + expressionRecognized = true; + break; + + case MethodCallExpression methodCallExpression: + expression = methodCallExpression.Arguments.First(); + expressionRecognized = true; + break; + + default: + expressionRecognized = false; + break; } - } - if (expression is ParameterExpression) - { - list.Add(expression); - } - - if (expression is MethodCallExpression) - { - list.Add(expression); - } + if (expressionRecognized && ExpressionQualifiesForNullPropagation(expression)) + { + list.Add(expression); + } + } while (expressionRecognized); return list; } diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index e5b6ba94..7fae3072 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -1135,20 +1135,20 @@ Expression ParseFunctionNullPropagation() throw ParseError(errorPos, Res.NullPropagationRequiresCorrectArgs); } - if (args[0] is MemberExpression memberExpression) + if (_expressionHelper.ExpressionQualifiesForNullPropagation(args[0])) { bool hasDefaultParameter = args.Length == 2; Expression expressionIfFalse = hasDefaultParameter ? args[1] : Expression.Constant(null); - if (_expressionHelper.TryGenerateAndAlsoNotNullExpression(memberExpression, hasDefaultParameter, out Expression generatedExpression)) + if (_expressionHelper.TryGenerateAndAlsoNotNullExpression(args[0], hasDefaultParameter, out Expression generatedExpression)) { - return GenerateConditional(generatedExpression, memberExpression, expressionIfFalse, errorPos); + return GenerateConditional(generatedExpression, args[0], expressionIfFalse, errorPos); } - return memberExpression; + return args[0]; } - throw ParseError(errorPos, Res.NullPropagationRequiresMemberExpression); + throw ParseError(errorPos, Res.NullPropagationRequiresValidExpression); } // Is(...) function diff --git a/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs index 1bbf127e..e91ab513 100644 --- a/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs @@ -30,6 +30,8 @@ internal interface IExpressionHelper bool TryGenerateAndAlsoNotNullExpression(Expression sourceExpression, bool addSelf, out Expression generatedExpression); + bool ExpressionQualifiesForNullPropagation(Expression expression); + void WrapConstantExpression(ref Expression argument); } } diff --git a/src/System.Linq.Dynamic.Core/Res.cs b/src/System.Linq.Dynamic.Core/Res.cs index 98bcbe00..619e7f7b 100644 --- a/src/System.Linq.Dynamic.Core/Res.cs +++ b/src/System.Linq.Dynamic.Core/Res.cs @@ -52,7 +52,7 @@ internal static class Res 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 NullPropagationRequiresValidExpression = "The 'np' (null-propagation) function requires the first argument to be a MemberExpression, ParameterExpression or MethodCallExpression"; public const string OpenBracketExpected = "'[' expected"; public const string OpenCurlyParenExpected = "'{' expected"; public const string OpenParenExpected = "'(' expected"; diff --git a/test/EntityFramework.DynamicLinq.Tests.net452/EntityFramework.DynamicLinq.Tests.net452.csproj b/test/EntityFramework.DynamicLinq.Tests.net452/EntityFramework.DynamicLinq.Tests.net452.csproj index d713a80b..90221732 100644 --- a/test/EntityFramework.DynamicLinq.Tests.net452/EntityFramework.DynamicLinq.Tests.net452.csproj +++ b/test/EntityFramework.DynamicLinq.Tests.net452/EntityFramework.DynamicLinq.Tests.net452.csproj @@ -43,6 +43,9 @@ ..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll + + ..\..\packages\FluentAssertions.5.10.3\lib\net45\FluentAssertions.dll + ..\..\packages\Linq.PropertyTranslator.Core.1.0.3.0\lib\net452\Linq.PropertyTranslator.Core.dll diff --git a/test/EntityFramework.DynamicLinq.Tests.net452/packages.config b/test/EntityFramework.DynamicLinq.Tests.net452/packages.config index 3148cfef..28822b08 100644 --- a/test/EntityFramework.DynamicLinq.Tests.net452/packages.config +++ b/test/EntityFramework.DynamicLinq.Tests.net452/packages.config @@ -2,6 +2,7 @@ + diff --git a/test/EntityFramework.DynamicLinq.Tests/EntityFramework.DynamicLinq.Tests.csproj b/test/EntityFramework.DynamicLinq.Tests/EntityFramework.DynamicLinq.Tests.csproj index c20bb237..7ea0b8d0 100644 --- a/test/EntityFramework.DynamicLinq.Tests/EntityFramework.DynamicLinq.Tests.csproj +++ b/test/EntityFramework.DynamicLinq.Tests/EntityFramework.DynamicLinq.Tests.csproj @@ -32,6 +32,7 @@ + diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs index f3e27d3e..3a8efcf5 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs @@ -5,6 +5,7 @@ using System.Linq.Dynamic.Core.Tests.Helpers.Models; using System.Linq.Expressions; using System.Reflection; +using FluentAssertions; using Xunit; using User = System.Linq.Dynamic.Core.Tests.Helpers.Models.User; @@ -14,10 +15,18 @@ public class DynamicExpressionParserTests { private class MyClass { + public List MyStrings { get; set; } + + public List MyClasses { get; set; } + public int Foo() { return 42; } + + public string Name { get; set; } + + public MyClass Child { get; set; } } private class ComplexParseLambda1Result @@ -1044,5 +1053,39 @@ public void DynamicExpressionParser_ParseLambda_SupportEnumerationStringComparis // Assert Check.That(result).IsEqualTo(expectedResult); } + + [Fact] + public void DynamicExpressionParser_ParseLambda_NullPropagation_MethodCallExpression() + { + // Arrange + var dataSource = new MyClass(); + + var expressionText = "np(MyClasses.FirstOrDefault())"; + + // Act + LambdaExpression expression = DynamicExpressionParser.ParseLambda(ParsingConfig.Default, dataSource.GetType(), typeof(MyClass), expressionText); + Delegate del = expression.Compile(); + MyClass result = del.DynamicInvoke(dataSource) as MyClass; + + // Assert + result.Should().BeNull(); + } + + [Theory] + [InlineData("np(MyClasses.FirstOrDefault().Name)")] + [InlineData("np(MyClasses.FirstOrDefault(Name == \"a\").Name)")] + public void DynamicExpressionParser_ParseLambda_NullPropagation_MethodCallExpression_With_Property(string expressionText) + { + // Arrange + var dataSource = new MyClass(); + + // Act + LambdaExpression expression = DynamicExpressionParser.ParseLambda(ParsingConfig.Default, dataSource.GetType(), typeof(string), expressionText); + Delegate del = expression.Compile(); + string result = del.DynamicInvoke(dataSource) as string; + + // Assert + result.Should().BeNull(); + } } } diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs index f94e0be9..86281489 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs @@ -1343,6 +1343,8 @@ public void ExpressionTests_NullCoalescing() [InlineData("np(nested.nullablenumber, 42)", "Select(Param_0 => IIF((((Param_0 != null) AndAlso (Param_0.nested != null)) AndAlso (Param_0.nested.nullablenumber != null)), Param_0.nested.nullablenumber, 42))")] [InlineData("np(nested._enumnullable)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.nested != null)), Param_0.nested._enumnullable, null))")] [InlineData("np(item.GuidNull)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.item != null)), Param_0.item.GuidNull, null))")] + [InlineData("np(items.FirstOrDefault())", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.items != null)), Param_0.items.FirstOrDefault(), null))")] + [InlineData("np(items.FirstOrDefault(it != \"x\"))", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.items != null)), Param_0.items.FirstOrDefault(Param_1 => (Param_1 != \"x\")), null))")] public void ExpressionTests_NullPropagating(string test, string query) { // Arrange @@ -1378,7 +1380,8 @@ public void ExpressionTests_NullPropagating(string test, string query) Id = 100, GuidNormal = Guid.NewGuid(), GuidNull = Guid.NewGuid() - } + }, + items = new [] { "a", "b" } } }.AsQueryable();