Skip to content

Commit 285bb67

Browse files
committed
Support string to enum conversion using "in-expression"
1 parent 84c2379 commit 285bb67

5 files changed

Lines changed: 133 additions & 82 deletions

File tree

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

Lines changed: 89 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -365,78 +365,26 @@ private Expression ParseIn()
365365

366366
_textParser.NextToken();
367367

368+
var expressions = new Dictionary<Expression, int>();
369+
368370
if (_textParser.CurrentToken.Id == TokenId.OpenParen) // literals (or other inline list)
369371
{
370-
var values = new List<Expression>();
371-
var comparisons = new List<Expression>();
372-
Expression? containsLeft = null;
373-
string? containsLeftText = null;
374-
var canUseContains = true;
375-
376372
while (_textParser.CurrentToken.Id != TokenId.CloseParen)
377373
{
378374
_textParser.NextToken();
379375

380376
// we need to parse unary expressions because otherwise 'in' clause will fail in use cases like 'in (-1, -1)' or 'in (!true)'
381377
Expression right = ParseUnary();
382378

383-
// if the identifier is an Enum (or nullable Enum), try to convert the right-side also to an Enum.
384-
if (TypeHelper.GetNonNullableType(left.Type).GetTypeInfo().IsEnum)
385-
{
386-
if (right is ConstantExpression constantExprRight)
387-
{
388-
right = ParseEnumToConstantExpression(token.Pos, left.Type, constantExprRight);
389-
}
390-
else if (_expressionHelper.TryUnwrapAsConstantExpression(right, out var unwrappedConstantExprRight))
391-
{
392-
right = ParseEnumToConstantExpression(token.Pos, left.Type, unwrappedConstantExprRight);
393-
}
394-
}
395-
396-
// else, check for direct type match
397-
else if (left.Type != right.Type)
398-
{
399-
CheckAndPromoteOperands(typeof(IEqualitySignatures), TokenId.DoubleEqual, "==", ref left, ref right, token.Pos);
400-
}
401-
402-
var equalsExpression = _expressionHelper.GenerateEqual(left, right);
403-
comparisons.Add(equalsExpression);
404-
405-
if (canUseContains && equalsExpression is BinaryExpression binaryExpression && binaryExpression.NodeType == ExpressionType.Equal)
406-
{
407-
containsLeft ??= binaryExpression.Left;
408-
containsLeftText ??= binaryExpression.Left.ToString();
409-
410-
if (containsLeft.Type != binaryExpression.Left.Type || !string.Equals(containsLeftText, binaryExpression.Left.ToString(), StringComparison.Ordinal) || binaryExpression.Right.Type != containsLeft.Type)
411-
{
412-
canUseContains = false;
413-
}
414-
else
415-
{
416-
values.Add(binaryExpression.Right);
417-
}
418-
}
419-
else
420-
{
421-
canUseContains = false;
422-
}
379+
expressions.Add(right, token.Pos);
423380

424381
if (_textParser.CurrentToken.Id == TokenId.End)
425382
{
426383
throw ParseError(token.Pos, Res.CloseParenOrCommaExpected);
427384
}
428385
}
429386

430-
if (canUseContains && containsLeft != null)
431-
{
432-
var typeArgs = new[] { containsLeft.Type };
433-
var args = new Expression[] { Expression.NewArrayInit(containsLeft.Type, values), containsLeft };
434-
accumulate = Expression.Call(typeof(Enumerable), nameof(Enumerable.Contains), typeArgs, args);
435-
}
436-
else
437-
{
438-
accumulate = _expressionHelper.GenerateBinaryOrElseTree(comparisons);
439-
}
387+
accumulate = ProcessInExpressions(accumulate, expressions);
440388

441389
// Since this started with an open paren, make sure to move off the close
442390
_textParser.NextToken();
@@ -445,16 +393,29 @@ private Expression ParseIn()
445393
{
446394
Expression right = ParsePrimary();
447395

448-
if (!typeof(IEnumerable).IsAssignableFrom(right.Type))
396+
if (!TypeHelper.TryGetAsEnumerable(right.Type, out _))
449397
{
450-
throw ParseError(_textParser.CurrentToken.Pos, Res.IdentifierImplementingInterfaceExpected, typeof(IEnumerable));
398+
throw ParseError(_textParser.CurrentToken.Pos, Res.IdentifierImplementingInterfaceExpected, typeof(IEnumerable<>));
451399
}
452400

453-
var typeArgs = new[] { left.Type };
401+
// Handle "it.TestEnum in @0", and the @0 should be a object like a List<string>.
402+
if (_symbols.Count > 0 && right is ConstantExpression constantExprRight && constantExprRight.Value != null)
403+
{
404+
foreach (var item in (IEnumerable)constantExprRight.Value)
405+
{
406+
expressions.Add(Expression.Constant(item), token.Pos);
407+
}
454408

455-
var args = new[] { right, left };
409+
accumulate = ProcessInExpressions(accumulate, expressions);
410+
}
456411

457-
accumulate = Expression.Call(typeof(Enumerable), nameof(Enumerable.Contains), typeArgs, args);
412+
// Handle "'y' in Name"
413+
else
414+
{
415+
var typeArgs = new[] { left.Type };
416+
var args = new[] { right, left };
417+
accumulate = Expression.Call(typeof(Enumerable), nameof(Enumerable.Contains), typeArgs, args);
418+
}
458419
}
459420
else
460421
{
@@ -470,6 +431,71 @@ private Expression ParseIn()
470431
return accumulate;
471432
}
472433

434+
private Expression ProcessInExpressions(Expression left, Dictionary<Expression, int> expressions)
435+
{
436+
var values = new List<Expression>();
437+
var comparisons = new List<Expression>();
438+
Expression? containsLeft = null;
439+
string? containsLeftText = null;
440+
var canUseContains = true;
441+
442+
for (int i = 0; i < expressions.Count; i++)
443+
{
444+
var right = expressions.ElementAt(i).Key;
445+
var tokenPos = expressions.ElementAt(i).Value;
446+
447+
// if the identifier is an Enum (or nullable Enum), try to convert the right-side also to an Enum.
448+
if (TypeHelper.GetNonNullableType(left.Type).GetTypeInfo().IsEnum)
449+
{
450+
if (right is ConstantExpression constantExprRight)
451+
{
452+
right = ParseEnumToConstantExpression(tokenPos, left.Type, constantExprRight);
453+
}
454+
else if (_expressionHelper.TryUnwrapAsConstantExpression(right, out var unwrappedConstantExprRight))
455+
{
456+
right = ParseEnumToConstantExpression(tokenPos, left.Type, unwrappedConstantExprRight);
457+
}
458+
}
459+
460+
// else, check for direct type match
461+
else if (left.Type != right.Type)
462+
{
463+
CheckAndPromoteOperands(typeof(IEqualitySignatures), TokenId.DoubleEqual, "==", ref left, ref right, tokenPos);
464+
}
465+
466+
var equalsExpression = _expressionHelper.GenerateEqual(left, right);
467+
comparisons.Add(equalsExpression);
468+
469+
if (canUseContains && equalsExpression is BinaryExpression binaryExpression && binaryExpression.NodeType == ExpressionType.Equal)
470+
{
471+
containsLeft ??= binaryExpression.Left;
472+
containsLeftText ??= binaryExpression.Left.ToString();
473+
474+
if (containsLeft.Type != binaryExpression.Left.Type || !string.Equals(containsLeftText, binaryExpression.Left.ToString(), StringComparison.Ordinal) || binaryExpression.Right.Type != containsLeft.Type)
475+
{
476+
canUseContains = false;
477+
}
478+
else
479+
{
480+
values.Add(binaryExpression.Right);
481+
}
482+
}
483+
else
484+
{
485+
canUseContains = false;
486+
}
487+
}
488+
489+
if (canUseContains && containsLeft != null)
490+
{
491+
var typeArgs = new[] { containsLeft.Type };
492+
var args = new Expression[] { Expression.NewArrayInit(containsLeft.Type, values), containsLeft };
493+
return Expression.Call(typeof(Enumerable), nameof(Enumerable.Contains), typeArgs, args);
494+
}
495+
496+
return _expressionHelper.GenerateBinaryOrElseTree(comparisons);
497+
}
498+
473499
// &, | bitwise operators
474500
private Expression ParseLogicalAndOrOperator()
475501
{
@@ -2081,9 +2107,9 @@ private Expression ParseMemberAccess(Type? type, Expression? expression, string?
20812107
throw ParseError(errorPos, Res.UnknownPropertyOrField, id, TypeHelper.GetTypeName(type));
20822108
}
20832109

2084-
private bool TryFindPropertyOrField(Type type, string id, Expression? expression, [NotNullWhen(true)] out Expression? propertyOrFieldExpression)
2110+
private bool TryFindPropertyOrField(Type type, string memberName, Expression? expression, [NotNullWhen(true)] out Expression? propertyOrFieldExpression)
20852111
{
2086-
var member = FindPropertyOrField(type, id, expression == null);
2112+
var member = FindPropertyOrField(type, memberName, expression == null);
20872113
switch (member)
20882114
{
20892115
case PropertyInfo property:

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,19 @@ internal static bool IsDynamicClass(Type type)
1313

1414
internal static bool TryGetAsEnumerable(Type type, [NotNullWhen(true)] out Type? enumerableType)
1515
{
16-
if (type.IsArray)
17-
{
18-
enumerableType = typeof(IEnumerable<>).MakeGenericType(type.GetElementType()!);
19-
return true;
20-
}
21-
2216
if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
2317
{
2418
enumerableType = type;
2519
return true;
2620
}
2721

28-
enumerableType = null;
29-
return false;
22+
enumerableType = type
23+
.GetInterfaces()
24+
.FirstOrDefault(i =>
25+
i.GetTypeInfo().IsGenericType &&
26+
i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
27+
28+
return enumerableType is not null;
3029
}
3130

3231
public static bool TryGetFirstGenericArgument(Type type, [NotNullWhen(true)] out Type? genericType)

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System.Linq.Dynamic.Core.Tests.Helpers.Entities;
22

33
#if EFCORE
4-
using System.Collections.Generic;
54
using Microsoft.EntityFrameworkCore;
65
#else
76
using System.Data.Entity;
@@ -20,10 +19,16 @@ public partial class EntitiesTests
2019
public void Entities_Where_In_And()
2120
{
2221
// Arrange
23-
var expected = _context.Blogs.Include(b => b.Posts).Where(b => new[] { 1000, 1001, 1002 }.Contains(b.BlogId) && new[] { "Blog1", "Blog2" }.Contains(b.Name)).ToArray();
22+
var expected = _context.Blogs.Include(b => b.Posts)
23+
.Where(b =>
24+
new[] { 1000, 1001, 1002 }.Contains(b.BlogId) && new[] { "Blog1", "Blog2" }.Contains(b.Name) && b.Name.Contains('o')
25+
)
26+
.ToArray();
2427

2528
// Act
26-
var test = _context.Blogs.Include(b => b.Posts).Where(@"BlogId in (1000, 1001, 1002) and Name in (""Blog1"", ""Blog2"")").ToArray();
29+
var test = _context.Blogs.Include(b => b.Posts)
30+
.Where(@"BlogId in (1000, 1001, 1002) and Name in (""Blog1"", ""Blog2"") && Name.Contains('o')")
31+
.ToArray();
2732

2833
// Assert
2934
Assert.Equal(expected, test);

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

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,10 +1310,22 @@ public void ExpressionTests_In_Enum()
13101310
var expected = qry.Where(x => new[] { TestEnum.Var1, TestEnum.Var2 }.Contains(x.TestEnum)).ToArray();
13111311
var result1 = qry.Where("it.TestEnum in (\"Var1\", \"Var2\")").ToArray();
13121312
var result2 = qry.Where("it.TestEnum in (0, 1)").ToArray();
1313+
var result3 = qry.Where("it.TestEnum in @0", new[] { TestEnum.Var1, TestEnum.Var2 });
1314+
var objectList = new List<string> { "Var1", "Var2" };
1315+
var result4 = qry.Where("it.TestEnum in @0", objectList);
1316+
var result5 = qry.Where("it.TestEnum in @0", GetVar1AndVar2());
13131317

13141318
// Assert
1315-
Check.That(result1).ContainsExactly(expected);
1316-
Check.That(result2).ContainsExactly(expected);
1319+
Assert.Equivalent(result1, expected);
1320+
Assert.Equivalent(result2, expected);
1321+
Assert.Equivalent(result3, expected);
1322+
Assert.Equivalent(result4, expected);
1323+
Assert.Equivalent(result5, expected);
1324+
}
1325+
1326+
private static List<string> GetVar1AndVar2()
1327+
{
1328+
return new List<string> { "Var1", "Var" + "2" };
13171329
}
13181330

13191331
[Fact]
@@ -1330,10 +1342,18 @@ public void ExpressionTests_In_EnumIsNullable()
13301342
var expected = new[] { model1, model2 };
13311343
var result1 = qry.Where("it.TestEnumNullable in (\"Var1\", \"Var2\")").ToArray();
13321344
var result2 = qry.Where("it.TestEnumNullable in (0, 1)").ToArray();
1345+
var result3 = qry.Where("it.TestEnumNullable in @0", new[] { TestEnum.Var1, TestEnum.Var2 });
1346+
1347+
var objectList = new List<string> { "Var1", "Var2" };
1348+
var result4 = qry.Where("it.TestEnumNullable in @0", objectList);
1349+
var result5 = qry.Where("it.TestEnumNullable in @0", GetVar1AndVar2());
13331350

13341351
// Assert
1335-
Check.That(result1).ContainsExactly(expected);
1336-
Check.That(result2).ContainsExactly(expected);
1352+
Assert.Equivalent(result1, expected);
1353+
Assert.Equivalent(result2, expected);
1354+
Assert.Equivalent(result3, expected);
1355+
Assert.Equivalent(result4, expected);
1356+
Assert.Equivalent(result5, expected);
13371357
}
13381358

13391359
[Fact]

test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,13 +266,14 @@ public void Parse_ParseMultipleInOperators()
266266
{
267267
// Arrange
268268
ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "x")];
269-
var sut = new ExpressionParser(parameters, "MainCompanyId in (1, 2) and Name in (\"A\", \"B\") && 'y' in Name && 'z' in Name", null, null);
269+
var values = new object?[] { new List<int> { 42, 43 }, new List<int> { 100, 100 + 1 } };
270+
var sut = new ExpressionParser(parameters, "MainCompanyId in (1, 2) and Name in (\"A\", \"B\") && 'y' in Name && 'z' in Name and MainCompanyId in @0 and MainCompanyId in @1", values, null);
270271

271272
// Act
272273
var parsedExpression = sut.Parse(null).ToString();
273274

274275
// Assert
275-
Check.That(parsedExpression).Equals("(((new [] {1, 2}.Contains(x.MainCompanyId) AndAlso new [] {\"A\", \"B\"}.Contains(x.Name)) AndAlso x.Name.Contains(y)) AndAlso x.Name.Contains(z))");
276+
Check.That(parsedExpression).Equals("(((((new [] {1, 2}.Contains(x.MainCompanyId) AndAlso new [] {\"A\", \"B\"}.Contains(x.Name)) AndAlso x.Name.Contains(y)) AndAlso x.Name.Contains(z)) AndAlso new [] {42, 43}.Contains(x.MainCompanyId)) AndAlso new [] {100, 101}.Contains(x.MainCompanyId))");
276277
}
277278

278279
[Fact]
@@ -326,13 +327,13 @@ public void Parse_ParseInWrappedInParenthesis()
326327
{
327328
// Arrange
328329
ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "x")];
329-
var sut = new ExpressionParser(parameters, "(MainCompanyId in @0)", [new long?[] { 1, 2 }], null);
330+
var sut = new ExpressionParser(parameters, "(MainCompanyId in @0)", [new long?[] { 1, (long) int.MaxValue + 1 }], null);
330331

331332
// Act
332333
var parsedExpression = sut.Parse(null).ToString();
333334

334335
// Assert
335-
Check.That(parsedExpression).Equals("value(System.Nullable`1[System.Int64][]).Contains(x.MainCompanyId)");
336+
Check.That(parsedExpression).Equals("new [] {1, 2147483648}.Contains(x.MainCompanyId)");
336337
}
337338

338339
[Fact]

0 commit comments

Comments
 (0)