Skip to content

Commit 8919415

Browse files
authored
Support MethodCalls in NullPropagation function : np(...) (#368)
* Update docfx * NP : Support MethodCallExpression * more tests * tests
1 parent f6adade commit 8919415

File tree

9 files changed

+89
-34
lines changed

9 files changed

+89
-34
lines changed

src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs

+29-27
Original file line numberDiff line numberDiff line change
@@ -270,21 +270,16 @@ public bool TryGenerateAndAlsoNotNullExpression(Expression sourceExpression, boo
270270
return true;
271271
}
272272

273-
private static Expression GetMemberExpression(Expression expression)
273+
public bool ExpressionQualifiesForNullPropagation(Expression expression)
274274
{
275-
if (expression is MemberExpression memberExpression)
276-
{
277-
return memberExpression;
278-
}
279-
280-
if (expression is ParameterExpression parameterExpression)
281-
{
282-
return parameterExpression;
283-
}
275+
return expression is MemberExpression || expression is ParameterExpression || expression is MethodCallExpression;
276+
}
284277

285-
if (expression is MethodCallExpression methodCallExpression)
278+
private Expression GetMemberExpression(Expression expression)
279+
{
280+
if (ExpressionQualifiesForNullPropagation(expression))
286281
{
287-
return methodCallExpression;
282+
return expression;
288283
}
289284

290285
if (expression is LambdaExpression lambdaExpression)
@@ -303,7 +298,7 @@ private static Expression GetMemberExpression(Expression expression)
303298
return null;
304299
}
305300

306-
private static List<Expression> CollectExpressions(bool addSelf, Expression sourceExpression)
301+
private List<Expression> CollectExpressions(bool addSelf, Expression sourceExpression)
307302
{
308303
Expression expression = GetMemberExpression(sourceExpression);
309304

@@ -317,24 +312,31 @@ private static List<Expression> CollectExpressions(bool addSelf, Expression sour
317312
}
318313
}
319314

320-
while (expression is MemberExpression memberExpression)
315+
bool expressionRecognized;
316+
do
321317
{
322-
expression = GetMemberExpression(memberExpression.Expression);
323-
if (expression is MemberExpression)
318+
switch (expression)
324319
{
325-
list.Add(expression);
320+
case MemberExpression memberExpression:
321+
expression = GetMemberExpression(memberExpression.Expression);
322+
expressionRecognized = true;
323+
break;
324+
325+
case MethodCallExpression methodCallExpression:
326+
expression = methodCallExpression.Arguments.First();
327+
expressionRecognized = true;
328+
break;
329+
330+
default:
331+
expressionRecognized = false;
332+
break;
326333
}
327-
}
328334

329-
if (expression is ParameterExpression)
330-
{
331-
list.Add(expression);
332-
}
333-
334-
if (expression is MethodCallExpression)
335-
{
336-
list.Add(expression);
337-
}
335+
if (expressionRecognized && ExpressionQualifiesForNullPropagation(expression))
336+
{
337+
list.Add(expression);
338+
}
339+
} while (expressionRecognized);
338340

339341
return list;
340342
}

src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs

+5-5
Original file line numberDiff line numberDiff line change
@@ -1135,20 +1135,20 @@ Expression ParseFunctionNullPropagation()
11351135
throw ParseError(errorPos, Res.NullPropagationRequiresCorrectArgs);
11361136
}
11371137

1138-
if (args[0] is MemberExpression memberExpression)
1138+
if (_expressionHelper.ExpressionQualifiesForNullPropagation(args[0]))
11391139
{
11401140
bool hasDefaultParameter = args.Length == 2;
11411141
Expression expressionIfFalse = hasDefaultParameter ? args[1] : Expression.Constant(null);
11421142

1143-
if (_expressionHelper.TryGenerateAndAlsoNotNullExpression(memberExpression, hasDefaultParameter, out Expression generatedExpression))
1143+
if (_expressionHelper.TryGenerateAndAlsoNotNullExpression(args[0], hasDefaultParameter, out Expression generatedExpression))
11441144
{
1145-
return GenerateConditional(generatedExpression, memberExpression, expressionIfFalse, errorPos);
1145+
return GenerateConditional(generatedExpression, args[0], expressionIfFalse, errorPos);
11461146
}
11471147

1148-
return memberExpression;
1148+
return args[0];
11491149
}
11501150

