diff --git a/CHANGELOG.md b/CHANGELOG.md index 19a7a995..41cf8a02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# v1.6.4 (19 May 2025) +- [#915](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/915) - Add support for "not in" and "not_in" [feature] contributed by [StefH](https://github.com/StefH) +- [#923](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/923) - Fix MethodFinder TryFindAggregateMethod to support array [bug] contributed by [StefH](https://github.com/StefH) +- [#925](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/925) - Add extra unittests for NullPropagation / ToString / AllowEqualsAndToStringMethodsOnObject is true [test] contributed by [StefH](https://github.com/StefH) +- [#926](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/926) - Add validation when passing ParsingConfig in args [feature] contributed by [StefH](https://github.com/StefH) +- [#914](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/914) - Add support for "not in" [feature] +- [#919](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/919) - Calling Sum in a Sum throws an InvalidOperationException [bug] + # v1.6.3 (09 May 2025) - [#922](https://github.com/zzzprojects/System.Linq.Dynamic.Core/pull/922) - Update DynamicGetMemberBinder to only add BindingRestrictions for dynamic type and cache the DynamicMetaObject [bug] contributed by [StefH](https://github.com/StefH) - [#921](https://github.com/zzzprojects/System.Linq.Dynamic.Core/issues/921) - Strange Performance issue after upgrading from 1.6.0.2 to 1.6.2 [bug] diff --git a/Generate-ReleaseNotes.bat b/Generate-ReleaseNotes.bat index 3b983077..d81555a9 100644 --- a/Generate-ReleaseNotes.bat +++ b/Generate-ReleaseNotes.bat @@ -1,5 +1,5 @@ rem https://github.com/StefH/GitHubReleaseNotes -SET version=v1.6.3 +SET version=v1.6.4 GitHubReleaseNotes --output CHANGELOG.md --exclude-labels known_issue out_of_scope not_planned invalid question documentation wontfix environment duplicate --language en --version %version% --token %GH_TOKEN% diff --git a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs index 65365eae..c5d52287 100644 --- a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs +++ b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs @@ -125,6 +125,7 @@ public static bool All(this IQueryable source, ParsingConfig config, string pred Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -176,6 +177,7 @@ public static bool Any(this IQueryable source, ParsingConfig config, string pred Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -246,6 +248,7 @@ public static double Average(this IQueryable source, ParsingConfig config, strin Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -399,6 +402,7 @@ public static int Count(this IQueryable source, ParsingConfig config, string pre Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -529,6 +533,7 @@ public static dynamic First(this IQueryable source, ParsingConfig config, string Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -601,6 +606,7 @@ public static dynamic FirstOrDefault(this IQueryable source, ParsingConfig confi Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -679,8 +685,9 @@ internal static IQueryable InternalGroupBy(IQueryable source, ParsingConfig conf { Check.NotNull(source); Check.NotNull(config); - Check.NotEmpty(keySelector, nameof(keySelector)); - Check.NotEmpty(resultSelector, nameof(resultSelector)); + Check.NotEmpty(keySelector); + Check.NotEmpty(resultSelector); + Check.Args(args); bool createParameterCtor = config.EvaluateGroupByAtDatabase || SupportsLinqToObjects(config, source); LambdaExpression keyLambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, keySelector, args); @@ -807,7 +814,8 @@ internal static IQueryable InternalGroupBy(IQueryable source, ParsingConfig conf { Check.NotNull(source); Check.NotNull(config); - Check.NotEmpty(keySelector, nameof(keySelector)); + Check.NotEmpty(keySelector); + Check.Args(args); bool createParameterCtor = config.EvaluateGroupByAtDatabase || SupportsLinqToObjects(config, source); LambdaExpression keyLambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, keySelector, args); @@ -932,12 +940,13 @@ private static IEnumerable GroupByManyInternal(IEnumerabl /// An obtained by performing a grouped join on two sequences. public static IQueryable GroupJoin(this IQueryable outer, ParsingConfig config, IEnumerable inner, string outerKeySelector, string innerKeySelector, string resultSelector, params object?[] args) { - Check.NotNull(outer, nameof(outer)); + Check.NotNull(outer); Check.NotNull(config); - Check.NotNull(inner, nameof(inner)); - Check.NotEmpty(outerKeySelector, nameof(outerKeySelector)); - Check.NotEmpty(innerKeySelector, nameof(innerKeySelector)); - Check.NotEmpty(resultSelector, nameof(resultSelector)); + Check.NotNull(inner); + Check.NotEmpty(outerKeySelector); + Check.NotEmpty(innerKeySelector); + Check.NotEmpty(resultSelector); + Check.Args(args); Type outerType = outer.ElementType; Type innerType = inner.AsQueryable().ElementType; @@ -989,12 +998,13 @@ public static IQueryable Join(this IQueryable outer, ParsingConfig config, IEnum { //http://stackoverflow.com/questions/389094/how-to-create-a-dynamic-linq-join-extension-method - Check.NotNull(outer, nameof(outer)); + Check.NotNull(outer); Check.NotNull(config); - Check.NotNull(inner, nameof(inner)); - Check.NotEmpty(outerKeySelector, nameof(outerKeySelector)); - Check.NotEmpty(innerKeySelector, nameof(innerKeySelector)); - Check.NotEmpty(resultSelector, nameof(resultSelector)); + Check.NotNull(inner); + Check.NotEmpty(outerKeySelector); + Check.NotEmpty(innerKeySelector); + Check.NotEmpty(resultSelector); + Check.Args(args); Type outerType = outer.ElementType; Type innerType = inner.AsQueryable().ElementType; @@ -1094,6 +1104,7 @@ public static dynamic Last(this IQueryable source, ParsingConfig config, string Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -1166,6 +1177,7 @@ public static dynamic LastOrDefault(this IQueryable source, ParsingConfig config Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -1244,6 +1256,7 @@ public static long LongCount(this IQueryable source, ParsingConfig config, strin Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -1316,6 +1329,7 @@ public static object Max(this IQueryable source, ParsingConfig config, string pr Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, typeof(object), predicate, args); @@ -1388,6 +1402,7 @@ public static object Min(this IQueryable source, ParsingConfig config, string pr Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, typeof(object), predicate, args); @@ -1545,6 +1560,8 @@ public static IOrderedQueryable OrderBy(this IQueryable public static IOrderedQueryable OrderBy(this IQueryable source, ParsingConfig config, string ordering, params object?[] args) { + Check.Args(args); + if (args.Length > 0 && args[0] != null && args[0]!.GetType().GetInterfaces().Any(i => i.Name.Contains("IComparer`1"))) { return InternalOrderBy(source, config, ordering, args[0]!, args); @@ -1584,6 +1601,7 @@ internal static IOrderedQueryable InternalOrderBy(IQueryable source, ParsingConf Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(ordering); + Check.Args(args); ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(source.ElementType, string.Empty, config.RenameEmptyParameterExpressionNames)]; var parser = new ExpressionParser(parameters, ordering, args, config, true); @@ -1758,6 +1776,7 @@ public static IQueryable Select(this IQueryable source, ParsingConfig config, st Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(selector); + Check.Args(args); bool createParameterCtor = config.EvaluateGroupByAtDatabase || SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, selector, args); @@ -1799,6 +1818,7 @@ public static IQueryable Select(this IQueryable source, Parsin Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(selector); + Check.Args(args); bool createParameterCtor = config.EvaluateGroupByAtDatabase || SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, typeof(TResult), selector, args); @@ -1841,8 +1861,9 @@ public static IQueryable Select(this IQueryable source, ParsingConfig config, Ty { Check.NotNull(source); Check.NotNull(config); - Check.NotNull(resultType, nameof(resultType)); + Check.NotNull(resultType); Check.NotEmpty(selector); + Check.Args(args); bool createParameterCtor = config.EvaluateGroupByAtDatabase || SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, resultType, selector, args); @@ -1907,6 +1928,7 @@ public static IQueryable SelectMany(this IQueryable source, ParsingConfig config Check.NotNull(config); Check.NotNull(resultType); Check.NotEmpty(selector); + Check.Args(args); return SelectManyInternal(source, config, resultType, selector, args); } @@ -1978,6 +2000,7 @@ public static IQueryable SelectMany(this IQueryable source, Pa Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(selector); + Check.Args(args); bool createParameterCtor = config.EvaluateGroupByAtDatabase || SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, selector, args); @@ -2076,10 +2099,12 @@ public static IQueryable SelectMany( { Check.NotNull(source); Check.NotNull(config); - Check.NotEmpty(collectionSelector, nameof(collectionSelector)); - Check.NotEmpty(collectionParameterName, nameof(collectionParameterName)); - Check.NotEmpty(resultSelector, nameof(resultSelector)); - Check.NotEmpty(resultParameterName, nameof(resultParameterName)); + Check.NotEmpty(collectionSelector); + Check.NotEmpty(collectionParameterName); + Check.NotEmpty(resultSelector); + Check.NotEmpty(resultParameterName); + Check.Args(collectionSelectorArgs); + Check.Args(resultSelectorArgs); bool createParameterCtor = config.EvaluateGroupByAtDatabase || SupportsLinqToObjects(config, source); LambdaExpression sourceSelectLambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, collectionSelector, collectionSelectorArgs); @@ -2227,6 +2252,7 @@ public static dynamic SingleOrDefault(this IQueryable source, ParsingConfig conf Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -2309,6 +2335,7 @@ public static IQueryable SkipWhile(this IQueryable source, ParsingConfig config, Check.NotNull(source); Check.NotNull(config); Check.NotNull(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -2365,6 +2392,7 @@ public static object Sum(this IQueryable source, ParsingConfig config, string pr Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -2439,6 +2467,7 @@ public static IQueryable TakeWhile(this IQueryable source, ParsingConfig config, Check.NotNull(source); Check.NotNull(config); Check.NotNull(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); @@ -2553,6 +2582,7 @@ internal static IOrderedQueryable InternalThenBy(IOrderedQueryable source, Parsi Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(ordering); + Check.Args(args); ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(source.ElementType, string.Empty, config.RenameEmptyParameterExpressionNames) }; ExpressionParser parser = new ExpressionParser(parameters, ordering, args, config); @@ -2649,6 +2679,7 @@ public static IQueryable Where(this IQueryable source, ParsingConfig config, str Check.NotNull(source); Check.NotNull(config); Check.NotEmpty(predicate); + Check.Args(args); bool createParameterCtor = SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, null, predicate, args); diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs index cc1ef839..05b26969 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionHelper.cs @@ -328,7 +328,7 @@ public bool TryGenerateAndAlsoNotNullExpression(Expression sourceExpression, boo { var expressions = CollectExpressions(addSelf, sourceExpression); - if (expressions.Count == 1 && !(expressions[0] is MethodCallExpression)) + if (expressions.Count == 1 && expressions[0] is not MethodCallExpression) { generatedExpression = sourceExpression; return false; diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index c2fb0102..b4a7245f 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -218,11 +218,11 @@ internal IList ParseOrdering(bool forceThenBy = false) { var expr = ParseConditionalOperator(); var ascending = true; - if (TokenIdentifierIs("asc") || TokenIdentifierIs("ascending")) + if (TokenIsIdentifier("asc") || TokenIsIdentifier("ascending")) { _textParser.NextToken(); } - else if (TokenIdentifierIs("desc") || TokenIdentifierIs("descending")) + else if (TokenIsIdentifier("desc") || TokenIsIdentifier("descending")) { _textParser.NextToken(); ascending = false; @@ -337,19 +337,34 @@ private Expression ParseAndOperator() return left; } - // in operator for literals - example: "x in (1,2,3,4)" - // in operator to mimic contains - example: "x in @0", compare to @0.Contains(x) - // Adapted from ticket submitted by github user mlewis9548 + // "in" / "not in" / "not_in" operator for literals - example: "x in (1,2,3,4)" + // "in" / "not in" / "not_in" operator to mimic contains - example: "x in @0", compare to @0.Contains(x) private Expression ParseIn() { Expression left = ParseLogicalAndOrOperator(); Expression accumulate = left; - while (TokenIdentifierIs("in")) + while (_textParser.TryGetToken(["in", "not_in", "not"], [TokenId.Exclamation], out var token)) { - var op = _textParser.CurrentToken; + var not = false; + if (token.Text == "not_in") + { + not = true; + } + else if (token.Text == "not" || token.Id == TokenId.Exclamation) + { + not = true; + + _textParser.NextToken(); + + if (!TokenIsIdentifier("in")) + { + throw ParseError(token.Pos, Res.TokenExpected, "in"); + } + } _textParser.NextToken(); + if (_textParser.CurrentToken.Id == TokenId.OpenParen) // literals (or other inline list) { while (_textParser.CurrentToken.Id != TokenId.CloseParen) @@ -364,18 +379,18 @@ private Expression ParseIn() { if (right is ConstantExpression constantExprRight) { - right = ParseEnumToConstantExpression(op.Pos, left.Type, constantExprRight); + right = ParseEnumToConstantExpression(token.Pos, left.Type, constantExprRight); } else if (_expressionHelper.TryUnwrapAsConstantExpression(right, out var unwrappedConstantExprRight)) { - right = ParseEnumToConstantExpression(op.Pos, left.Type, unwrappedConstantExprRight); + right = ParseEnumToConstantExpression(token.Pos, left.Type, unwrappedConstantExprRight); } } // else, check for direct type match else if (left.Type != right.Type) { - CheckAndPromoteOperands(typeof(IEqualitySignatures), TokenId.DoubleEqual, "==", ref left, ref right, op.Pos); + CheckAndPromoteOperands(typeof(IEqualitySignatures), TokenId.DoubleEqual, "==", ref left, ref right, token.Pos); } if (accumulate.Type != typeof(bool)) @@ -389,7 +404,7 @@ private Expression ParseIn() if (_textParser.CurrentToken.Id == TokenId.End) { - throw ParseError(op.Pos, Res.CloseParenOrCommaExpected); + throw ParseError(token.Pos, Res.CloseParenOrCommaExpected); } } @@ -413,7 +428,12 @@ private Expression ParseIn() } else { - throw ParseError(op.Pos, Res.OpenParenOrIdentifierExpected); + throw ParseError(token.Pos, Res.OpenParenOrIdentifierExpected); + } + + if (not) + { + accumulate = Expression.Not(accumulate); } } @@ -759,7 +779,7 @@ private Expression ParseAdditive() private Expression ParseArithmetic() { Expression left = ParseUnary(); - while (_textParser.CurrentToken.Id is TokenId.Asterisk or TokenId.Slash or TokenId.Percent || TokenIdentifierIs("mod")) + while (_textParser.CurrentToken.Id is TokenId.Asterisk or TokenId.Slash or TokenId.Percent || TokenIsIdentifier("mod")) { Token op = _textParser.CurrentToken; _textParser.NextToken(); @@ -787,11 +807,11 @@ private Expression ParseArithmetic() // -, !, not unary operators private Expression ParseUnary() { - if (_textParser.CurrentToken.Id == TokenId.Minus || _textParser.CurrentToken.Id == TokenId.Exclamation || TokenIdentifierIs("not")) + if (_textParser.CurrentToken.Id == TokenId.Minus || _textParser.CurrentToken.Id == TokenId.Exclamation || TokenIsIdentifier("not")) { Token op = _textParser.CurrentToken; _textParser.NextToken(); - if (op.Id == TokenId.Minus && (_textParser.CurrentToken.Id == TokenId.IntegerLiteral || _textParser.CurrentToken.Id == TokenId.RealLiteral)) + if (op.Id == TokenId.Minus && _textParser.CurrentToken.Id is TokenId.IntegerLiteral or TokenId.RealLiteral) { _textParser.CurrentToken.Text = "-" + _textParser.CurrentToken.Text; _textParser.CurrentToken.Pos = op.Pos; @@ -1445,7 +1465,7 @@ private Expression ParseNew() if (!arrayInitializer) { string? propName; - if (TokenIdentifierIs("as")) + if (TokenIsIdentifier("as")) { _textParser.NextToken(); propName = GetIdentifierAs(); @@ -2527,11 +2547,11 @@ private static Exception IncompatibleOperandsError(string opName, Expression lef #endif } - private bool TokenIdentifierIs(string id) + private bool TokenIsIdentifier(string id) { - return _textParser.CurrentToken.Id == TokenId.Identifier && string.Equals(id, _textParser.CurrentToken.Text, StringComparison.OrdinalIgnoreCase); + return _textParser.TokenIsIdentifier(id); } - + private string GetIdentifier() { _textParser.ValidateToken(TokenId.Identifier, Res.IdentifierExpected); diff --git a/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs b/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs index 32d8315e..30877e29 100644 --- a/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs +++ b/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs @@ -10,6 +10,7 @@ internal class MethodFinder { private readonly ParsingConfig _parsingConfig; private readonly IExpressionHelper _expressionHelper; + private readonly IDictionary _cachedMethods; /// /// #794 @@ -43,19 +44,32 @@ public MethodFinder(ParsingConfig parsingConfig, IExpressionHelper expressionHel { _parsingConfig = Check.NotNull(parsingConfig); _expressionHelper = Check.NotNull(expressionHelper); + _cachedMethods = new Dictionary + { + { typeof(Enumerable), typeof(Enumerable).GetMethods().Where(m => !m.IsGenericMethodDefinition).ToArray() }, + { typeof(Queryable), typeof(Queryable).GetMethods().Where(m => !m.IsGenericMethodDefinition).ToArray() } + }; } public bool TryFindAggregateMethod(Type callType, string methodName, Type parameterType, [NotNullWhen(true)] out MethodInfo? aggregateMethod) { - aggregateMethod = callType - .GetMethods() - .Where(m => m.Name == methodName && !m.IsGenericMethodDefinition) - .SelectMany(m => m.GetParameters(), (m, p) => new { Method = m, Parameter = p }) - .Where(x => x.Parameter.ParameterType == parameterType) - .Select(x => x.Method) - .FirstOrDefault(); - - return aggregateMethod != null; + var nonGenericMethodsByName = _cachedMethods[callType] + .Where(m => m.Name == methodName) + .ToArray(); + + if (TypeHelper.TryGetAsEnumerable(parameterType, out var parameterTypeAsEnumerable)) + { + aggregateMethod = nonGenericMethodsByName + .SelectMany(m => m.GetParameters(), (m, p) => new { Method = m, Parameter = p }) + .Where(x => x.Parameter.ParameterType == parameterTypeAsEnumerable) + .Select(x => x.Method) + .FirstOrDefault(); + + return aggregateMethod != null; + } + + aggregateMethod = null; + return false; } public bool CheckAggregateMethodAndTryUpdateArgsToMatchMethodArgs(string methodName, ref Expression[] args) diff --git a/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs b/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs index 843732f7..6ffd2a19 100644 --- a/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/TypeHelper.cs @@ -6,6 +6,24 @@ namespace System.Linq.Dynamic.Core.Parser; internal static class TypeHelper { + internal static bool TryGetAsEnumerable(Type type, [NotNullWhen(true)] out Type? enumerableType) + { + if (type.IsArray) + { + enumerableType = typeof(IEnumerable<>).MakeGenericType(type.GetElementType()!); + return true; + } + + if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + enumerableType = type; + return true; + } + + enumerableType = null; + return false; + } + public static bool TryGetFirstGenericArgument(Type type, [NotNullWhen(true)] out Type? genericType) { var genericArguments = type.GetTypeInfo().GetGenericTypeArguments(); @@ -196,79 +214,79 @@ public static bool IsCompatibleWith(Type source, Type target) } return false; #else - if (source == target) - { - return true; - } + if (source == target) + { + return true; + } - if (!target.GetTypeInfo().IsValueType) - { - return target.IsAssignableFrom(source); - } + if (!target.GetTypeInfo().IsValueType) + { + return target.IsAssignableFrom(source); + } - Type st = GetNonNullableType(source); - Type tt = GetNonNullableType(target); + Type st = GetNonNullableType(source); + Type tt = GetNonNullableType(target); - if (st != source && tt == target) - { - return false; - } + if (st != source && tt == target) + { + return false; + } - Type sc = st.GetTypeInfo().IsEnum ? typeof(object) : st; - Type tc = tt.GetTypeInfo().IsEnum ? typeof(object) : tt; + Type sc = st.GetTypeInfo().IsEnum ? typeof(object) : st; + Type tc = tt.GetTypeInfo().IsEnum ? typeof(object) : tt; - if (sc == typeof(sbyte)) - { - if (tc == typeof(sbyte) || tc == typeof(short) || tc == typeof(int) || tc == typeof(long) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) - return true; - } - else if (sc == typeof(byte)) - { - if (tc == typeof(byte) || tc == typeof(short) || tc == typeof(ushort) || tc == typeof(int) || tc == typeof(uint) || tc == typeof(long) || tc == typeof(ulong) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) - return true; - } - else if (sc == typeof(short)) - { - if (tc == typeof(short) || tc == typeof(int) || tc == typeof(long) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) - return true; - } - else if (sc == typeof(ushort)) - { - if (tc == typeof(ushort) || tc == typeof(int) || tc == typeof(uint) || tc == typeof(long) || tc == typeof(ulong) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) - return true; - } - else if (sc == typeof(int)) - { - if (tc == typeof(int) || tc == typeof(long) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) - return true; - } - else if (sc == typeof(uint)) - { - if (tc == typeof(uint) || tc == typeof(long) || tc == typeof(ulong) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) - return true; - } - else if (sc == typeof(long)) - { - if (tc == typeof(long) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) - return true; - } - else if (sc == typeof(ulong)) - { - if (tc == typeof(ulong) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) - return true; - } - else if (sc == typeof(float)) - { - if (tc == typeof(float) || tc == typeof(double)) - return true; - } - - if (st == tt) - { + if (sc == typeof(sbyte)) + { + if (tc == typeof(sbyte) || tc == typeof(short) || tc == typeof(int) || tc == typeof(long) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) return true; - } + } + else if (sc == typeof(byte)) + { + if (tc == typeof(byte) || tc == typeof(short) || tc == typeof(ushort) || tc == typeof(int) || tc == typeof(uint) || tc == typeof(long) || tc == typeof(ulong) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) + return true; + } + else if (sc == typeof(short)) + { + if (tc == typeof(short) || tc == typeof(int) || tc == typeof(long) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) + return true; + } + else if (sc == typeof(ushort)) + { + if (tc == typeof(ushort) || tc == typeof(int) || tc == typeof(uint) || tc == typeof(long) || tc == typeof(ulong) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) + return true; + } + else if (sc == typeof(int)) + { + if (tc == typeof(int) || tc == typeof(long) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) + return true; + } + else if (sc == typeof(uint)) + { + if (tc == typeof(uint) || tc == typeof(long) || tc == typeof(ulong) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) + return true; + } + else if (sc == typeof(long)) + { + if (tc == typeof(long) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) + return true; + } + else if (sc == typeof(ulong)) + { + if (tc == typeof(ulong) || tc == typeof(float) || tc == typeof(double) || tc == typeof(decimal)) + return true; + } + else if (sc == typeof(float)) + { + if (tc == typeof(float) || tc == typeof(double)) + return true; + } - return false; + if (st == tt) + { + return true; + } + + return false; #endif } @@ -391,19 +409,19 @@ private static int GetNumericTypeKind(Type type) return 0; } #else - if (type.GetTypeInfo().IsEnum) - { - return 0; - } + if (type.GetTypeInfo().IsEnum) + { + return 0; + } - if (type == typeof(char) || type == typeof(float) || type == typeof(double) || type == typeof(decimal)) - return 1; - if (type == typeof(sbyte) || type == typeof(short) || type == typeof(int) || type == typeof(long)) - return 2; - if (type == typeof(byte) || type == typeof(ushort) || type == typeof(uint) || type == typeof(ulong)) - return 3; + if (type == typeof(char) || type == typeof(float) || type == typeof(double) || type == typeof(decimal)) + return 1; + if (type == typeof(sbyte) || type == typeof(short) || type == typeof(int) || type == typeof(long)) + return 2; + if (type == typeof(byte) || type == typeof(ushort) || type == typeof(uint) || type == typeof(ulong)) + return 3; - return 0; + return 0; #endif } @@ -484,7 +502,7 @@ private static void AddInterface(ICollection types, Type type) public static bool TryParseEnum(string value, Type? type, [NotNullWhen(true)] out object? enumValue) { - if (type is { } && type.GetTypeInfo().IsEnum && Enum.IsDefined(type, value)) + if (type != null && type.GetTypeInfo().IsEnum && Enum.IsDefined(type, value)) { enumValue = Enum.Parse(type, value, true); return true; diff --git a/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs b/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs index cea61324..4f5a3529 100644 --- a/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs +++ b/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs @@ -479,6 +479,40 @@ public void ValidateToken(TokenId tokenId, string? errorMessage = null) } } + /// + /// Check if the current token is an with the provided id . + /// + /// The id + public bool TokenIsIdentifier(string id) + { + return CurrentToken.Id == TokenId.Identifier && string.Equals(id, CurrentToken.Text, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Try to get a token based on the id or . + /// + /// The ids. + /// The tokenIds. + /// The found token, or default when not found. + public bool TryGetToken(string[] ids, TokenId[] tokenIds, out Token token) + { + token = default; + + if (ids.Any(TokenIsIdentifier)) + { + token = CurrentToken; + return true; + } + + if (tokenIds.Any(tokenId => tokenId == CurrentToken.Id)) + { + token = CurrentToken; + return true; + } + + return false; + } + private void SetTextPos(int pos) { _textPos = pos; diff --git a/src/System.Linq.Dynamic.Core/Validation/Check.cs b/src/System.Linq.Dynamic.Core/Validation/Check.cs index 00994c89..e0bbb32b 100644 --- a/src/System.Linq.Dynamic.Core/Validation/Check.cs +++ b/src/System.Linq.Dynamic.Core/Validation/Check.cs @@ -8,6 +8,18 @@ namespace System.Linq.Dynamic.Core.Validation; [DebuggerStepThrough] internal static class Check { + private const string ParsingConfigError = "The ParsingConfig should be provided as first argument to this method."; + + public static object?[]? Args(object?[]? args, [CallerArgumentExpression("args")] string? parameterName = null) + { + if (args?.Any(a => a is ParsingConfig) == true) + { + throw new ArgumentException(ParsingConfigError, parameterName); + } + + return args; + } + public static T Condition(T value, Predicate predicate, [CallerArgumentExpression("value")] string? parameterName = null) { NotNull(predicate); diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.Sum.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.Sum.cs new file mode 100644 index 00000000..b1d26866 --- /dev/null +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.Sum.cs @@ -0,0 +1,57 @@ +using System.Linq.Dynamic.Core.Tests.Helpers.Models; +using Xunit; + +namespace System.Linq.Dynamic.Core.Tests; + +public partial class ExpressionTests +{ + [Fact] + public void ExpressionTests_Sum() + { + // Arrange + int[] initValues = [1, 2, 3, 4, 5]; + var qry = initValues.AsQueryable().Select(x => new { strValue = "str", intValue = x }).GroupBy(x => x.strValue); + + // Act + var result = qry.Select("Sum(intValue)").AsDynamicEnumerable().ToArray()[0]; + + // Assert + Assert.Equal(15, result); + } + + [Fact] + public void ExpressionTests_Sum_LowerCase() + { + // Arrange + int[] initValues = [1, 2, 3, 4, 5]; + var qry = initValues.AsQueryable().Select(x => new { strValue = "str", intValue = x }).GroupBy(x => x.strValue); + + // Act + var result = qry.Select("sum(intValue)").AsDynamicEnumerable().ToArray()[0]; + + // Assert + Assert.Equal(15, result); + } + + [Fact] + public void ExpressionTests_Sum2() + { + // Arrange + var initValues = new[] + { + new SimpleValuesModel { FloatValue = 1 }, + new SimpleValuesModel { FloatValue = 2 }, + new SimpleValuesModel { FloatValue = 3 }, + }; + + var qry = initValues.AsQueryable(); + + // Act + var result = qry.Select("FloatValue").Sum(); + var result2 = ((IQueryable)qry.Select("FloatValue")).Sum(); + + // Assert + Assert.Equal(6.0f, result); + Assert.Equal(6.0f, result2); + } +} \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs index 1c00d1d6..d71845ea 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs @@ -1281,8 +1281,8 @@ public void ExpressionTests_In_Enum() // Act var expected = qry.Where(x => new[] { TestEnum.Var1, TestEnum.Var2 }.Contains(x.TestEnum)).ToArray(); - var result1 = qry.Where("it.TestEnum in (\"Var1\", \"Var2\")", config).ToArray(); - var result2 = qry.Where("it.TestEnum in (0, 1)", config).ToArray(); + var result1 = qry.Where(config, "it.TestEnum in (\"Var1\", \"Var2\")").ToArray(); + var result2 = qry.Where(config, "it.TestEnum in (0, 1)").ToArray(); // Assert Check.That(result1).ContainsExactly(expected); @@ -1307,28 +1307,44 @@ public void ExpressionTests_In_Short() public void ExpressionTests_In_String() { // Arrange - var testRange = Enumerable.Range(1, 100).ToArray(); var testModels = User.GenerateSampleModels(10); - var testModelByUsername = string.Format("Username in (\"{0}\",\"{1}\",\"{2}\")", testModels[0].UserName, testModels[1].UserName, testModels[2].UserName); + var testModelByUsername = $"Username in (\"{testModels[0].UserName}\",\"{testModels[1].UserName}\",\"{testModels[2].UserName}\")"; + + // Act + var result1 = testModels.AsQueryable().Where(testModelByUsername).ToArray(); + var result2 = testModels.AsQueryable().Where("Id in (@0, @1, @2)", testModels[0].Id, testModels[1].Id, testModels[2].Id).ToArray(); + + // Assert + Assert.Equal(testModels.Take(3).ToArray(), result1); + Assert.Equal(testModels.Take(3).ToArray(), result2); + } + + [Fact] + public void ExpressionTests_In_IntegerArray() + { + // Arrange + var testRange = Enumerable.Range(1, 10).ToArray(); var testInExpression = new[] { 2, 4, 6, 8 }; // Act var result1a = testRange.AsQueryable().Where("it in (2,4,6,8)").ToArray(); var result1b = testRange.AsQueryable().Where("it in (2, 4, 6, 8)").ToArray(); - // https://github.com/NArnott/System.Linq.Dynamic/issues/52 - var result2 = testModels.AsQueryable().Where(testModelByUsername).ToArray(); - var result3 = - testModels.AsQueryable() - .Where("Id in (@0, @1, @2)", testModels[0].Id, testModels[1].Id, testModels[2].Id) - .ToArray(); - var result4 = testRange.AsQueryable().Where("it in @0", testInExpression).ToArray(); + var result2 = testRange.AsQueryable().Where("it in @0", testInExpression).ToArray(); // Assert - Assert.Equal(new[] { 2, 4, 6, 8 }, result1a); - Assert.Equal(new[] { 2, 4, 6, 8 }, result1b); - Assert.Equal(testModels.Take(3).ToArray(), result2); - Assert.Equal(testModels.Take(3).ToArray(), result3); - Assert.Equal(new[] { 2, 4, 6, 8 }, result4); + Assert.Equal([2, 4, 6, 8], result1a); + Assert.Equal([2, 4, 6, 8], result1b); + Assert.Equal([2, 4, 6, 8], result2); + } + + [Fact] + public void ExpressionTests_InvalidNotIn_ThrowsException() + { + // Arrange + var testRange = Enumerable.Range(1, 10).ToArray(); + + // Act + Assert + Check.ThatCode(() => testRange.AsQueryable().Where("it not not in (2,4,6,8)").ToArray()).Throws(); } [Fact] @@ -1519,6 +1535,26 @@ public void ExpressionTests_Multiply_Number() Check.That(result).ContainsExactly(expected); } + [Fact] + public void ExpressionTests_NotIn_IntegerArray() + { + // Arrange + var testRange = Enumerable.Range(1, 9).ToArray(); + var testInExpression = new[] { 2, 4, 6, 8 }; + + // Act + var result1a = testRange.AsQueryable().Where("it not in (2,4,6,8)").ToArray(); + var result1b = testRange.AsQueryable().Where("it not_in (2, 4, 6, 8)").ToArray(); + var result2 = testRange.AsQueryable().Where("it not in @0", testInExpression).ToArray(); + var result3 = testRange.AsQueryable().Where("it not_in @0", testInExpression).ToArray(); + + // Assert + Assert.Equal([1, 3, 5, 7, 9], result1a); + Assert.Equal([1, 3, 5, 7, 9], result1b); + Assert.Equal([1, 3, 5, 7, 9], result2); + Assert.Equal([1, 3, 5, 7, 9], result3); + } + [Fact] public void ExpressionTests_NullCoalescing() { @@ -1699,7 +1735,7 @@ public void ExpressionTests_NullPropagating_Config_Has_UseDefault(string test, s queryAsString = queryAsString.Substring(queryAsString.IndexOf(".Select") + 1).TrimEnd(']'); Check.That(queryAsString).Equals(query); } - + [Fact] public void ExpressionTests_NullPropagation_Method() { @@ -2103,7 +2139,7 @@ public void ExpressionTests_StringEscaping() // Act var result = baseQuery.Where("it.Value == \"ab\\\"cd\"").ToList(); - + // Assert Assert.Single(result); Assert.Equal("ab\"cd", result[0].Value); @@ -2154,57 +2190,6 @@ public void ExpressionTests_Subtract_Number() Check.That(result).ContainsExactly(expected); } - [Fact] - public void ExpressionTests_Sum() - { - // Arrange - int[] initValues = { 1, 2, 3, 4, 5 }; - var qry = initValues.AsQueryable().Select(x => new { strValue = "str", intValue = x }).GroupBy(x => x.strValue); - - // Act - var result = qry.Select("Sum(intValue)").AsDynamicEnumerable().ToArray()[0]; - - // Assert - Assert.Equal(15, result); - } - - [Fact] - public void ExpressionTests_Sum_LowerCase() - { - // Arrange - int[] initValues = { 1, 2, 3, 4, 5 }; - var qry = initValues.AsQueryable().Select(x => new { strValue = "str", intValue = x }).GroupBy(x => x.strValue); - - // Act - var result = qry.Select("sum(intValue)").AsDynamicEnumerable().ToArray()[0]; - - // Assert - Assert.Equal(15, result); - } - - [Fact] - public void ExpressionTests_Sum2() - { - // Arrange - var initValues = new[] - { - new SimpleValuesModel { FloatValue = 1 }, - new SimpleValuesModel { FloatValue = 2 }, - new SimpleValuesModel { FloatValue = 3 }, - }; - - var qry = initValues.AsQueryable(); - - // Act - var result = qry.Select("FloatValue").Sum(); - var result2 = ((IQueryable)qry.Select("FloatValue")).Sum(); - - // Assert - Assert.Equal(6.0f, result); - Assert.Equal(6.0f, result2); - } - - [Fact] public void ExpressionTests_Type_Integer() { diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.Sum.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.Sum.cs new file mode 100644 index 00000000..2c33276e --- /dev/null +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.Sum.cs @@ -0,0 +1,117 @@ +using System.Collections.Generic; +using System.Linq.Dynamic.Core.Parser; +using System.Linq.Expressions; +using Xunit; + +namespace System.Linq.Dynamic.Core.Tests.Parser; + +public partial class ExpressionParserTests +{ + [Fact] + public void Parse_Aggregate_Sum_With_Predicate() + { + // Arrange + var childType = DynamicClassFactory.CreateType( + [ + new DynamicProperty("Value", typeof(int)) + ]); + + var parentType = DynamicClassFactory.CreateType( + [ + new DynamicProperty("SubFoos", childType.MakeArrayType()) + ]); + + // Act + var parser = new ExpressionParser( + [ + Expression.Parameter(parentType, "Foo") + ], + "Foo.SubFoos.Sum(s => s.Value)", + [], + new ParsingConfig()); + + // Assert + parser.Parse(typeof(int)); + } + + [Fact] + public void Parse_Aggregate_Sum_In_Sum_With_Predicate_On_IEnumerable() + { + // Arrange + var childType = DynamicClassFactory.CreateType( + [ + new DynamicProperty("DoubleArray", typeof(IEnumerable)) + ]); + + var parentType = DynamicClassFactory.CreateType( + [ + new DynamicProperty("SubFoos", childType.MakeArrayType()) + ]); + + // Act + var parser = new ExpressionParser( + [ + Expression.Parameter(parentType, "Foo") + ], + "Foo.SubFoos.Sum(s => s.DoubleArray.Sum())", + [], + new ParsingConfig()); + + // Assert + parser.Parse(typeof(double)); + } + + [Fact] + public void Parse_Aggregate_Sum_In_Sum_With_Predicate_On_Array() + { + // Arrange + var childType = DynamicClassFactory.CreateType( + [ + new DynamicProperty("DoubleArray", typeof(double[])) + ]); + + var parentType = DynamicClassFactory.CreateType( + [ + new DynamicProperty("SubFoos", childType.MakeArrayType()) + ]); + + // Act + var parser = new ExpressionParser( + [ + Expression.Parameter(parentType, "Foo") + ], + "Foo.SubFoos.Sum(s => s.DoubleArray.Sum())", + [], + new ParsingConfig()); + + // Assert + parser.Parse(typeof(double)); + } + + [Fact] + public void Parse_Aggregate_Sum_In_Sum_In_Sum_With_Predicate_On_ArrayArray() + { + // Arrange + var childType = DynamicClassFactory.CreateType( + [ + new DynamicProperty("DoubleArrayArray", typeof(double[][])) + ]); + + var parentType = DynamicClassFactory.CreateType( + [ + new DynamicProperty("SubFoos", childType.MakeArrayType()) + ]); + + // Act + var parser = new ExpressionParser( + [ + Expression.Parameter(parentType, "Foo") + ], + "Foo.SubFoos.Sum(s => s.DoubleArrayArray.Sum(x => x.Sum()))", + [], + new ParsingConfig()); + + // Assert + parser.Parse(typeof(double)); + } +} \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs index 9369d143..f6696720 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs @@ -35,7 +35,7 @@ public class MyView public ExpressionParserTests() { _dynamicTypeProviderMock = new Mock(); - _dynamicTypeProviderMock.Setup(dt => dt.GetCustomTypes()).Returns(new HashSet() { typeof(Company), typeof(MainCompany) }); + _dynamicTypeProviderMock.Setup(dt => dt.GetCustomTypes()).Returns([typeof(Company), typeof(MainCompany)]); _dynamicTypeProviderMock.Setup(dt => dt.ResolveType(typeof(Company).FullName!)).Returns(typeof(Company)); _dynamicTypeProviderMock.Setup(dt => dt.ResolveType(typeof(MainCompany).FullName!)).Returns(typeof(MainCompany)); _dynamicTypeProviderMock.Setup(dt => dt.ResolveTypeBySimpleName("Company")).Returns(typeof(Company)); @@ -57,8 +57,8 @@ public void Parse_BitwiseOperatorOr_On_2EnumFlags() #else var expected = "Convert((Convert(A, Int32) | Convert(B, Int32)), ExampleFlags)"; #endif - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x") }; - var sut = new ExpressionParser(parameters, expression, new object[] { ExampleFlags.A, ExampleFlags.B }, null); + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x")]; + var sut = new ExpressionParser(parameters, expression, [ExampleFlags.A, ExampleFlags.B], null); // Act var parsedExpression = sut.Parse(null).ToString(); @@ -87,8 +87,8 @@ public void Parse_BitwiseOperatorAnd_On_2EnumFlags() #else var expected = "Convert((Convert(A, Int32) & Convert(B, Int32)), ExampleFlags)"; #endif - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x") }; - var sut = new ExpressionParser(parameters, expression, new object[] { ExampleFlags.A, ExampleFlags.B }, null); + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x")]; + var sut = new ExpressionParser(parameters, expression, [ExampleFlags.A, ExampleFlags.B], null); // Act var parsedExpression = sut.Parse(null).ToString(); @@ -117,8 +117,8 @@ public void Parse_BitwiseOperatorOr_On_3EnumFlags() #else var expected = "Convert(((Convert(A, Int32) | Convert(B, Int32)) | Convert(C, Int32)), ExampleFlags)"; #endif - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x") }; - var sut = new ExpressionParser(parameters, expression, new object[] { ExampleFlags.A, ExampleFlags.B, ExampleFlags.C }, null); + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x")]; + var sut = new ExpressionParser(parameters, expression, [ExampleFlags.A, ExampleFlags.B, ExampleFlags.C], null); // Act var parsedExpression = sut.Parse(null).ToString(); @@ -147,8 +147,8 @@ public void Parse_BitwiseOperatorAnd_On_3EnumFlags() #else var expected = "Convert(((Convert(A, Int32) & Convert(B, Int32)) & Convert(C, Int32)), ExampleFlags)"; #endif - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x") }; - var sut = new ExpressionParser(parameters, expression, new object[] { ExampleFlags.A, ExampleFlags.B, ExampleFlags.C }, null); + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x")]; + var sut = new ExpressionParser(parameters, expression, [ExampleFlags.A, ExampleFlags.B, ExampleFlags.C], null); // Act var parsedExpression = sut.Parse(null).ToString(); @@ -172,7 +172,7 @@ public void Parse_ParseBinaryInteger() { // Arrange var expression = "0b1100000011101"; - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x") }; + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x")]; var sut = new ExpressionParser(parameters, expression, null, null); // Act @@ -187,7 +187,7 @@ public void Parse_ParseHexadecimalInteger() { // Arrange var expression = "0xFF"; - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x") }; + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x")]; var sut = new ExpressionParser(parameters, expression, null, null); // Act @@ -216,7 +216,7 @@ public void Parse_ParseHexadecimalInteger() public void Parse_ParseComparisonOperator(string expression, string result) { // Arrange - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x") }; + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(int), "x")]; var sut = new ExpressionParser(parameters, expression, null, null); // Act @@ -233,7 +233,7 @@ public void Parse_ParseComparisonOperator(string expression, string result) public void Parse_ParseOrOperator(string expression, string result) { // Arrange - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(bool), "x") }; + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(bool), "x")]; var sut = new ExpressionParser(parameters, expression, null, null); // Act @@ -250,7 +250,7 @@ public void Parse_ParseOrOperator(string expression, string result) public void Parse_ParseAndOperator(string expression, string result) { // Arrange - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(bool), "x") }; + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(bool), "x")]; var sut = new ExpressionParser(parameters, expression, null, null); // Act @@ -264,22 +264,51 @@ public void Parse_ParseAndOperator(string expression, string result) public void Parse_ParseMultipleInOperators() { // Arrange - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "x") }; - var sut = new ExpressionParser(parameters, "MainCompanyId in (1, 2) and Name in (\"A\", \"B\")", null, null); + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "x")]; + var sut = new ExpressionParser(parameters, "MainCompanyId in (1, 2) and Name in (\"A\", \"B\") && 'y' in Name && 'z' in Name", null, null); // Act var parsedExpression = sut.Parse(null).ToString(); // Assert - Check.That(parsedExpression).Equals("(((x.MainCompanyId == 1) OrElse (x.MainCompanyId == 2)) AndAlso ((x.Name == \"A\") OrElse (x.Name == \"B\")))"); + Check.That(parsedExpression).Equals("(((((x.MainCompanyId == 1) OrElse (x.MainCompanyId == 2)) AndAlso ((x.Name == \"A\") OrElse (x.Name == \"B\"))) AndAlso x.Name.Contains(y)) AndAlso x.Name.Contains(z))"); + } + + [Fact] + public void Parse_ParseMultipleInAndNotInOperators() + { + // Arrange + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "x")]; + var sut = new ExpressionParser(parameters, "MainCompanyId in (1, 2) and Name not in (\"A\", \"B\") && 'y' in Name && 'z' not in Name", null, null); + + // Act + var parsedExpression = sut.Parse(null).ToString(); + + // Assert + Check.That(parsedExpression).Equals("(((((x.MainCompanyId == 1) OrElse (x.MainCompanyId == 2)) AndAlso Not(((x.Name == \"A\") OrElse (x.Name == \"B\")))) AndAlso x.Name.Contains(y)) AndAlso Not(x.Name.Contains(z)))"); + } + + + [Fact] + public void Parse_ParseMultipleInAndNotInAndNot_InOperators() + { + // Arrange + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "x")]; + var sut = new ExpressionParser(parameters, "MainCompanyId in (1, 2) and MainCompanyId not in (3, 4) and Name not_in (\"A\", \"B\") && 'y' in Name && 'z' not in Name && 's' not_in Name", null, null); + + // Act + var parsedExpression = sut.Parse(null).ToString(); + + // Assert + Check.That(parsedExpression).Equals("(((((((x.MainCompanyId == 1) OrElse (x.MainCompanyId == 2)) AndAlso Not(((x.MainCompanyId == 3) OrElse (x.MainCompanyId == 4)))) AndAlso Not(((x.Name == \"A\") OrElse (x.Name == \"B\")))) AndAlso x.Name.Contains(y)) AndAlso Not(x.Name.Contains(z))) AndAlso Not(x.Name.Contains(s)))"); } [Fact] public void Parse_ParseInWrappedInParenthesis() { // Arrange - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "x") }; - var sut = new ExpressionParser(parameters, "(MainCompanyId in @0)", new object[] { new long?[] { 1, 2 } }, null); + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "x")]; + var sut = new ExpressionParser(parameters, "(MainCompanyId in @0)", [new long?[] { 1, 2 }], null); // Act var parsedExpression = sut.Parse(null).ToString(); @@ -309,7 +338,7 @@ public void Parse_CastActingOnIt() public void Parse_CastStringIntShouldReturnConstantExpression(string expression, object result) { // Arrange - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(bool), "x") }; + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(bool), "x")]; var sut = new ExpressionParser(parameters, expression, null, null); // Act @@ -332,7 +361,7 @@ public void Parse_CastStringIntShouldReturnConstantExpression(string expression, public void Parse_NullableShouldReturnNullable(string expression, object resultType, object result) { // Arrange - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(bool), "x") }; + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(bool), "x")]; var sut = new ExpressionParser(parameters, expression, null, null); // Act @@ -361,7 +390,7 @@ public void Parse_When_PrioritizePropertyOrFieldOverTheType_IsTrue(string expres CustomTypeProvider = _dynamicTypeProviderMock.Object, AllowEqualsAndToStringMethodsOnObject = true }; - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "company") }; + ParameterExpression[] parameters = [ ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "company") ]; var sut = new ExpressionParser(parameters, expression, null, config); // Act @@ -394,7 +423,8 @@ public void Parse_When_PrioritizePropertyOrFieldOverTheType_IsFalse(string expre { PrioritizePropertyOrFieldOverTheType = false }; - ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "company") }; + ParameterExpression[] parameters = [ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "company") + ]; // Act string parsedExpression; @@ -436,7 +466,7 @@ public void Parse_StringConcat(string expression, string result) public void Parse_InvalidExpressionShouldThrowArgumentException() { // Arrange & Act - Action act = () => DynamicExpressionParser.ParseLambda(ParsingConfig.Default, false, "Properties[\"foo\"] > 2", Array.Empty()); + Action act = () => DynamicExpressionParser.ParseLambda(ParsingConfig.Default, false, "Properties[\"foo\"] > 2", []); // Assert act.Should().Throw().WithMessage("Method 'Compare' not found on type 'System.String' or 'System.Int32'"); diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Sum.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Sum.cs index 7bab86be..c95db7ed 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Sum.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Sum.cs @@ -1,36 +1,35 @@ using System.Linq.Dynamic.Core.Tests.Helpers.Models; using Xunit; -namespace System.Linq.Dynamic.Core.Tests +namespace System.Linq.Dynamic.Core.Tests; + +public partial class QueryableTests { - public partial class QueryableTests + [Fact] + public void Sum() { - [Fact] - public void Sum() - { - // Arrange - var incomes = User.GenerateSampleModels(100).Select(u => u.Income); + // Arrange + var incomes = User.GenerateSampleModels(100).Select(u => u.Income); - // Act - var expected = incomes.Sum(); - var actual = incomes.AsQueryable().Sum(); + // Act + var expected = incomes.Sum(); + var actual = incomes.AsQueryable().Sum(); - // Assert - Assert.Equal(expected, actual); - } + // Assert + Assert.Equal(expected, actual); + } - [Fact] - public void Sum_Selector() - { - // Arrange - var users = User.GenerateSampleModels(100); + [Fact] + public void Sum_Selector() + { + // Arrange + var users = User.GenerateSampleModels(100); - // Act - var expected = users.Sum(u => u.Income); - var result = users.AsQueryable().Sum("Income"); + // Act + var expected = users.Sum(u => u.Income); + var result = users.AsQueryable().Sum("Income"); - // Assert - Assert.Equal(expected, result); - } + // Assert + Assert.Equal(expected, result); } -} +} \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs index 1622a54e..22d22a72 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs @@ -155,6 +155,8 @@ public void Where_Dynamic_Exceptions() Assert.Throws(() => qry.Where((string?)null)); Assert.Throws(() => qry.Where("")); Assert.Throws(() => qry.Where(" ")); + var parsingConfigException = Assert.Throws(() => qry.Where("UserName == \"x\"", ParsingConfig.Default)); + Assert.Equal("The ParsingConfig should be provided as first argument to this method. (Parameter 'args')", parsingConfigException.Message); } [Fact] @@ -389,6 +391,58 @@ public void Where_Dynamic_CompareObjectToInt_ConvertObjectToSupportComparisonIsF act.Should().Throw().And.Message.Should().MatchRegex("The binary operator .* is not defined for the types"); } + [Fact] + public void Where_Dynamic_NullPropagation_Test1_On_NullableDoubleToString_When_AllowEqualsAndToStringMethodsOnObject_True() + { + // Arrange + var config = new ParsingConfig + { + AllowEqualsAndToStringMethodsOnObject = true + }; + var queryable = new[] + { + new { id = "1", d = (double?) null }, + new { id = "2", d = (double?) 5 }, + new { id = "3", d = (double?) 50 }, + new { id = "4", d = (double?) 40 } + }.AsQueryable(); + + // Act + var result = queryable + .Where(config, """np(it.d, 0).ToString().StartsWith("5", StringComparison.OrdinalIgnoreCase)""") + .Select("d") + .ToArray(); + + // Assert + result.Should().ContainInOrder(5, 50); + } + + [Fact] + public void Where_Dynamic_NullPropagation_Test2_On_NullableDoubleToString_When_AllowEqualsAndToStringMethodsOnObject_True() + { + // Arrange + var config = new ParsingConfig + { + AllowEqualsAndToStringMethodsOnObject = true + }; + var queryable = new[] + { + new { id = "1", d = (double?) null }, + new { id = "2", d = (double?) 5 }, + new { id = "3", d = (double?) 50 }, + new { id = "4", d = (double?) 40 } + }.AsQueryable(); + + // Act + var result = queryable + .Where(config, """np(it.d.ToString(), "").StartsWith("5", StringComparison.OrdinalIgnoreCase)""") + .Select("d") + .ToArray(); + + // Assert + result.Should().ContainInOrder(5, 50); + } + [ExcludeFromCodeCoverage] private class PersonWithObject { diff --git a/version.xml b/version.xml index 9fefde31..e265d039 100644 --- a/version.xml +++ b/version.xml @@ -1,5 +1,5 @@ - 3 + 4 \ No newline at end of file