From e925a09857bfbe9a8b79bafbfb34bac06d81ca73 Mon Sep 17 00:00:00 2001 From: Thibault Reigner Date: Fri, 10 Oct 2025 19:23:53 +0800 Subject: [PATCH 1/2] DynamicExpressionParser - Handle indexed properties with any number of indices in expression --- .../Parser/ExpressionParser.cs | 13 ++-- .../DynamicExpressionParserTests.cs | 66 +++++++++++++++++++ 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index be2a43c1..7badf19a 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -2358,15 +2358,18 @@ private Expression ParseElementAccess(Expression expr) case 1: var indexMethod = (MethodInfo)mb!; - var indexParameterType = indexMethod.GetParameters().First().ParameterType; + var indexMethodArguments = indexMethod.GetParameters(); - var indexArgumentExpression = args[0]; // Indexer only has 1 parameter, so we can use args[0] here - if (indexParameterType != indexArgumentExpression.Type) + var indexArgumentExpressions = new Expression[args.Length]; + for (var i = 0; i < indexMethodArguments.Length; ++i) { - indexArgumentExpression = Expression.Convert(indexArgumentExpression, indexParameterType); + var indexParameterType = indexMethodArguments[i].ParameterType; + indexArgumentExpressions[i] = indexParameterType != args[i].Type + ? Expression.Convert(args[i], indexParameterType) + : args[i]; } - return Expression.Call(expr, indexMethod, indexArgumentExpression); + return Expression.Call(expr, indexMethod, indexArgumentExpressions); default: throw ParseError(errorPos, Res.AmbiguousIndexerInvocation, TypeHelper.GetTypeName(expr.Type)); diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs index a65012b0..e81b445f 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs @@ -70,6 +70,18 @@ public override HashSet GetCustomTypes() } } + private class ClassWithIndexers + { + public int this[int i1] + { + get => i1 + 1; + } + public string this[int i1, string i2] + { + get => i1 + "-" + i2; + } + } + private class ComplexParseLambda1Result { public int? Age; @@ -660,6 +672,60 @@ public void DynamicExpressionParser_ParseLambda_Issue58() Check.That(result).Equals(42); } + [Fact] + public void DynamicExpressionParser_ParseLambda_Indexer1D() + { + // Arrange + var customTypeProvider = new Mock(); + customTypeProvider.Setup(c => c.GetCustomTypes()).Returns([typeof(ClassWithIndexers)]); + var config = new ParsingConfig + { + CustomTypeProvider = customTypeProvider.Object + }; + var expressionParams = new[] + { + Expression.Parameter(typeof(ClassWithIndexers), "myObj") + }; + + var myClassInstance = new ClassWithIndexers(); + var invokersMerge = new List { myClassInstance }; + + // Act + var expression = DynamicExpressionParser.ParseLambda(config, false, expressionParams, null, "myObj[3]"); + var del = expression.Compile(); + var result = del.DynamicInvoke(invokersMerge.ToArray()); + + // Assert + Check.That(result).Equals(4); + } + + [Fact] + public void DynamicExpressionParser_ParseLambda_Indexer2D() + { + // Arrange + var customTypeProvider = new Mock(); + customTypeProvider.Setup(c => c.GetCustomTypes()).Returns([typeof(ClassWithIndexers)]); + var config = new ParsingConfig + { + CustomTypeProvider = customTypeProvider.Object + }; + var expressionParams = new[] + { + Expression.Parameter(typeof(ClassWithIndexers), "myObj") + }; + + var myClassInstance = new ClassWithIndexers(); + var invokersMerge = new List { myClassInstance }; + + // Act + var expression = DynamicExpressionParser.ParseLambda(config, false, expressionParams, null, "myObj[3,\"1\"]"); + var del = expression.Compile(); + var result = del.DynamicInvoke(invokersMerge.ToArray()); + + // Assert + Check.That(result).Equals("3-1"); + } + [Fact] public void DynamicExpressionParser_ParseLambda_DuplicateParameterNames_ThrowsException() { From 3895e279f7383fa80d9f1035cbdae2e276a66394 Mon Sep 17 00:00:00 2001 From: Thibault Reigner Date: Fri, 10 Oct 2025 21:08:49 +0800 Subject: [PATCH 2/2] Adjust exception message when misusing indexer (e.g : incorrect number of parameters) --- .../Parser/ExpressionParser.cs | 3 +-- src/System.Linq.Dynamic.Core/Res.cs | 2 +- .../DynamicExpressionParserTests.cs | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 7badf19a..4cc77a00 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -2353,8 +2353,7 @@ private Expression ParseElementAccess(Expression expr) switch (_methodFinder.FindIndexer(expr.Type, args, out var mb)) { case 0: - throw ParseError(errorPos, Res.NoApplicableIndexer, - TypeHelper.GetTypeName(expr.Type)); + throw ParseError(errorPos, Res.NoApplicableIndexer, TypeHelper.GetTypeName(expr.Type), args.Length); case 1: var indexMethod = (MethodInfo)mb!; diff --git a/src/System.Linq.Dynamic.Core/Res.cs b/src/System.Linq.Dynamic.Core/Res.cs index d2831c24..3d423495 100644 --- a/src/System.Linq.Dynamic.Core/Res.cs +++ b/src/System.Linq.Dynamic.Core/Res.cs @@ -55,7 +55,7 @@ internal static class Res public const string MissingAsClause = "Expression is missing an 'as' clause"; public const string NeitherTypeConvertsToOther = "Neither of the types '{0}' and '{1}' converts to the other"; public const string NewOperatorIsNotAllowed = "Using the new operator is not allowed via the ParsingConfig."; - public const string NoApplicableIndexer = "No applicable indexer exists in type '{0}'"; + public const string NoApplicableIndexer = "No applicable indexer exists in type '{0}' with {1} parameters"; public const string NoApplicableMethod = "No applicable method '{0}' exists in type '{1}'"; public const string NoItInScope = "No 'it' is in scope"; public const string NoMatchingConstructor = "No matching constructor in type '{0}'"; diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs index e81b445f..4d5d19b5 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs @@ -725,6 +725,25 @@ public void DynamicExpressionParser_ParseLambda_Indexer2D() // Assert Check.That(result).Equals("3-1"); } + + [Fact] + public void DynamicExpressionParser_ParseLambda_IndexerParameterMismatch() + { + // Arrange + var customTypeProvider = new Mock(); + customTypeProvider.Setup(c => c.GetCustomTypes()).Returns([typeof(ClassWithIndexers)]); + var config = new ParsingConfig + { + CustomTypeProvider = customTypeProvider.Object + }; + var expressionParams = new[] + { + Expression.Parameter(typeof(ClassWithIndexers), "myObj") + }; + + Assert.Throws(() => + DynamicExpressionParser.ParseLambda(config, false, expressionParams, null, "myObj[3,\"1\",1]")); + } [Fact] public void DynamicExpressionParser_ParseLambda_DuplicateParameterNames_ThrowsException()