1151-
throw ParseError(errorPos, Res.NullPropagationRequiresMemberExpression);
1151+
throw ParseError(errorPos, Res.NullPropagationRequiresValidExpression);
11521152
}
11531153

11541154
// Is(...) function

src/System.Linq.Dynamic.Core/Parser/IExpressionHelper.cs

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ internal interface IExpressionHelper
3030

3131
bool TryGenerateAndAlsoNotNullExpression(Expression sourceExpression, bool addSelf, out Expression generatedExpression);
3232

33+
bool ExpressionQualifiesForNullPropagation(Expression expression);
34+
3335
void WrapConstantExpression(ref Expression argument);
3436
}
3537
}

src/System.Linq.Dynamic.Core/Res.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ internal static class Res
5252
public const string NoParentInScope = "No 'parent' is in scope";
5353
public const string NoRootInScope = "No 'root' is in scope";
5454
public const string NullPropagationRequiresCorrectArgs = "The 'np' (null-propagation) function requires 1 or 2 arguments";
55-
public const string NullPropagationRequiresMemberExpression = "The 'np' (null-propagation) function requires the first argument to be a MemberExpression";
55+
public const string NullPropagationRequiresValidExpression = "The 'np' (null-propagation) function requires the first argument to be a MemberExpression, ParameterExpression or MethodCallExpression";
5656
public const string OpenBracketExpected = "'[' expected";
5757
public const string OpenCurlyParenExpected = "'{' expected";
5858
public const string OpenParenExpected = "'(' expected";

test/EntityFramework.DynamicLinq.Tests.net452/EntityFramework.DynamicLinq.Tests.net452.csproj

+3
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@
4343
<Reference Include="EntityFramework.SqlServer, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089, processorArchitecture=MSIL">
4444
<HintPath>..\..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll</HintPath>
4545
</Reference>
46+
<Reference Include="FluentAssertions, Version=5.10.3.0, Culture=neutral, PublicKeyToken=33f2691a05b67b6a, processorArchitecture=MSIL">
47+
<HintPath>..\..\packages\FluentAssertions.5.10.3\lib\net45\FluentAssertions.dll</HintPath>
48+
</Reference>
4649
<Reference Include="Linq.PropertyTranslator.Core, Version=1.0.3.0, Culture=neutral, processorArchitecture=MSIL">
4750
<HintPath>..\..\packages\Linq.PropertyTranslator.Core.1.0.3.0\lib\net452\Linq.PropertyTranslator.Core.dll</HintPath>
4851
</Reference>

test/EntityFramework.DynamicLinq.Tests.net452/packages.config

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<packages>
33
<package id="Castle.Core" version="4.4.0" targetFramework="net452" />
44
<package id="EntityFramework" version="6.1.3" targetFramework="net452" />
5+
<package id="FluentAssertions" version="5.10.3" targetFramework="net452" />
56
<package id="Linq.PropertyTranslator.Core" version="1.0.3.0" targetFramework="net452" />
67
<package id="Microsoft.AspNet.Identity.Core" version="2.2.1" targetFramework="net452" />
78
<package id="Microsoft.AspNet.Identity.EntityFramework" version="2.2.1" targetFramework="net452" />

test/EntityFramework.DynamicLinq.Tests/EntityFramework.DynamicLinq.Tests.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
</ItemGroup>
3333

