From 0c93fd384613f3acc479beefebeb644c37c45a15 Mon Sep 17 00:00:00 2001 From: Jay Oursler Date: Fri, 14 May 2021 17:22:17 -0500 Subject: [PATCH 1/4] Fixes #31 by adding functionality to filter then aggregate IEnumerable properties to then use as the left hand input to an operator --- MicroRuleEngine.Core.Tests/ExampleUsage.cs | 43 ++++++++++++ MicroRuleEngine/MRE.cs | 79 +++++++++++++++++++--- 2 files changed, 113 insertions(+), 9 deletions(-) diff --git a/MicroRuleEngine.Core.Tests/ExampleUsage.cs b/MicroRuleEngine.Core.Tests/ExampleUsage.cs index 42482a6..a4d05fc 100644 --- a/MicroRuleEngine.Core.Tests/ExampleUsage.cs +++ b/MicroRuleEngine.Core.Tests/ExampleUsage.cs @@ -278,5 +278,48 @@ public static Order GetOrder() }; return order; } + + [TestMethod] + public void EnumerableFilterAndAggregation() + { + + + + Order order = GetOrder(); + + if (order.Items.Where(x => x.ItemCode.StartsWith("M")) + .Sum(x => x.Cost) > 6) + { + + } + + + Rule rule = new Rule + { + MemberName = "Items", + EnumerableFilter = new Rule + { + MemberName = "ItemCode", + Operator = "StartsWith", + Inputs = new []{"M"} + }, + EnumerableVauleExpression = new Selector + { + MemberName = "Cost", + Operator = "Sum" + }, + Operator = "GreaterThan", + TargetValue = 5 + }; + + MRE engine = new MRE(); + var compiledRule = engine.CompileRule(rule); + bool passes = compiledRule(order); + Assert.IsTrue(passes); + + order.Items[0].Cost = 4m; + passes = compiledRule(order); + Assert.IsFalse(passes); + } } } diff --git a/MicroRuleEngine/MRE.cs b/MicroRuleEngine/MRE.cs index 8117313..b8f8a00 100644 --- a/MicroRuleEngine/MRE.cs +++ b/MicroRuleEngine/MRE.cs @@ -27,8 +27,8 @@ public class MRE private static readonly Tuple>[] _enumrMethodsByName = new Tuple>[] { - Tuple.Create("Any", new Lazy(() => GetLinqMethod("Any", 2))), - Tuple.Create("All", new Lazy(() => GetLinqMethod("All", 2))), + Tuple.Create("Any", new Lazy(() => GetLinqMethod("Any", 2))), + Tuple.Create("All", new Lazy(() => GetLinqMethod("All", 2))), }; private static readonly Lazy _miIntTryParse = new Lazy(() => typeof(Int32).GetMethod("TryParse", new Type[] { typeof(string), Type.GetType("System.Int32&") })); @@ -42,7 +42,7 @@ public class MRE private static readonly Lazy _miDecimalTryParse = new Lazy(() => typeof(Decimal).GetMethod("TryParse", new Type[] { typeof(string), Type.GetType("System.Decimal&") })); - public Func CompileRule(Rule r) + public FuncCompileRule(Rule r) { var paramUser = Expression.Parameter(typeof(T)); Expression expr = GetExpressionForRule(typeof(T), r, paramUser); @@ -267,6 +267,50 @@ private static Expression BuildExpr(Type type, Rule rule, Expression param, bool propExpression = GetProperty(param, rule.MemberName); propType = propExpression.Type; } + + if(typeof(IEnumerable).IsAssignableFrom(propType)) + { + if (rule.EnumerableFilter != null) + { + var elementType = ElementType(propType); + var lambdaParam = Expression.Parameter(elementType, + "lambdaParam"); + propExpression = Expression.Call(GetLinqMethod("Where", + 2) + .MakeGenericMethod(elementType), + propExpression, + Expression.Lambda(BuildExpr(elementType, + rule.EnumerableFilter, + lambdaParam), + lambdaParam)); + + + } + + if(rule.EnumerableValueExpression != null) + { + Type elementType = ElementType(propType); + ParameterExpression parameter = Expression.Parameter(elementType, "s"); + PropertyInfo property = elementType.GetProperty(rule.EnumerableValueExpression.MemberName); + Expression selector = Expression.Lambda(Expression.MakeMemberAccess(parameter, + property), + parameter); + + MethodInfo generationMethod = GetLinqMethod(rule.EnumerableValueExpression.Operator, + 2, + property.PropertyType); + + var m = generationMethod.MakeGenericMethod(elementType); + + propExpression = Expression.Call(m, + propExpression, + selector); + + + propType = propExpression.Type; + } + } + if (useTryCatch) { propExpression = Expression.TryCatch( @@ -275,6 +319,9 @@ private static Expression BuildExpr(Type type, Rule rule, Expression param, bool ); } + + + // is the operator a known .NET operator? ExpressionType tBinary; @@ -397,6 +444,12 @@ private static MethodInfo GetLinqMethod(string name, int numParameter) .FirstOrDefault(m => m.Name == name && m.GetParameters().Length == numParameter); } + private static MethodInfo GetLinqMethod(string name, int numParameter, Type returnType) + { + return typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public) + .FirstOrDefault((m => m.Name == name && m.GetParameters().Length == numParameter && m.ReturnType == returnType)); + } + private static Expression GetDataRowField(Expression prm, string member, string typeName) { @@ -696,19 +749,27 @@ public static List Operators(Type type, bool addLogicOperators = false } [DataContract] - public class Rule + public class Selector + { + [DataMember] public string MemberName { get; set; } + [DataMember] public string Operator { get; set; } + } + + + [DataContract] + public class Rule : Selector { public Rule() { Inputs = Enumerable.Empty(); } - - [DataMember] public string MemberName { get; set; } - [DataMember] public string Operator { get; set; } + [DataMember] public object TargetValue { get; set; } - [DataMember] public IList Rules { get; set; } - [DataMember] public IEnumerable Inputs { get; set; } + [DataMember] public Rule EnumerableFilter { get; set; } + [DataMember] public Selector EnumerableValueExpression { get; set; } + [DataMember] public IList Rules { get; set; } + [DataMember] public IEnumerable Inputs { get; set; } public static Rule operator |(Rule lhs, Rule rhs) { From ee148297961c559a9239b00d1cdca70ce1383d1f Mon Sep 17 00:00:00 2001 From: Jay Oursler Date: Tue, 18 May 2021 12:26:41 -0500 Subject: [PATCH 2/4] Add the ability to use Value generation on an enum with out needing a MemberValue. --- MicroRuleEngine.Core.Tests/ExampleUsage.cs | 73 +++++++++++++++++++++- MicroRuleEngine/MRE.cs | 63 ++++++++++++++++--- 2 files changed, 125 insertions(+), 11 deletions(-) diff --git a/MicroRuleEngine.Core.Tests/ExampleUsage.cs b/MicroRuleEngine.Core.Tests/ExampleUsage.cs index a4d05fc..b1f5f18 100644 --- a/MicroRuleEngine.Core.Tests/ExampleUsage.cs +++ b/MicroRuleEngine.Core.Tests/ExampleUsage.cs @@ -245,6 +245,11 @@ public void RegexIsMatch()//Had to add a Regex evaluator to make it feel 'Comple Assert.IsFalse(passes); } + public class OrderParent + { + public Order PlacedOrder { get; set; } + } + public static Order GetOrder() { Order order = new Order() @@ -303,7 +308,7 @@ public void EnumerableFilterAndAggregation() Operator = "StartsWith", Inputs = new []{"M"} }, - EnumerableVauleExpression = new Selector + EnumerableValueExpression = new Selector { MemberName = "Cost", Operator = "Sum" @@ -321,5 +326,71 @@ public void EnumerableFilterAndAggregation() passes = compiledRule(order); Assert.IsFalse(passes); } + + [TestMethod] + public void CountAggregation() + { + + Order order = GetOrder(); + + Rule rule = new Rule + { + MemberName = "Items", + EnumerableValueExpression = new Selector + { + Operator = "Count" + }, + Operator = "GreaterThan", + TargetValue = 3 + }; + + MRE engine = new MRE(); + var compiledRule = engine.CompileRule(rule); + bool passes = compiledRule(order); + Assert.IsFalse(passes); + + order.Items.Add(new Item()); + order.Items.Add(new Item()); + + passes = compiledRule(order); + + Assert.IsTrue(passes); + + } + + [TestMethod] + public void EnumerableAggregationOnChild() + { + + + + Order order = GetOrder(); + + var orderParent = new OrderParent() {PlacedOrder = order}; + + + Rule rule = new Rule + { + MemberName = "PlacedOrder.Items", + EnumerableValueExpression = new Selector + { + Operator = "Count" + }, + Operator = "GreaterThan", + TargetValue = 3 + }; + + MRE engine = new MRE(); + var compiledRule = engine.CompileRule(rule); + bool passes = compiledRule(orderParent); + Assert.IsFalse(passes); + + order.Items.Add(new Item()); + order.Items.Add(new Item()); + + passes = compiledRule(orderParent); + + Assert.IsTrue(passes); + } } } diff --git a/MicroRuleEngine/MRE.cs b/MicroRuleEngine/MRE.cs index b8f8a00..1407618 100644 --- a/MicroRuleEngine/MRE.cs +++ b/MicroRuleEngine/MRE.cs @@ -140,12 +140,23 @@ protected static Expression BinaryExpression(IEnumerable expressions private static readonly Regex _regexIndexed = new Regex(@"(?'Collection'\w+)\[(?:(?'Index'\d+)|(?:['""](?'Key'\w+)[""']))\]", RegexOptions.Compiled); - private static Expression GetProperty(Expression param, string propname) + + private static Expression GetProperty(Expression param, + string propname) + { + return GetProperty(param, + propname, + out _); + } + + private static Expression GetProperty(Expression param, string propname, out PropertyInfo propertyInfo) { Expression propExpression = param; String[] childProperties = propname.Split('.'); var propertyType = param.Type; + propertyInfo = null; + foreach (var childprop in childProperties) { var isIndexed = _regexIndexed.Match(childprop); @@ -157,6 +168,7 @@ private static Expression GetProperty(Expression param, string propname) if (collectionProp == null) throw new RulesException( $"Cannot find collection property {collectionname} in class {propertyType.Name} (\"{propname}\")"); + propertyInfo = collectionProp; var collexpr = Expression.PropertyOrField(propExpression, collectionname); Expression expIndex; @@ -193,7 +205,8 @@ private static Expression GetProperty(Expression param, string propname) throw new RulesException( $"Cannot find property {childprop} in class {propertyType.Name} (\"{propname}\")"); propExpression = Expression.PropertyOrField(propExpression, childprop); - propertyType = property.PropertyType; + propertyType = property.PropertyType; + propertyInfo = property; } } @@ -290,21 +303,44 @@ private static Expression BuildExpr(Type type, Rule rule, Expression param, bool if(rule.EnumerableValueExpression != null) { Type elementType = ElementType(propType); - ParameterExpression parameter = Expression.Parameter(elementType, "s"); - PropertyInfo property = elementType.GetProperty(rule.EnumerableValueExpression.MemberName); - Expression selector = Expression.Lambda(Expression.MakeMemberAccess(parameter, - property), - parameter); + ParameterExpression parameter; + + PropertyInfo property; + Expression selector = null; + + if(string.IsNullOrEmpty(rule.EnumerableValueExpression.MemberName)) + { + GetProperty(param, rule.MemberName, out property); + } + else + { + property = elementType.GetProperty(rule.EnumerableValueExpression.MemberName); + parameter = Expression.Parameter(elementType, "s"); + selector = Expression.Lambda(Expression.MakeMemberAccess(parameter, + property), + parameter); + } + + + MethodInfo generationMethod = GetLinqMethod(rule.EnumerableValueExpression.Operator, 2, property.PropertyType); + if(generationMethod == null) + { + generationMethod = GetLinqMethod(rule.EnumerableValueExpression.Operator, rule.TargetValue.GetType()); + } + var m = generationMethod.MakeGenericMethod(elementType); - propExpression = Expression.Call(m, - propExpression, - selector); + propExpression = selector == null + ? Expression.Call(m, + propExpression) + : Expression.Call(m, + propExpression, + selector); propType = propExpression.Type; @@ -450,6 +486,13 @@ private static MethodInfo GetLinqMethod(string name, int numParameter, Type retu .FirstOrDefault((m => m.Name == name && m.GetParameters().Length == numParameter && m.ReturnType == returnType)); } + private static MethodInfo GetLinqMethod(string name, Type returnType) + { + return typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public) + .FirstOrDefault((m => m.Name == name && m.ReturnType == returnType)); + } + + private static Expression GetDataRowField(Expression prm, string member, string typeName) { From 65e194336cbab271a41a60e4961fe16390ce2aac Mon Sep 17 00:00:00 2001 From: Jay Oursler Date: Tue, 18 May 2021 13:51:48 -0500 Subject: [PATCH 3/4] Fix for Serialization and Deserialization issues. --- MicroRuleEngine.Core.Tests/ExampleUsage.cs | 42 ++++++++++++++++++++++ MicroRuleEngine/MRE.cs | 18 ++++++++++ 2 files changed, 60 insertions(+) diff --git a/MicroRuleEngine.Core.Tests/ExampleUsage.cs b/MicroRuleEngine.Core.Tests/ExampleUsage.cs index b1f5f18..2db1971 100644 --- a/MicroRuleEngine.Core.Tests/ExampleUsage.cs +++ b/MicroRuleEngine.Core.Tests/ExampleUsage.cs @@ -4,6 +4,7 @@ using System.Linq.Expressions; using Microsoft.VisualStudio.TestTools.UnitTesting; using MicroRuleEngine.Core.Tests.Models; +using Newtonsoft.Json; namespace MicroRuleEngine.Tests { @@ -392,5 +393,46 @@ public void EnumerableAggregationOnChild() Assert.IsTrue(passes); } + + [TestMethod] + public void SerializeThenDeserialize() + { + + + + Order order = GetOrder(); + + var orderParent = new OrderParent() {PlacedOrder = order}; + + + Rule rule = new Rule + { + MemberName = "PlacedOrder.Items", + EnumerableValueExpression = new Selector + { + Operator = "Count" + }, + Operator = "GreaterThan", + TargetValue = 3 + }; + + var jsonString = JsonConvert.SerializeObject(rule); + + var deserializedRule = JsonConvert.DeserializeObject(jsonString); + + + + MRE engine = new MRE(); + var compiledRule = engine.CompileRule(deserializedRule); + bool passes = compiledRule(orderParent); + Assert.IsFalse(passes); + + order.Items.Add(new Item()); + order.Items.Add(new Item()); + + passes = compiledRule(orderParent); + + Assert.IsTrue(passes); + } } } diff --git a/MicroRuleEngine/MRE.cs b/MicroRuleEngine/MRE.cs index 1407618..c042466 100644 --- a/MicroRuleEngine/MRE.cs +++ b/MicroRuleEngine/MRE.cs @@ -328,10 +328,22 @@ private static Expression BuildExpr(Type type, Rule rule, Expression param, bool 2, property.PropertyType); + + if(generationMethod == null) { generationMethod = GetLinqMethod(rule.EnumerableValueExpression.Operator, rule.TargetValue.GetType()); } + if(generationMethod == null) + { + generationMethod = GetLinqMethod(rule.EnumerableValueExpression.Operator); + } + + if (generationMethod == null) + { + throw new + RulesException($"Unable to find Linq method for {rule.EnumerableValueExpression.Operator}"); + } var m = generationMethod.MakeGenericMethod(elementType); @@ -492,6 +504,12 @@ private static MethodInfo GetLinqMethod(string name, Type returnType) .FirstOrDefault((m => m.Name == name && m.ReturnType == returnType)); } + private static MethodInfo GetLinqMethod(string name) + { + return typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public) + .FirstOrDefault((m => m.Name == name)); + } + private static Expression GetDataRowField(Expression prm, string member, string typeName) From 18e0e780c6bed3465150631e41e469f6f361b2d6 Mon Sep 17 00:00:00 2001 From: Jay Oursler Date: Tue, 18 May 2021 14:20:12 -0500 Subject: [PATCH 4/4] Add additional Test. --- MicroRuleEngine.Core.Tests/ExampleUsage.cs | 61 ++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/MicroRuleEngine.Core.Tests/ExampleUsage.cs b/MicroRuleEngine.Core.Tests/ExampleUsage.cs index 2db1971..918eb92 100644 --- a/MicroRuleEngine.Core.Tests/ExampleUsage.cs +++ b/MicroRuleEngine.Core.Tests/ExampleUsage.cs @@ -422,6 +422,67 @@ public void SerializeThenDeserialize() + MRE engine = new MRE(); + var compiledRule = engine.CompileRule(deserializedRule); + bool passes = compiledRule(orderParent); + Assert.IsFalse(passes); + + order.Items.Add(new Item()); + order.Items.Add(new Item()); + + passes = compiledRule(orderParent); + + Assert.IsTrue(passes); + } + + [TestMethod] + public void SerializeThenDeserializeComplexRules() + { + + + + Order order = GetOrder(); + + var orderParent = new OrderParent() {PlacedOrder = order}; + + + Rule rule = new Rule + { + Operator = "AndAlso", + Rules = new List + { + + new Rule + { + MemberName = "PlacedOrder.Items", + EnumerableValueExpression = new Selector + { + Operator = "Count" + }, + Operator = "GreaterThan", + TargetValue = 3 + }, + + new Rule + { + MemberName = "PlacedOrder.Items", + EnumerableValueExpression = new Selector + { + MemberName = "Cost", + Operator = "Sum" + }, + Operator = "GreaterThan", + TargetValue = 5 + } + } + }; + + var jsonString = JsonConvert.SerializeObject(rule); + + var deserializedRule = JsonConvert.DeserializeObject(jsonString); + + + MRE engine = new MRE(); var compiledRule = engine.CompileRule(deserializedRule); bool passes = compiledRule(orderParent);