Skip to content

Commit 2066dc0

Browse files
committed
Refactor In expression
1 parent 55b4865 commit 2066dc0

21 files changed

Lines changed: 412 additions & 184 deletions

src-console/ConsoleApp_net10/Program.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,24 @@ public class GroupedSalesData
3838
public int GroupLevel { get; set; }
3939
}
4040

41+
class MyEntity
42+
{
43+
// Factory method to create a list of MyEntity objects from a given list of ids
44+
public static IEnumerable<MyEntity> CreateList(IEnumerable<int> ids)
45+
{
46+
foreach (var id in ids) yield return new MyEntity { Id = id };
47+
}
48+
49+
public int Id { get; set; }
50+
}
51+
4152
class Program
4253
{
4354
static void Main(string[] args)
4455
{
56+
Issue987();
57+
return;
58+
4559
Issue918();
4660
return;
4761

@@ -72,6 +86,52 @@ static void Main(string[] args)
7286
Dynamic();
7387
}
7488

89+
private static void Issue987()
90+
{
91+
var list = new List<MyEntity>();
92+
for (int i = 0; i < 10000; i++)
93+
list.Add(new MyEntity { Id = i });
94+
95+
var test1 = list.AsQueryable()
96+
.Where("Id in (9495, 9496, 9498, 9500, 9501, 9503, 9505, 9508, 9509, 9510, 9511, 9514, 9515, 9517, 9518, 9519, 9520, 9521, 9523, 9524, 9525, 9526, 9527, 9528, 9529, 9530, 9531, 9532, 9533, 9534, 9535, 9536, 9538, 9539, 9540, 9541, 9542, 9543, 9544, 9545, 9546, 9547, 9548, 9549, 9550, 9552, 9554, 9556, 9557, 9558, 9559, 9560, 9561, 9562, 9563, 9565, 9567, 9569, 9570, 9575, 9576, 9577, 9578, 9579, 9580, 9581, 9582, 9583, 9584, 9585, 9586, 9587, 9588, 9589, 9590, 9591, 9592, 9593, 9594, 9595, 9596, 9597, 9598, 9599, 9600, 9601, 9602, 9603, 9604, 9605, 9606, 9607, 9608, 9609, 9610, 9611, 9612, 9613, 9614, 9615, 9616, 9617, 9618, 9619, 9620, 9621, 9622, 9623, 9624, 9625, 9626, 9627, 9628, 9629)")
97+
.ToList();
98+
99+
Console.WriteLine("Number of elements : " + test1.Count);
100+
101+
//the list of ids that were actually used in our application and resulted in the discovery of this bug
102+
var originalIdList = new List<int>() { 9495, 9496, 9498, 9500, 9501, 9503, 9505, 9508, 9509, 9510, 9511, 9514, 9515, 9517, 9518, 9519, 9520, 9521, 9523, 9524, 9525, 9526, 9527, 9528, 9529, 9530, 9531, 9532, 9533, 9534, 9535, 9536, 9538, 9539, 9540, 9541, 9542, 9543, 9544, 9545, 9546, 9547, 9548, 9549, 9550, 9552, 9554, 9556, 9557, 9558, 9559, 9560, 9561, 9562, 9563, 9565, 9567, 9569, 9570, 9575, 9576, 9577, 9578, 9579, 9580, 9581, 9582, 9583, 9584, 9585, 9586, 9587, 9588, 9589, 9590, 9591, 9592, 9593, 9594, 9595, 9596, 9597, 9598, 9599, 9600, 9601, 9602, 9603, 9604, 9605, 9606, 9607, 9608, 9609, 9610, 9611, 9612, 9613, 9614, 9615, 9616, 9617, 9618, 9619, 9620, 9621, 9622, 9623, 9624, 9625, 9626, 9627, 9628, 9629 };
103+
//list of ids also starting at 9495, but without gaps
104+
var adjacentIdList = Enumerable.Range(9495, 114);
105+
//original list starting at Id1
106+
var originalIdListStartingAt1 = originalIdList.Select(id => id - 9494);
107+
//list with gaps of 1
108+
var listWithGapsOf1 = Enumerable.Range(1, 114).Select(id => id * 2);
109+
//list with gaps of 2
110+
var listWithGapsOf2 = Enumerable.Range(1, 114).Select(id => id * 3);
111+
//list with gaps of 3
112+
var listWithGapsOf3 = Enumerable.Range(1, 114).Select(id => id * 4);
113+
//list with gaps of 4
114+
var listWithGapsOf4 = Enumerable.Range(1, 114).Select(id => id * 5);
115+
116+
//list of 10.000 entities , with ids starting at 0
117+
var entityList = MyEntity.CreateList(Enumerable.Range(1, 10_000));
118+
119+
//filter the list of entities by the list of ids using dynamic linq and write the number of elements in the filtered list to the console
120+
static void Filter(IEnumerable<MyEntity> entities, IEnumerable<int> ids)
121+
{
122+
var filtered = entities.AsQueryable().Where($"Id in ({string.Join(',', ids)})").ToList();
123+
Console.WriteLine("Number of elements : " + filtered.Count);
124+
}
125+
126+
Filter(entityList, originalIdList);
127+
Filter(entityList, adjacentIdList);
128+
Filter(entityList, originalIdListStartingAt1);
129+
Filter(entityList, listWithGapsOf1);
130+
Filter(entityList, listWithGapsOf2);
131+
Filter(entityList, listWithGapsOf3);
132+
Filter(entityList, listWithGapsOf4);
133+
}
134+
75135
private static void Issue918()
76136
{
77137
var persons = new DataTable();

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ namespace System.Linq.Dynamic.Core
2424
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
2525
public static class DynamicQueryableExtensions
2626
{
27-
#if !(SILVERLIGHT)
27+
#if !SILVERLIGHT
2828
private static readonly TraceSource TraceSource = new(nameof(DynamicQueryableExtensions));
2929
#endif
3030

@@ -34,7 +34,7 @@ private static Expression OptimizeExpression(Expression expression)
3434
{
3535
var optimized = ExtensibilityPoint.QueryOptimizer(expression);
3636

37-
#if !(SILVERLIGHT)
37+
#if !SILVERLIGHT
3838
if (optimized != expression)
3939
{
4040
TraceSource.TraceEvent(TraceEventType.Verbose, 0, "Expression before : {0}", expression);
@@ -2094,7 +2094,7 @@ public static IQueryable SelectMany(
20942094
string collectionParameterName,
20952095
string resultParameterName,
20962096
object?[]? collectionSelectorArgs = null,
2097-
params object[]? resultSelectorArgs)
2097+
params object?[]? resultSelectorArgs)
20982098
{
20992099
Check.NotNull(source);
21002100
Check.NotNull(config);
@@ -2682,7 +2682,6 @@ public static IQueryable Where(this IQueryable source, ParsingConfig config, str
26822682

26832683
bool createParameterCtor = SupportsLinqToObjects(config, source);
26842684
LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args);
2685-
26862685
var optimized = OptimizeExpression(Expression.Call(typeof(Queryable), nameof(Queryable.Where), [source.ElementType], source.Expression, Expression.Quote(lambda)));
26872686
return source.Provider.CreateQuery(optimized);
26882687
}

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,37 @@ public Expression GenerateNotEqual(Expression left, Expression right)
156156
return Expression.NotEqual(left, right);
157157
}
158158

159+
public Expression GenerateBinaryOrElseTree(IList<Expression> expressions)
160+
{
161+
Check.NotNullOrEmpty(expressions);
162+
163+
if (expressions.Count == 1)
164+
{
165+
return expressions[0];
166+
}
167+
168+
var currentLevel = new List<Expression>(expressions);
169+
while (currentLevel.Count > 1)
170+
{
171+
var nextLevel = new List<Expression>((currentLevel.Count + 1) / 2);
172+
for (var i = 0; i < currentLevel.Count; i += 2)
173+
{
174+
if (i + 1 < currentLevel.Count)
175+
{
176+
nextLevel.Add(Expression.OrElse(currentLevel[i], currentLevel[i + 1]));
177+
}
178+
else
179+
{
180+
nextLevel.Add(currentLevel[i]);
181+
}
182+
}
183+
184+
currentLevel = nextLevel;
185+
}
186+
187+
return currentLevel[0];
188+
}
189+
159190
public Expression GenerateGreaterThan(Expression left, Expression right)
160191
{
161192
TryConvertTypes(ref left, ref right);

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

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,12 @@ private Expression ParseIn()
367367

368368
if (_textParser.CurrentToken.Id == TokenId.OpenParen) // literals (or other inline list)
369369
{
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+
370376
while (_textParser.CurrentToken.Id != TokenId.CloseParen)
371377
{
372378
_textParser.NextToken();
@@ -393,13 +399,26 @@ private Expression ParseIn()
393399
CheckAndPromoteOperands(typeof(IEqualitySignatures), TokenId.DoubleEqual, "==", ref left, ref right, token.Pos);
394400
}
395401

396-
if (accumulate.Type != typeof(bool))
402+
var equalsExpression = _expressionHelper.GenerateEqual(left, right);
403+
comparisons.Add(equalsExpression);
404+
405+
if (canUseContains && equalsExpression is BinaryExpression binaryExpression && binaryExpression.NodeType == ExpressionType.Equal)
397406
{
398-
accumulate = _expressionHelper.GenerateEqual(left, right);
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+
}
399418
}
400419
else
401420
{
402-
accumulate = Expression.OrElse(accumulate, _expressionHelper.GenerateEqual(left, right));
421+
canUseContains = false;
403422
}
404423

405424
if (_textParser.CurrentToken.Id == TokenId.End)
@@ -408,6 +427,17 @@ private Expression ParseIn()
408427
}
409428
}
410429

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+
}
440+
411441
// Since this started with an open paren, make sure to move off the close
412442
_textParser.NextToken();
413443
}
@@ -1514,7 +1544,7 @@ private Expression ParseNew()
15141544
{
15151545
if (!propertyNames.Add(propName!))
15161546
{
1517-
throw ParseError(exprPos, Res.DuplicateIdentifier, propName);
1547+
throw ParseError(exprPos, Res.DuplicateIdentifier, propName!);
15181548
}
15191549

15201550
properties.Add(new DynamicProperty(propName!, expr.Type));

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Diagnostics.CodeAnalysis;
1+
using System.Collections.Generic;
2+
using System.Diagnostics.CodeAnalysis;
23
using System.Linq.Expressions;
34

45
namespace System.Linq.Dynamic.Core.Parser;
@@ -21,6 +22,8 @@ internal interface IExpressionHelper
2122

2223
Expression GenerateNotEqual(Expression left, Expression right);
2324

25+
Expression GenerateBinaryOrElseTree(IList<Expression> expressions);
26+
2427
Expression GenerateStringConcat(Expression left, Expression right);
2528

2629
Expression GenerateSubtract(Expression left, Expression right);
Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
using System.Linq.Expressions;
22
using System.Reflection;
33

4-
namespace System.Linq.Dynamic.Core.Parser.SupportedMethods
4+
namespace System.Linq.Dynamic.Core.Parser.SupportedMethods;
5+
6+
internal class MethodData
57
{
6-
internal class MethodData
7-
{
8-
public MethodBase MethodBase { get; set; }
9-
public ParameterInfo[] Parameters { get; set; }
10-
public Expression[] Args { get; set; }
11-
}
12-
}
8+
public MethodBase MethodBase { get; set; }
9+
10+
public ParameterInfo[] Parameters { get; set; }
11+
12+
public Expression[] Args { get; set; }
13+
}

