From 2b33655cebcb00558f0bf7813d366858c727146d Mon Sep 17 00:00:00 2001 From: Holden Mai Date: Sun, 18 Sep 2022 19:52:38 -0500 Subject: [PATCH] Issues 212, 256: Adding support for internal lambdas to reference parameters from the parent lambda. --- src/DynamicExpresso.Core/Parsing/Parser.cs | 9 +- test/DynamicExpresso.UnitTest/GithubIssues.cs | 86 +++++++ .../LambdaExpressionTest.cs | 243 ++++++++++++++++++ 3 files changed, 335 insertions(+), 3 deletions(-) diff --git a/src/DynamicExpresso.Core/Parsing/Parser.cs b/src/DynamicExpresso.Core/Parsing/Parser.cs index b95d4769..4357f0c4 100644 --- a/src/DynamicExpresso.Core/Parsing/Parser.cs +++ b/src/DynamicExpresso.Core/Parsing/Parser.cs @@ -3255,11 +3255,14 @@ public InterpreterExpression(ParserArguments parserArguments, string expressionT _expressionText = expressionText; _parameters = parameters; - // convert the parent's parameters to variables + // Take the parent expression's parameters and set them as an identifier that + // can be accessed by any lower call // note: this doesn't impact the initial settings, because they're cloned - foreach (var pe in parserArguments.DeclaredParameters) + foreach (var dp in parserArguments.DeclaredParameters) { - _interpreter.SetVariable(pe.Name, pe.Value, pe.Type); + // Have to mark the parameter as "Used" otherwise we can get a compilation error. + parserArguments.TryGetParameters(dp.Name, out var pe); + _interpreter.SetIdentifier(new Identifier(dp.Name, pe)); } // prior to evaluation, we don't know the generic arguments types diff --git a/test/DynamicExpresso.UnitTest/GithubIssues.cs b/test/DynamicExpresso.UnitTest/GithubIssues.cs index e2e5a28d..9aee1609 100644 --- a/test/DynamicExpresso.UnitTest/GithubIssues.cs +++ b/test/DynamicExpresso.UnitTest/GithubIssues.cs @@ -554,6 +554,92 @@ public void GitHub_Issue_235() var result2 = target.Eval("DateTimeKind.Local | DateTimeKind.Utc"); Assert.AreEqual((DateTimeKind)3, result2); } + + [Test] + public void GitHub_Issue_212() + { + var target = new Interpreter(InterpreterOptions.Default | InterpreterOptions.LambdaExpressions); + var list = new Parameter("list", new[] { 1, 2, 3 }); + var value1 = new Parameter("value", 1); + var value2 = new Parameter("value", 2); + var expression = "list.Where(x => x > value)"; + var lambda = target.Parse(expression, list, value1); + var result = lambda.Invoke(list, value2); + Assert.AreEqual(new[] { 3 }, result); + } + + [Test] + public void GitHub_Issue_212_bis() + { + var target = new Interpreter(InterpreterOptions.Default | InterpreterOptions.LambdaExpressions); + var list = new Parameter("list", new[] { 1, 2, 3 }); + var value1 = new Parameter("value", 1); + var value2 = new Parameter("value", 2); + var expression = "list.Where(x => x > value)"; + var lambda = target.Parse(expression, (new[] { list, value1 }).Select(p => new Parameter(p.Name, p.Type)).ToArray()); + var result = lambda.Invoke(list, value1); + Assert.AreEqual(new[] { 2, 3 }, result); + } + + [Test] + public void GitHub_Issue_200_capture() + { + var target = new Interpreter(InterpreterOptions.Default | InterpreterOptions.LambdaExpressions); + var list = new List { "ab", "cdc" }; + target.SetVariable("myList", list); + + // the str parameter is captured, and can be used in the nested lambda + var results = target.Eval("myList.Select(str => str.Select(c => str.Length))"); + Assert.AreEqual(new[] { new[] { 2, 2 }, new[] { 3, 3, 3 } }, results); + } + + [Test] + public void Lambda_Issue_256() + { + ICollection annualBonus = new List { + new BonusMatrix() { Grade = 1, BonusFactor = 7 }, + new BonusMatrix() { Grade = 2, BonusFactor = 5.5 }, + new BonusMatrix() { Grade = 3, BonusFactor = 4 }, + new BonusMatrix() { Grade = 4, BonusFactor = 3.5 }, + new BonusMatrix() { Grade = 5, BonusFactor = 3 } + }; + + ICollection employees = new List { + new Employee() { Id = "01", Name = "A", Grade = 5, Salary = 20000}, //bonus = 20000 * 7 = 60000 + new Employee() { Id = "02", Name = "B", Grade = 5, Salary = 18000}, //bonus = 18000 * 7 = 54000 + new Employee() { Id = "03", Name = "C", Grade = 4, Salary = 12000}, //bonus = 12000 * 5.5 = 42000 + new Employee() { Id = "04", Name = "D", Grade = 4, Salary = 10000}, //bonus = 10000 * 5.5 = 35000 + new Employee() { Id = "05", Name = "E", Grade = 3, Salary = 8500}, //bonus = 8500 * 4 = 34000 + new Employee() { Id = "06", Name = "F", Grade = 3, Salary = 8000}, //bonus = 8000 * 4 = 32000 + new Employee() { Id = "07", Name = "G", Grade = 2, Salary = 5000}, //bonus = 5000 * 3.5 = 27500 + new Employee() { Id = "08", Name = "H", Grade = 2, Salary = 4750}, //bonus = 4750 * 3.5 = 26125 + new Employee() { Id = "09", Name = "I", Grade = 1, Salary = 3500}, //bonus = 3500 * 3 = 24500 + new Employee() { Id = "10", Name = "J", Grade = 1, Salary = 3250} //bonus = 3250 * 3 = 22750 + }; + + var interpreter = new Interpreter(InterpreterOptions.LambdaExpressions | InterpreterOptions.Default); + interpreter.SetVariable(nameof(annualBonus), annualBonus); + interpreter.SetVariable(nameof(employees), employees); + + var totalBonus = employees.Sum(x => x.Salary * (annualBonus.SingleOrDefault(y => y.Grade == x.Grade).BonusFactor)); //total = 357875 + + var evalSum = interpreter.Eval("employees.Sum(x => x.Salary * (annualBonus.SingleOrDefault(y => y.Grade == x.Grade).BonusFactor))"); + Assert.AreEqual(totalBonus, evalSum); + } + + public class Employee + { + public string Id { get; set; } + public string Name { get; set; } + public int Grade { get; set; } + public double Salary { get; set; } + } + + public class BonusMatrix + { + public int Grade { get; set; } + public double BonusFactor { get; set; } + } } internal static class GithubIssuesTestExtensionsMethods diff --git a/test/DynamicExpresso.UnitTest/LambdaExpressionTest.cs b/test/DynamicExpresso.UnitTest/LambdaExpressionTest.cs index ba021abb..883f6a9b 100644 --- a/test/DynamicExpresso.UnitTest/LambdaExpressionTest.cs +++ b/test/DynamicExpresso.UnitTest/LambdaExpressionTest.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; namespace DynamicExpresso.UnitTest { @@ -282,6 +283,24 @@ public void Lambda_with_parameter() Assert.AreEqual(new[] { 4, 5 }, listInt); } + [Test] + public void Lambda_with_parameter_AsCompiledLambda() + { + var target = new Interpreter(InterpreterOptions.Default | InterpreterOptions.LambdaExpressions); + var parm = new Parameter("x", 1); + var list = new Parameter("list", new[] { 1, 2, 3 }); + var listLamba = target.Parse("list.Where(n => n > x)", list, parm).Compile>>(); + var result = listLamba(list.Value as int[], 2); + Assert.AreEqual(new[] { 3 }, result); + + var listInt = listLamba(list.Value as int[], 1); + Assert.AreEqual(new[] { 2, 3 }, listInt); + + // ensure the parameters can be reused with different values + listInt = target.Eval>("list.Where(n => n > x)", new Parameter("list", new[] { 2, 4, 5 }), new Parameter("x", 2)); + Assert.AreEqual(new[] { 4, 5 }, listInt); + } + [Test] public void Lambda_with_parameter_2() { @@ -300,6 +319,230 @@ public void Lambda_with_variable() var listInt = target.Eval>("list.Where(n => n > x)"); Assert.AreEqual(new[] { 2, 3 }, listInt); } + + public class NestedLambdaTestClass + { + public NestedLambdaTestClass() + { + } + + public List Children + { + get; set; + } + + public string Name + { + get; set; + } + + // TODO + // Add support for non generics with our lambda evaluation + // The below fails to compile + // public string GetChildrenIdentifiers(Func f) + public string GetChildrenIdentifiers(Func f) + { + if (Children == null) + { + return string.Empty; + } + return string.Join(",", Children.Select(f)); + } + } + + [Test] + public void Lambda_WithMultipleNestedExpressions() + { + NestedLambdaTestClass root = new NestedLambdaTestClass() + { + Name = "Root", + Children = new List() + { + new NestedLambdaTestClass() + { + Name = "A", + Children = new List() + { + new NestedLambdaTestClass() + { + Name = "B", + Children = new List() + { + new NestedLambdaTestClass() + { + Name = "C", + Children = new List() + { + new NestedLambdaTestClass() + { + Name = "C" + }, + new NestedLambdaTestClass() + { + Name = "F" + } + } + }, + new NestedLambdaTestClass() + { + Name = "F", + Children = new List() + { + new NestedLambdaTestClass() + { + Name = "C" + }, + new NestedLambdaTestClass() + { + Name = "F" + } + } + } + } + }, + new NestedLambdaTestClass() + { + Name = "D", + Children = new List() + { + new NestedLambdaTestClass() + { + Name = "E", + Children = new List() + { + new NestedLambdaTestClass() + { + Name = "C" + }, + new NestedLambdaTestClass() + { + Name = "F" + } + } + }, + new NestedLambdaTestClass() + { + Name = "G", + Children = new List() + { + new NestedLambdaTestClass() + { + Name = "C" + }, + new NestedLambdaTestClass() + { + Name = "F" + } + } + } + } + } + } + }, + new NestedLambdaTestClass() + { + Name = "B", + Children = new List() + { + new NestedLambdaTestClass() + { + Name = "B", + Children = new List() + { + new NestedLambdaTestClass() + { + Name = "C", + Children = new List() + { + new NestedLambdaTestClass() + { + Name = "C" + }, + new NestedLambdaTestClass() + { + Name = "F" + } + } + }, + new NestedLambdaTestClass() + { + Name = "F", + Children = new List() + { + new NestedLambdaTestClass() + { + Name = "C" + }, + new NestedLambdaTestClass() + { + Name = "F" + } + } + } + } + }, + new NestedLambdaTestClass() + { + Name = "D", + Children = new List() + { + new NestedLambdaTestClass() + { + Name = "E", + Children = new List() + { + new NestedLambdaTestClass() + { + Name = "C" + }, + new NestedLambdaTestClass() + { + Name = "F", + Children = new List() + { + new NestedLambdaTestClass() + { + Name = "C" + }, + new NestedLambdaTestClass() + { + Name = "F" + } + } + } + } + }, + new NestedLambdaTestClass() + { + Name = "G" + } + } + } + } + } + } + }; + var expectedResult = root.GetChildrenIdentifiers( + // root + l1 => l1.Name + l1.GetChildrenIdentifiers( + // level 2, references my parameter, plus original lamda + l2 => l2.Name + l1.Name + l2.GetChildrenIdentifiers( + // level 3, references my parameter, plus parameter from l1 lamda + l3 => l3.Name + l2.Name + l3.GetChildrenIdentifiers( + // level 4, references my parameter, plus all parameters that have been used + l4 => l4.Name + l2.Name + l3.Name + l1.Name + root.Name) + ))); + + var target = new Interpreter(InterpreterOptions.Default | InterpreterOptions.LambdaExpressions); + + var evalResult = target.Eval(@"root.GetChildrenIdentifiers( + l1 => l1.Name + l1.GetChildrenIdentifiers( + l2 => l2.Name + l1.Name + l2.GetChildrenIdentifiers( + l3 => l3.Name + l2.Name + l3.GetChildrenIdentifiers( + l4 => l4.Name + l2.Name + l3.Name + l1.Name + root.Name) + )))", new Parameter(nameof(root), root)); + Assert.AreEqual(expectedResult, evalResult); + } } ///