Skip to content

Commit 7d39d33

Browse files
authored
Add support for "not in" and "not_in" (#915)
1 parent b8a558e commit 7d39d33

File tree

4 files changed

+180
-59
lines changed

4 files changed

+180
-59
lines changed

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

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -218,11 +218,11 @@ internal IList<DynamicOrdering> ParseOrdering(bool forceThenBy = false)
218218
{
219219
var expr = ParseConditionalOperator();
220220
var ascending = true;
221-
if (TokenIdentifierIs("asc") || TokenIdentifierIs("ascending"))
221+
if (TokenIsIdentifier("asc") || TokenIsIdentifier("ascending"))
222222
{
223223
_textParser.NextToken();
224224
}
225-
else if (TokenIdentifierIs("desc") || TokenIdentifierIs("descending"))
225+
else if (TokenIsIdentifier("desc") || TokenIsIdentifier("descending"))
226226
{
227227
_textParser.NextToken();
228228
ascending = false;
@@ -337,19 +337,34 @@ private Expression ParseAndOperator()
337337
return left;
338338
}
339339

340-
// in operator for literals - example: "x in (1,2,3,4)"
341-
// in operator to mimic contains - example: "x in @0", compare to @0.Contains(x)
342-
// Adapted from ticket submitted by github user mlewis9548
340+
// "in" / "not in" / "not_in" operator for literals - example: "x in (1,2,3,4)"
341+
// "in" / "not in" / "not_in" operator to mimic contains - example: "x in @0", compare to @0.Contains(x)
343342
private Expression ParseIn()
344343
{
345344
Expression left = ParseLogicalAndOrOperator();
346345
Expression accumulate = left;
347346

348-
while (TokenIdentifierIs("in"))
347+
while (_textParser.TryGetToken(["in", "not_in", "not"], [TokenId.Exclamation], out var token))
349348
{
350-
var op = _textParser.CurrentToken;
349+
var not = false;
350+
if (token.Text == "not_in")
351+
{
352+
not = true;
353+
}
354+
else if (token.Text == "not" || token.Id == TokenId.Exclamation)
355+
{
356+
not = true;
357+
358+
_textParser.NextToken();
359+
360+
if (!TokenIsIdentifier("in"))
361+
{
362+
throw ParseError(token.Pos, Res.TokenExpected, "in");
363+
}
364+
}
351365

352366
_textParser.NextToken();
367+
353368
if (_textParser.CurrentToken.Id == TokenId.OpenParen) // literals (or other inline list)
354369
{
355370
while (_textParser.CurrentToken.Id != TokenId.CloseParen)
@@ -364,18 +379,18 @@ private Expression ParseIn()
364379
{
365380
if (right is ConstantExpression constantExprRight)
366381
{
367-
right = ParseEnumToConstantExpression(op.Pos, left.Type, constantExprRight);
382+
right = ParseEnumToConstantExpression(token.Pos, left.Type, constantExprRight);
368383
}
369384
else if (_expressionHelper.TryUnwrapAsConstantExpression(right, out var unwrappedConstantExprRight))
370385
{
371-
right = ParseEnumToConstantExpression(op.Pos, left.Type, unwrappedConstantExprRight);
386+
right = ParseEnumToConstantExpression(token.Pos, left.Type, unwrappedConstantExprRight);
372387
}
373388
}
374389

375390
// else, check for direct type match
376391
else if (left.Type != right.Type)
377392
{
378-
CheckAndPromoteOperands(typeof(IEqualitySignatures), TokenId.DoubleEqual, "==", ref left, ref right, op.Pos);
393+
CheckAndPromoteOperands(typeof(IEqualitySignatures), TokenId.DoubleEqual, "==", ref left, ref right, token.Pos);
379394
}
380395

381396
if (accumulate.Type != typeof(bool))
@@ -389,7 +404,7 @@ private Expression ParseIn()
389404

390405
if (_textParser.CurrentToken.Id == TokenId.End)
391406
{
392-
throw ParseError(op.Pos, Res.CloseParenOrCommaExpected);
407+
throw ParseError(token.Pos, Res.CloseParenOrCommaExpected);
393408
}
394409
}
395410

@@ -413,7 +428,12 @@ private Expression ParseIn()
413428
}
414429
else
415430
{
416-
throw ParseError(op.Pos, Res.OpenParenOrIdentifierExpected);
431+
throw ParseError(token.Pos, Res.OpenParenOrIdentifierExpected);
432+
}
433+
434+
if (not)
435+
{
436+
accumulate = Expression.Not(accumulate);
417437
}
418438
}
419439

@@ -759,7 +779,7 @@ private Expression ParseAdditive()
759779
private Expression ParseArithmetic()
760780
{
761781
Expression left = ParseUnary();
762-
while (_textParser.CurrentToken.Id is TokenId.Asterisk or TokenId.Slash or TokenId.Percent || TokenIdentifierIs("mod"))
782+
while (_textParser.CurrentToken.Id is TokenId.Asterisk or TokenId.Slash or TokenId.Percent || TokenIsIdentifier("mod"))
763783
{
764784
Token op = _textParser.CurrentToken;
765785
_textParser.NextToken();
@@ -787,11 +807,11 @@ private Expression ParseArithmetic()
787807
// -, !, not unary operators
788808
private Expression ParseUnary()
789809
{
790-
if (_textParser.CurrentToken.Id == TokenId.Minus || _textParser.CurrentToken.Id == TokenId.Exclamation || TokenIdentifierIs("not"))
810+
if (_textParser.CurrentToken.Id == TokenId.Minus || _textParser.CurrentToken.Id == TokenId.Exclamation || TokenIsIdentifier("not"))
791811
{
792812
Token op = _textParser.CurrentToken;
793813
_textParser.NextToken();
794-
if (op.Id == TokenId.Minus && (_textParser.CurrentToken.Id == TokenId.IntegerLiteral || _textParser.CurrentToken.Id == TokenId.RealLiteral))
814+
if (op.Id == TokenId.Minus && _textParser.CurrentToken.Id is TokenId.IntegerLiteral or TokenId.RealLiteral)
795815
{
796816
_textParser.CurrentToken.Text = "-" + _textParser.CurrentToken.Text;
797817
_textParser.CurrentToken.Pos = op.Pos;
@@ -1445,7 +1465,7 @@ private Expression ParseNew()
14451465
if (!arrayInitializer)
14461466
{
14471467
string? propName;
1448-
if (TokenIdentifierIs("as"))
1468+
if (TokenIsIdentifier("as"))
14491469
{
14501470
_textParser.NextToken();
14511471
propName = GetIdentifierAs();
@@ -2527,11 +2547,11 @@ private static Exception IncompatibleOperandsError(string opName, Expression lef
25272547
#endif
25282548
}
25292549

2530-
private bool TokenIdentifierIs(string id)
2550+
private bool TokenIsIdentifier(string id)
25312551
{
2532-
return _textParser.CurrentToken.Id == TokenId.Identifier && string.Equals(id, _textParser.CurrentToken.Text, StringComparison.OrdinalIgnoreCase);
2552+
return _textParser.TokenIsIdentifier(id);
25332553
}
2534-
2554+
25352555
private string GetIdentifier()
25362556
{
25372557
_textParser.ValidateToken(TokenId.Identifier, Res.IdentifierExpected);

src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,40 @@ public void ValidateToken(TokenId tokenId, string? errorMessage = null)
479479
}
480480
}
481481

482+
/// <summary>
483+
/// Check if the current token is an <see cref="TokenId.Identifier"/> with the provided id .
484+
/// </summary>
485+
/// <param name="id">The id</param>
486+
public bool TokenIsIdentifier(string id)
487+
{
488+
return CurrentToken.Id == TokenId.Identifier && string.Equals(id, CurrentToken.Text, StringComparison.OrdinalIgnoreCase);
489+
}
490+
491+
/// <summary>
492+
/// Try to get a token based on the id or <see cref="TokenId"/>.
493+
/// </summary>
494+
/// <param name="ids">The ids.</param>
495+
/// <param name="tokenIds">The tokenIds.</param>
496+
/// <param name="token">The found token, or default when not found.</param>
497+
public bool TryGetToken(string[] ids, TokenId[] tokenIds, out Token token)
498+
{
499+
token = default;
500+
501+
if (ids.Any(TokenIsIdentifier))
502+
{
503+
token = CurrentToken;
504+
return true;
505+
}
506+
507+
if (tokenIds.Any(tokenId => tokenId == CurrentToken.Id))
508+
{
509+
token = CurrentToken;
510+
return true;
511+
}
512+
513+
return false;
514+
}
515+
482516
private void SetTextPos(int pos)
483517
{
484518
_textPos = pos;

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

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,28 +1307,44 @@ public void ExpressionTests_In_Short()
13071307
public void ExpressionTests_In_String()
13081308
{
13091309
// Arrange
1310-
var testRange = Enumerable.Range(1, 100).ToArray();
13111310
var testModels = User.GenerateSampleModels(10);
1312-
var testModelByUsername = string.Format("Username in (\"{0}\",\"{1}\",\"{2}\")", testModels[0].UserName, testModels[1].UserName, testModels[2].UserName);
1311+
var testModelByUsername = $"Username in (\"{testModels[0].UserName}\",\"{testModels[1].UserName}\",\"{testModels[2].UserName}\")";
1312+
1313+
// Act
1314+
var result1 = testModels.AsQueryable().Where(testModelByUsername).ToArray();
1315+
var result2 = testModels.AsQueryable().Where("Id in (@0, @1, @2)", testModels[0].Id, testModels[1].Id, testModels[2].Id).ToArray();
1316+
1317+
// Assert
1318+
Assert.Equal(testModels.Take(3).ToArray(), result1);
1319+
Assert.Equal(testModels.Take(3).ToArray(), result2);
1320+
}
1321+
1322+
[Fact]
1323+
public void ExpressionTests_In_IntegerArray()
1324+
{
1325+
// Arrange
1326+
var testRange = Enumerable.Range(1, 10).ToArray();
13131327
var testInExpression = new[] { 2, 4, 6, 8 };
13141328

13151329
// Act
13161330
var result1a = testRange.AsQueryable().Where("it in (2,4,6,8)").ToArray();
13171331
var result1b = testRange.AsQueryable().Where("it in (2, 4, 6, 8)").ToArray();
1318-
// https://github.com/NArnott/System.Linq.Dynamic/issues/52
1319-
var result2 = testModels.AsQueryable().Where(testModelByUsername).ToArray();
1320-
var result3 =
1321-
testModels.AsQueryable()
1322-
.Where("Id in (@0, @1, @2)", testModels[0].Id, testModels[1].Id, testModels[2].Id)
1323-
.ToArray();
1324-
var result4 = testRange.AsQueryable().Where("it in @0", testInExpression).ToArray();
1332+
var result2 = testRange.AsQueryable().Where("it in @0", testInExpression).ToArray();
13251333

13261334
// Assert
1327-
Assert.Equal(new[] { 2, 4, 6, 8 }, result1a);
1328-
Assert.Equal(new[] { 2, 4, 6, 8 }, result1b);
1329-
Assert.Equal(testModels.Take(3).ToArray(), result2);
1330-
Assert.Equal(testModels.Take(3).ToArray(), result3);
1331-
Assert.Equal(new[] { 2, 4, 6, 8 }, result4);
1335+
Assert.Equal([2, 4, 6, 8], result1a);
1336+
Assert.Equal([2, 4, 6, 8], result1b);
1337+
Assert.Equal([2, 4, 6, 8], result2);
1338+
}
1339+
1340+
[Fact]
1341+
public void ExpressionTests_InvalidNotIn_ThrowsException()
1342+
{
1343+
// Arrange
1344+
var testRange = Enumerable.Range(1, 10).ToArray();
1345+
1346+
// Act + Assert
1347+
Check.ThatCode(() => testRange.AsQueryable().Where("it not not in (2,4,6,8)").ToArray()).Throws<ParseException>();
13321348
}
13331349

13341350
[Fact]
@@ -1519,6 +1535,26 @@ public void ExpressionTests_Multiply_Number()
15191535
Check.That(result).ContainsExactly(expected);
15201536
}
15211537

1538+
[Fact]
1539+
public void ExpressionTests_NotIn_IntegerArray()
1540+
{
1541+
// Arrange
1542+
var testRange = Enumerable.Range(1, 9).ToArray();
1543+
var testInExpression = new[] { 2, 4, 6, 8 };
1544+
1545+
// Act
1546+
var result1a = testRange.AsQueryable().Where("it not in (2,4,6,8)").ToArray();
1547+
var result1b = testRange.AsQueryable().Where("it not_in (2, 4, 6, 8)").ToArray();
1548+
var result2 = testRange.AsQueryable().Where("it not in @0", testInExpression).ToArray();
1549+
var result3 = testRange.AsQueryable().Where("it not_in @0", testInExpression).ToArray();
1550+
1551+
// Assert
1552+
Assert.Equal([1, 3, 5, 7, 9], result1a);
1553+
Assert.Equal([1, 3, 5, 7, 9], result1b);
1554+
Assert.Equal([1, 3, 5, 7, 9], result2);
1555+
Assert.Equal([1, 3, 5, 7, 9], result3);
1556+
}
1557+
15221558
[Fact]
15231559
public void ExpressionTests_NullCoalescing()
15241560
{
@@ -1699,7 +1735,7 @@ public void ExpressionTests_NullPropagating_Config_Has_UseDefault(string test, s
16991735
queryAsString = queryAsString.Substring(queryAsString.IndexOf(".Select") + 1).TrimEnd(']');
17001736
Check.That(queryAsString).Equals(query);
17011737
}
1702-
1738+
17031739
[Fact]
17041740
public void ExpressionTests_NullPropagation_Method()
17051741
{
@@ -2103,7 +2139,7 @@ public void ExpressionTests_StringEscaping()
21032139

21042140
// Act
21052141
var result = baseQuery.Where("it.Value == \"ab\\\"cd\"").ToList();
2106-
2142+
21072143
// Assert
21082144
Assert.Single(result);
21092145
Assert.Equal("ab\"cd", result[0].Value);

0 commit comments

Comments
 (0)