Skip to content

Commit 97b89ae

Browse files
committed
Null-coalescing operator support (#9)
1 parent 8fc4281 commit 97b89ae

File tree

5 files changed

+94
-16
lines changed

5 files changed

+94
-16
lines changed

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

+30-3
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ enum TokenId
5151
GreaterThanEqual,
5252
DoubleBar,
5353
DoubleGreaterThan,
54-
DoubleLessThan
54+
DoubleLessThan,
55+
NullCoalescing
5556
}
5657

5758
interface ILogicalSignatures
@@ -407,7 +408,7 @@ public IEnumerable<DynamicOrdering> ParseOrdering()
407408
Expression ParseExpression()
408409
{
409410
int errorPos = _token.pos;
410-
Expression expr = ParseConditionalOr();
411+
Expression expr = ParseNullCoalescing();
411412
if (_token.id == TokenId.Question)
412413
{
413414
NextToken();
@@ -420,6 +421,19 @@ Expression ParseExpression()
420421
return expr;
421422
}
422423

424+
// ?? (null-coalescing) operator
425+
Expression ParseNullCoalescing()
426+
{
427+
Expression expr = ParseConditionalOr();
428+
if (_token.id == TokenId.NullCoalescing)
429+
{
430+
NextToken();
431+
Expression right = ParseExpression();
432+
expr = Expression.Coalesce(expr, right);
433+
}
434+
return expr;
435+
}
436+
423437
// ||, or operator
424438
Expression ParseConditionalOr()
425439
{
@@ -1383,11 +1397,16 @@ static string GetTypeName(Type type)
13831397
static bool TryGetMemberName(Expression expression, out string memberName)
13841398
{
13851399
var memberExpression = expression as MemberExpression;
1400+
if (memberExpression == null && expression.NodeType == ExpressionType.Coalesce)
1401+
{
1402+
memberExpression = ((expression as BinaryExpression).Left) as MemberExpression;
1403+
}
13861404
if (memberExpression != null)
13871405
{
13881406
memberName = memberExpression.Member.Name;
13891407
return true;
13901408
}
1409+
13911410
#if NETFX_CORE
13921411
var indexExpression = expression as IndexExpression;
13931412
if (indexExpression != null && indexExpression.Indexer.DeclaringType == typeof(DynamicObjectClass))
@@ -2350,7 +2369,15 @@ void NextToken()
23502369
break;
23512370
case '?':
23522371
NextChar();
2353-
t = TokenId.Question;
2372+
if (_ch == '?')
2373+
{
2374+
NextChar();
2375+
t = TokenId.NullCoalescing;
2376+
}
2377+
else
2378+
{
2379+
t = TokenId.Question;
2380+
}
23542381
break;
23552382
case '[':
23562383
NextChar();

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

+31-9
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public class EntitiesTests : IDisposable
1919
{
2020
BlogContext _context;
2121

22-
#region Entities Test Support
22+
#region Entities Test Support
2323

2424
static readonly Random Rnd = new Random(1);
2525

@@ -77,9 +77,31 @@ void PopulateTestData(int blogCount = 25, int postCount = 10)
7777
_context.SaveChanges();
7878
}
7979

80-
#endregion
80+
#endregion
8181

82-
#region Select Tests
82+
#region Select Tests
83+
84+
[Fact]
85+
public void Entities_Select_SingleColumn_NullCoalescing()
86+
{
87+
//Arrange
88+
var blog1 = new Blog { BlogId = 1000, Name = "Blog1", NullableInt = null };
89+
var blog2 = new Blog { BlogId = 2000, Name = "Blog2", NullableInt = 5 };
90+
_context.Blogs.Add(blog1);
91+
_context.Blogs.Add(blog2);
92+
_context.SaveChanges();
93+
94+
var expected1 = _context.Blogs.Select(x => x.NullableInt ?? 10).ToArray();
95+
var expected2 = _context.Blogs.Select(x => x.NullableInt ?? 9 + x.BlogId).ToArray();
96+
97+
//Act
98+
var test1 = _context.Blogs.Select<int>("NullableInt ?? 10").ToArray();
99+
var test2 = _context.Blogs.Select<int>("NullableInt ?? 9 + BlogId").ToArray();
100+
101+
//Assert
102+
Assert.Equal(expected1, test1);
103+
Assert.Equal(expected2, test2);
104+
}
83105

84106
[Fact]
85107
public void Entities_Select_SingleColumn()
@@ -106,7 +128,7 @@ public void Entities_Select_MultipleColumn()
106128

107129
//Act
108130
var test = _context.Blogs.Select("new (\"x\" as X, BlogId, Name)").ToDynamicArray();
109-
131+
110132
//Assert
111133
Assert.Equal(
112134
expected,
@@ -156,9 +178,9 @@ public void Entities_Select_BlogPosts()
156178
// }
157179
//}
158180

159-
#endregion
181+
#endregion
160182

161-
#region GroupBy Tests
183+
#region GroupBy Tests
162184

163185
[Fact]
164186
public void Entities_GroupBy_SingleKey()
@@ -309,9 +331,9 @@ public void Entities_GroupBy_SingleKey_Sum()
309331
}
310332
}
311333

312-
#endregion
334+
#endregion
313335

314-
#region Executor Tests
336+
#region Executor Tests
315337

316338
[Fact]
317339
public void FirstOrDefault_AsStringExpressions()
@@ -331,7 +353,7 @@ public void FirstOrDefault_AsStringExpressions()
331353
Assert.Equal(firstExpected.ToArray(), firstTest.ToArray());
332354
}
333355

334-
#endregion
356+
#endregion
335357
}
336358
}
337359
#endif

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