src/System.Linq.Dynamic.Core/Validation/Check.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ internal static class Check
1010
{
1111
private const string ParsingConfigError = "The ParsingConfig should be provided as first argument to this method.";
1212

13-
public static object?[]? Args(object?[]? args, [CallerArgumentExpression("args")] string? parameterName = null)
13+
public static object?[]? Args(object?[]? args, [CallerArgumentExpression(nameof(args))] string? parameterName = null)
1414
{
1515
if (args?.Any(a => a is ParsingConfig) == true)
1616
{
@@ -20,7 +20,7 @@ internal static class Check
2020
return args;
2121
}
2222

23-
public static T Condition<T>(T value, Predicate<T> predicate, [CallerArgumentExpression("value")] string? parameterName = null)
23+
public static T Condition<T>(T value, Predicate<T> predicate, [CallerArgumentExpression(nameof(value))] string? parameterName = null)
2424
{
2525
NotNull(predicate);
2626

@@ -34,7 +34,7 @@ public static T Condition<T>(T value, Predicate<T> predicate, [CallerArgumentExp
3434
return value;
3535
}
3636

37-
public static T NotNull<T>(T value, [CallerArgumentExpression("value")] string? parameterName = null)
37+
public static T NotNull<T>(T value, [CallerArgumentExpression(nameof(value))] string? parameterName = null)
3838
{
3939
if (value is null)
4040
{
@@ -59,7 +59,7 @@ public static T NotNull<T>(T value, string parameterName, string propertyName)
5959
return value;
6060
}
6161

62-
public static IEnumerable<T> NotNullOrEmpty<T>(IEnumerable<T> value, [CallerArgumentExpression("value")] string? parameterName = null)
62+
public static IEnumerable<T> NotNullOrEmpty<T>(IEnumerable<T> value, [CallerArgumentExpression(nameof(value))] string? parameterName = null)
6363
{
6464
IEnumerable<T> result = NotNull(value, parameterName);
6565

@@ -75,10 +75,10 @@ public static IEnumerable<T> NotNullOrEmpty<T>(IEnumerable<T> value, [CallerArgu
7575
return result;
7676
}
7777

78-
public static string NotEmpty(string? value, [CallerArgumentExpression("value")] string? parameterName = null) =>
78+
public static string NotEmpty(string? value, [CallerArgumentExpression(nameof(value))] string? parameterName = null) =>
7979
NotNullOrWhiteSpace(value, parameterName);
8080

81-
public static string NotNullOrEmpty(string? value, [CallerArgumentExpression("value")] string? parameterName = null)
81+
public static string NotNullOrEmpty(string? value, [CallerArgumentExpression(nameof(value))] string? parameterName = null)
8282
{
8383
if (value is null)
8484
{
@@ -95,7 +95,7 @@ public static string NotNullOrEmpty(string? value, [CallerArgumentExpression("va
9595
return value;
9696
}
9797

98-
public static string NotNullOrWhiteSpace(string? value, [CallerArgumentExpression("value")] string? parameterName = null)
98+
public static string NotNullOrWhiteSpace(string? value, [CallerArgumentExpression(nameof(value))] string? parameterName = null)
9999
{
100100
if (value is null)
101101
{
@@ -112,7 +112,7 @@ public static string NotNullOrWhiteSpace(string? value, [CallerArgumentExpressio
112112
return value;
113113
}
114114

115-
public static IEnumerable<T> HasNoNulls<T>(IEnumerable<T> value, [CallerArgumentExpression("value")] string? parameterName = null)
115+
public static IEnumerable<T> HasNoNulls<T>(IEnumerable<T> value, [CallerArgumentExpression(nameof(value))] string? parameterName = null)
116116
{
117117
if (value is null)
118118
{

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ private class ComplexParseLambda1Result
8787
{
8888
public int? Age;
8989
public int TotalIncome;
90-
public string Name;
90+
public string? Name;
9191
}
9292

9393
[DynamicLinqType]

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
#if EFCORE
1+
using System.Linq.Dynamic.Core.Tests.Helpers.Entities;
2+
3+
#if EFCORE
4+
using System.Collections.Generic;
25
using Microsoft.EntityFrameworkCore;
36
#else
47
using System.Data.Entity;
@@ -25,4 +28,40 @@ public void Entities_Where_In_And()
2528
// Assert
2629
Assert.Equal(expected, test);
2730
}
31+
32+
[Fact]
33+
public void Entities_Where_In_DifferentTypes()
34+
{
35+
// Arrange
36+
var expected = _context.Blogs.Include(b => b.Posts).Where(b => new long[] { 1000, 1001, 1002 }.Contains(b.BlogLongId)).ToArray();
37+
38+
// Act
39+
var test = _context.Blogs.Include(b => b.Posts).Where(@"BlogLongId in (1000, 1001, 1002)").ToArray();
40+
41+
// Assert
42+
Assert.Equal(expected, test);
43+
}
44+
45+
[Fact]
46+
public void Entities_Where_In_Issue987()
47+
{
48+
// Arrange
49+
for (int i = 0; i < 10000; i++)
50+
{
51+
var blogText = new BlogText
52+
{
53+
Id = i
54+
};
55+
_context.BlogTexts.Add(blogText);
56+
}
57+
_context.SaveChanges();
58+
59+
// Act
60+
var test = _context.BlogTexts
61+
.Where("Id in (9495, 9496, 9498, 9500, 9501, 9503, 9505, 9508, 9509, 9510, 9511, 9514, 9515, 9517, 9518, 9519, 9520, 9521, 9523, 9524, 9525, 9526, 9527, 9528, 9529, 9530, 9531, 9532, 9533, 9534, 9535, 9536, 9538, 9539, 9540, 9541, 9542, 9543, 9544, 9545, 9546, 9547, 9548, 9549, 9550, 9552, 9554, 9556, 9557, 9558, 9559, 9560, 9561, 9562, 9563, 9565, 9567, 9569, 9570, 9575, 9576, 9577, 9578, 9579, 9580, 9581, 9582, 9583, 9584, 9585, 9586, 9587, 9588, 9589, 9590, 9591, 9592, 9593, 9594, 9595, 9596, 9597, 9598, 9599, 9600, 9601, 9602, 9603, 9604, 9605, 9606, 9607, 9608, 9609, 9610, 9611, 9612, 9613, 9614, 9615, 9616, 9617, 9618, 9619, 9620, 9621, 9622, 9623, 9624, 9625, 9626, 9627, 9628, 9629)")
62+
.ToList();
63+
64+
// Assert
65+
Assert.Equal(114, test.Count);
66+
}
2867
}

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@ public partial class EntitiesTests
99
[Fact(Skip = "not supported")]
1010
public void Entities_TakeWhile()
1111
{
12-
// Arrange
13-
const int total = 33;
14-
1512
// Act
1613
var expected = _context.Blogs.OrderBy(b => b.BlogId).TakeWhile(b => b.BlogId > 5).ToArray();
1714
var result = _context.Blogs.OrderBy("BlogId").TakeWhile("b.BlogId > 5").ToDynamicArray<Blog>();

0 commit comments

Comments
 (0)