Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Suggestions applied from PR review:
- Tests added for a model with a max property
- CanonicalContains is now just Contains without ref string needle
  • Loading branch information
victorr99 committed Dec 6, 2025
commit cee98e1ab4e6d4acd5d5b6d576012a6f1342f0f2
30 changes: 9 additions & 21 deletions src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 (CanonicalContains(["Contains", "ContainsKey", "Skip", "Take"], ref 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;
Expand Down Expand Up @@ -2194,7 +2194,7 @@ private bool TryParseEnumerable(Expression instance, Type enumerableType, string
}

Type[] typeArgs;
if (CanonicalContains(["OfType", "Cast"], ref methodName))
if (Contains(["OfType", "Cast"], methodName))
{
if (args.Length != 1)
{
Expand All @@ -2204,7 +2204,7 @@ private bool TryParseEnumerable(Expression instance, Type enumerableType, string
typeArgs = [ResolveTypeFromArgumentExpression(methodName, args[0])];
args = [];
}
else if (CanonicalContains(["Max", "Min", "Select", "OrderBy", "OrderByDescending", "ThenBy", "ThenByDescending", "GroupBy"], ref methodName))
else if (Contains(["Max", "Min", "Select", "OrderBy", "OrderByDescending", "ThenBy", "ThenByDescending", "GroupBy"], methodName))
{
if (args.Length == 2)
{
Expand All @@ -2219,7 +2219,7 @@ private bool TryParseEnumerable(Expression instance, Type enumerableType, string
typeArgs = [elementType];
}
}
else if (CanonicalContains(["SelectMany"], ref methodName))
else if (Contains(["SelectMany"], methodName))
{
var bodyType = Expression.Lambda(args[0], innerIt).Body.Type;
var interfaces = bodyType.GetInterfaces().Union([bodyType]);
Expand All @@ -2238,7 +2238,7 @@ private bool TryParseEnumerable(Expression instance, Type enumerableType, string
}
else
{
if (CanonicalContains(["Concat", "Contains", "ContainsKey", "DefaultIfEmpty", "Except", "Intersect", "Skip", "Take", "Union", "SequenceEqual"], ref methodName))
if (Contains(["Concat", "Contains", "ContainsKey", "DefaultIfEmpty", "Except", "Intersect", "Skip", "Take", "Union", "SequenceEqual"], methodName))
{
args = [instance, args[0]];
}
Expand All @@ -2259,23 +2259,11 @@ private bool TryParseEnumerable(Expression instance, Type enumerableType, string
return true;
}

private bool CanonicalContains(string[] haystack, ref string needle)
private bool Contains(string[] haystack, string needle)
{
if (_parsingConfig.IsCaseSensitive)
{
return haystack.Contains(needle);
}

var element = needle;
var index = Array.FindIndex(haystack, item => item.Equals(element, StringComparison.OrdinalIgnoreCase));

if (index == -1)
{
return false;
}

needle = haystack[index];
return true;
return _parsingConfig.IsCaseSensitive
? haystack.Contains(needle)
: haystack.Any(item => item.Equals(needle, StringComparison.OrdinalIgnoreCase));
}

private Type ResolveTypeFromArgumentExpression(string functionName, Expression argumentExpression, int? arguments = null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -518,4 +518,36 @@ public void Parse_InvalidCasingShouldThrowInvalidOperationException()
// Assert
act.Should().Throw<InvalidOperationException>().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; }
}
}