3434
<ItemGroup>
35+
<PackageReference Include="FluentAssertions" Version="5.10.3" />
3536
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0" />
3637
<PackageReference Include="MongoDB.Driver" Version="2.4.4" />
3738
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />

test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs

+43
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Linq.Dynamic.Core.Tests.Helpers.Models;
66
using System.Linq.Expressions;
77
using System.Reflection;
8+
using FluentAssertions;
89
using Xunit;
910
using User = System.Linq.Dynamic.Core.Tests.Helpers.Models.User;
1011

@@ -14,10 +15,18 @@ public class DynamicExpressionParserTests
1415
{
1516
private class MyClass
1617
{
18+
public List<string> MyStrings { get; set; }
19+
20+
public List<MyClass> MyClasses { get; set; }
21+
1722
public int Foo()
1823
{
1924
return 42;
2025
}
26+
27+
public string Name { get; set; }
28+
29+
public MyClass Child { get; set; }
2130
}
2231

2332
private class ComplexParseLambda1Result
@@ -1044,5 +1053,39 @@ public void DynamicExpressionParser_ParseLambda_SupportEnumerationStringComparis
10441053
// Assert
10451054
Check.That(result).IsEqualTo(expectedResult);
10461055
}
1056+
1057+
[Fact]
1058+
public void DynamicExpressionParser_ParseLambda_NullPropagation_MethodCallExpression()
1059+
{
1060+
// Arrange
1061+
var dataSource = new MyClass();
1062+
1063+
var expressionText = "np(MyClasses.FirstOrDefault())";
1064+
1065+
// Act
1066+
LambdaExpression expression = DynamicExpressionParser.ParseLambda(ParsingConfig.Default, dataSource.GetType(), typeof(MyClass), expressionText);
1067+
Delegate del = expression.Compile();
1068+
MyClass result = del.DynamicInvoke(dataSource) as MyClass;
1069+
1070+
// Assert
1071+
result.Should().BeNull();
1072+
}
1073+
1074+
[Theory]
1075+
[InlineData("np(MyClasses.FirstOrDefault().Name)")]
1076+
[InlineData("np(MyClasses.FirstOrDefault(Name == \"a\").Name)")]
1077+
public void DynamicExpressionParser_ParseLambda_NullPropagation_MethodCallExpression_With_Property(string expressionText)
1078+
{
1079+
// Arrange
1080+
var dataSource = new MyClass();
1081+
1082+
// Act
1083+
LambdaExpression expression = DynamicExpressionParser.ParseLambda(ParsingConfig.Default, dataSource.GetType(), typeof(string), expressionText);
1084+
Delegate del = expression.Compile();
1085+
string result = del.DynamicInvoke(dataSource) as string;
1086+
1087+
// Assert
1088+
result.Should().BeNull();
1089+
}
10471090
}
10481091
}

test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1343,6 +1343,8 @@ public void ExpressionTests_NullCoalescing()
13431343
[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))")]
13441344
[InlineData("np(nested._enumnullable)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.nested != null)), Param_0.nested._enumnullable, null))")]
13451345
[InlineData("np(item.GuidNull)", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.item != null)), Param_0.item.GuidNull, null))")]
1346+
[InlineData("np(items.FirstOrDefault())", "Select(Param_0 => IIF(((Param_0 != null) AndAlso (Param_0.items != null)), Param_0.items.FirstOrDefault(), null))")]
1347+
[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))")]
13461348
public void ExpressionTests_NullPropagating(string test, string query)
13471349
{
13481350
// Arrange
@@ -1378,7 +1380,8 @@ public void ExpressionTests_NullPropagating(string test, string query)
13781380
Id = 100,
13791381
GuidNormal = Guid.NewGuid(),
13801382
GuidNull = Guid.NewGuid()
1381-
}
1383+
},
1384+
items = new [] { "a", "b" }
13821385
}
13831386
}.AsQueryable();
13841387

0 commit comments

Comments
 (0)