diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index b8091f55..0e65d661 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -2139,7 +2139,7 @@ private bool TryParseEnumerable(Expression instance, Type enumerableType, string // Create a new innerIt based on the elementType. var innerIt = ParameterExpressionHelper.CreateParameterExpression(elementType, string.Empty, _parsingConfig.RenameEmptyParameterExpressionNames); - if (new[] { "Contains", "ContainsKey", "Skip", "Take" }.Contains(methodName)) + if (Contains(["Contains", "ContainsKey", "Skip", "Take"], methodName)) { // For any method that acts on the parent element type, we need to specify the outerIt as scope. _it = outerIt; @@ -2194,7 +2194,7 @@ private bool TryParseEnumerable(Expression instance, Type enumerableType, string } Type[] typeArgs; - if (new[] { "OfType", "Cast" }.Contains(methodName)) + if (Contains(["OfType", "Cast"], methodName)) { if (args.Length != 1) { @@ -2204,7 +2204,7 @@ private bool TryParseEnumerable(Expression instance, Type enumerableType, string typeArgs = [ResolveTypeFromArgumentExpression(methodName, args[0])]; args = []; } - else if (new[] { "Max", "Min", "Select", "OrderBy", "OrderByDescending", "ThenBy", "ThenByDescending", "GroupBy" }.Contains(methodName)) + else if (Contains(["Max", "Min", "Select", "OrderBy", "OrderByDescending", "ThenBy", "ThenByDescending", "GroupBy"], methodName)) { if (args.Length == 2) { @@ -2219,7 +2219,7 @@ private bool TryParseEnumerable(Expression instance, Type enumerableType, string typeArgs = [elementType]; } } - else if (methodName == "SelectMany") + else if (Contains(["SelectMany"], methodName)) { var bodyType = Expression.Lambda(args[0], innerIt).Body.Type; var interfaces = bodyType.GetInterfaces().Union([bodyType]); @@ -2238,7 +2238,7 @@ private bool TryParseEnumerable(Expression instance, Type enumerableType, string } else { - if (new[] { "Concat", "Contains", "ContainsKey", "DefaultIfEmpty", "Except", "Intersect", "Skip", "Take", "Union", "SequenceEqual" }.Contains(methodName)) + if (Contains(["Concat", "Contains", "ContainsKey", "DefaultIfEmpty", "Except", "Intersect", "Skip", "Take", "Union", "SequenceEqual"], methodName)) { args = [instance, args[0]]; } @@ -2259,6 +2259,13 @@ private bool TryParseEnumerable(Expression instance, Type enumerableType, string return true; } + private bool Contains(string[] haystack, string needle) + { + return _parsingConfig.IsCaseSensitive + ? haystack.Contains(needle) + : haystack.Any(item => item.Equals(needle, StringComparison.OrdinalIgnoreCase)); + } + private Type ResolveTypeFromArgumentExpression(string functionName, Expression argumentExpression, int? arguments = null) { var argument = arguments == null ? string.Empty : arguments == 1 ? "first " : "second "; diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs index f6696720..1e92c7d0 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs @@ -471,4 +471,83 @@ public void Parse_InvalidExpressionShouldThrowArgumentException() // Assert act.Should().Throw().WithMessage("Method 'Compare' not found on type 'System.String' or 'System.Int32'"); } + + [Theory] + [InlineData("List.MAX(x => x)", false, "List.Max(Param_0 => Param_0)")] + [InlineData("List.min(x => x)", false, "List.Min(Param_0 => Param_0)")] + [InlineData("List.Min(x => x)", false, "List.Min(Param_0 => Param_0)")] + [InlineData("List.Min(x => x)", true, "List.Min(Param_0 => Param_0)")] + [InlineData("List.WHERE(x => x.Ticks >= 100000).max()", false, "List.Where(Param_0 => (Param_0.Ticks >= 100000)).Max()")] + public void Parse_LinqMethodsRespectCasing(string expression, bool caseSensitive, string result) + { + // Arrange + var parameters = new[] { Expression.Parameter(typeof(DateTime[]), "List") }; + + var parser = new ExpressionParser( + parameters, + expression, + [], + new ParsingConfig + { + IsCaseSensitive = caseSensitive + }); + + // Act + var parsedExpression = parser.Parse(typeof(DateTime)).ToString(); + + // Assert + parsedExpression.Should().Be(result); + } + + [Fact] + public void Parse_InvalidCasingShouldThrowInvalidOperationException() + { + // Arrange & Act + var parameters = new[] { Expression.Parameter(typeof(DateTime[]), "List") }; + + Action act = () => new ExpressionParser( + parameters, + "List.MAX(x => x)", + [], + new ParsingConfig + { + IsCaseSensitive = true + }) + .Parse(typeof(DateTime)); + + // Assert + act.Should().Throw().WithMessage("No generic method 'MAX' on type 'System.Linq.Enumerable' is compatible with the supplied type arguments and arguments. No type arguments should be provided if the method is non-generic.*"); + } + + [Theory] + [InlineData("List.max(x => x.max)", "List.Max(Param_0 => Param_0.max)")] + [InlineData("List.MAX(x => x.max)", "List.Max(Param_0 => Param_0.max)")] + [InlineData("List[0].MAX", "List[0].max")] + [InlineData("List.select(max => max.MAX).max()", "List.Select(Param_0 => Param_0.max).Max()")] + [InlineData("List.max(max)", "List.Max(Param_0 => Param_0.max)")] + public void Parse_LinqMethodsRespectCasingFromModel(string expression, string result) + { + // Arrange + var parameters = new[] { Expression.Parameter(typeof(Model[]), "List") }; + + var parser = new ExpressionParser( + parameters, + expression, + [], + new ParsingConfig + { + IsCaseSensitive = false + }); + + // Act + var parsedExpression = parser.Parse(typeof(DateTime)).ToString(); + + // Assert + parsedExpression.Should().Be(result); + } + + private class Model + { + public DateTime max { get; set; } + } } \ No newline at end of file