+30-4
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,33 @@ namespace System.Linq.Dynamic.Core.Tests
99
public class ExpressionTests
1010
{
1111
[Fact]
12-
public void ExpressionTests_ParseConditionalOr1()
12+
public void ExpressionTests_NullCoalescing()
13+
{
14+
//Arrange
15+
var testModels = User.GenerateSampleModels(3, true);
16+
testModels[0].NullableInt = null;
17+
testModels[1].NullableInt = null;
18+
testModels[2].NullableInt = 5;
19+
20+
var expectedResult1 = testModels.AsQueryable().Select(u => new { UserName = u.UserName, X = u.NullableInt ?? (3 * u.Income) }).Cast<object>().ToArray();
21+
var expectedResult2 = testModels.AsQueryable().Where(u => (u.NullableInt ?? 10) == 10).ToArray();
22+
var expectedResult3 = testModels.Select(m => m.NullableInt ?? 10).ToArray();
23+
24+
//Act
25+
var result1 = testModels.AsQueryable().Select("new (UserName, NullableInt ?? (3 * Income) as X)");
26+
var result2 = testModels.AsQueryable().Where("(NullableInt ?? 10) == 10");
27+
var result3a = testModels.AsQueryable().Select("NullableInt ?? @0", 10);
28+
var result3b = testModels.AsQueryable().Select<int>("NullableInt ?? @0", 10);
29+
30+
//Assert
31+
Assert.Equal(expectedResult1.ToString(), result1.ToDynamicArray().ToString());
32+
Assert.Equal(expectedResult2, result2.ToDynamicArray<User>());
33+
Assert.Equal(expectedResult3, result3a.ToDynamicArray<int>());
34+
Assert.Equal(expectedResult3, result3b.ToDynamicArray<int>());
35+
}
36+
37+
[Fact]
38+
public void ExpressionTests_ConditionalOr1()
1339
{
1440
//Arrange
1541
int[] values = { 1, 2, 3, 4, 5 };
@@ -24,7 +50,7 @@ public void ExpressionTests_ParseConditionalOr1()
2450
}
2551

2652
[Fact]
27-
public void ExpressionTests_ParseConditionalOr2()
53+
public void ExpressionTests_ConditionalOr2()
2854
{
2955
//Arrange
3056
int[] values = { 1, 2, 3, 4, 5 };
@@ -39,7 +65,7 @@ public void ExpressionTests_ParseConditionalOr2()
3965
}
4066

4167
[Fact]
42-
public void ExpressionTests_ParseConditionalAnd1()
68+
public void ExpressionTests_ConditionalAnd1()
4369
{
4470
//Arrange
4571
var values = new[] { new { s = "s", i = 1 }, new { s = "abc", i = 2 } };
@@ -54,7 +80,7 @@ public void ExpressionTests_ParseConditionalAnd1()
5480
}
5581

5682
[Fact]
57-
public void ExpressionTests_ParseConditionalAnd2()
83+
public void ExpressionTests_ConditionalAnd2()
5884
{
5985
//Arrange
6086
var values = new[] { new { s = "s", i = 1 }, new { s = "abc", i = 2 } };

test/System.Linq.Dynamic.Core.Tests/Helpers/Entities/Blog.cs

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ public class Blog
66
{
77
public int BlogId { get; set; }
88
public string Name { get; set; }
9+
public int? NullableInt { get; set; }
910

1011
public virtual ICollection<Post> Posts { get; set; }
1112
}

test/System.Linq.Dynamic.Core.Tests/Helpers/Models/User.cs

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ public class User
88

99
public string UserName { get; set; }
1010

11+
public int? NullableInt { get; set; }
12+
1113
public int Income { get; set; }
1214

1315
public UserProfile Profile { get; set; }

0 commit comments

Comments
 (0)