使用LinqExpressions构建MicroRuleEngine

所以我正在构建一个MicroRuleEngine(很想看到它作为一个OpenSource项目起飞)并且我遇到了一个空引用错误在执行编译的ExpressionTree时我并不完全确定原因。 针对简单属性的规则有效,但是针对Child Properties又称Customer.Client.Address.StreetName等不起作用。

以下是MicroRuleEngine

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Linq.Expressions; namespace Trial { public class MicroRuleEngine { public bool PassesRules(List rules, T toInspect) { bool pass = true; foreach (var rule in rules) { var cr = this.CompileRule(rule); pass = pass && cr.Invoke(toInspect); if (!pass) return pass; } return pass; } public Func CompileRule(Rule r) { var paramUser = Expression.Parameter(typeof(T)); Expression expr = BuildExpr(r, paramUser); // build a lambda function User->bool and compile it return Expression.Lambda<Func>(expr, paramUser).Compile(); } Expression BuildExpr(Rule r, ParameterExpression param) { Expression propExpression; Type propType;// typeof(T).GetProperty(r.MemberName).PropertyType; ExpressionType tBinary; if (r.MemberName.Contains('.')) { // support to be sorted on child fields. String[] childProperties = r.MemberName.Split('.'); var property = typeof(T).GetProperty(childProperties[0]); var paramExp = Expression.Parameter(typeof(T), "SomeObject"); propExpression = Expression.MakeMemberAccess(paramExp, property); for (int i = 1; i  'u.Age == 15' return Expression.MakeBinary(tBinary, propExpression, right); } else { var method = propType.GetMethod(r.Operator); var tParam = method.GetParameters()[0].ParameterType; var right = Expression.Constant(Convert.ChangeType(r.TargetValue, tParam)); // use a method call, eg 'Contains' -> 'u.Tags.Contains(some_tag)' return Expression.Call(propExpression, method, right); } } } public class Rule { public string MemberName { get; set; } public string Operator { get; set; } public string TargetValue { get; set; } } } 

而这是失败的测试

 [TestMethod] public void ChildPropertyRuleTest() { Container container = new Container() { Repository = "TestRepo", Shipment = new Shipment() { OrderNumber = "555" } }; MicroRuleEngine mr = new MicroRuleEngine(); var rules = new List() { new Rule() { MemberName = "Shipment.OrderNumber", Operator = "Contains", TargetValue = "55" } }; var pases = mr.PassesRules(rules, container); Assert.IsTrue(!pases); } 

不要以为你已经看过动态表达式解析器,它被捆绑为VS2008示例的示例项目。 它包含一个名为ExpressionParser的类型,可用于将字符串表达式转换为Expression实例。 我之前使用过这个将字符串表达式转换为可编译的委托,例如,我可以做类似的事情:

 string expression = "(1 + 2)"; var func = FunctionFactory.Create(expression); int result = func(1, 2); // Result should be 3. 

…其中FunctionFactoryExpressionParser类型的包装器。 我也可以这样做:

 expression = "(a * b)"; var func2 = FunctionFactory.Create(expresion new[] { "a", "b" }); int result = func2(10, 50); // Result should be 500; 

或者有点有形的东西:

 expression = "(Age == 5)"; var func3 = FunctionFactory.Create(expression); bool isFive = func3(new Person { Age = 5 }); 

这对你有用吗? 你可以在这里阅读我的博客文章 。

所以我遇到的错误是我试图找到如何访问子属性的所有示例都使用MemberAccess Expressions来向下走属性,我发现使用PropertyExpressions对我所做的简单测试没有问题。 以下是现在正在运行的更新

 public class MicroRuleEngine { public bool PassesRules(List rules, T toInspect) { return this.CompileRules(rules).Invoke(toInspect); } public Func CompileRule(Rule r) { var paramUser = Expression.Parameter(typeof(T)); Expression expr = BuildExpr(r, paramUser); return Expression.Lambda>(expr, paramUser).Compile(); } public Func CompileRules(IList rules) { var paramUser = Expression.Parameter(typeof(T)); List expressions = new List(); foreach (var r in rules) { expressions.Add(BuildExpr(r, paramUser)); } var expr = AndExpressions(expressions); return Expression.Lambda>(expr, paramUser).Compile(); } Expression AndExpressions(IList expressions) { if(expressions.Count == 1) return expressions[0]; Expression exp = Expression.And(expressions[0], expressions[1]); for(int i = 2; expressions.Count > i; i++) { exp = Expression.And(exp, expressions[i]); } return exp; } Expression BuildExpr(Rule r, ParameterExpression param) { Expression propExpression; Type propType; ExpressionType tBinary; if (r.MemberName.Contains('.')) { String[] childProperties = r.MemberName.Split('.'); var property = typeof(T).GetProperty(childProperties[0]); var paramExp = Expression.Parameter(typeof(T), "SomeObject"); propExpression = Expression.PropertyOrField(param, childProperties[0]); for (int i = 1; i < childProperties.Length; i++) { property = property.PropertyType.GetProperty(childProperties[i]); propExpression = Expression.PropertyOrField(propExpression, childProperties[i]); } propType = propExpression.Type; } else { propExpression = Expression.PropertyOrField(param, r.MemberName); propType = propExpression.Type; } // is the operator a known .NET operator? if (ExpressionType.TryParse(r.Operator, out tBinary)) { var right = Expression.Constant(Convert.ChangeType(r.TargetValue, propType)); // use a binary operation, eg 'Equal' -> 'u.Age == 15' return Expression.MakeBinary(tBinary, propExpression, right); } else { var method = propType.GetMethod(r.Operator); var tParam = method.GetParameters()[0].ParameterType; var right = Expression.Constant(Convert.ChangeType(r.TargetValue, tParam)); // use a method call, eg 'Contains' -> 'u.Tags.Contains(some_tag)' return Expression.Call(propExpression, method, right); } } } public class Rule { public string MemberName { get; set; } public string Operator { get; set; } public string TargetValue { get; set; } } 

您的测试中是否可以在您的容器中初始化Shipment属性?

其他一些建议:如果必须使用表达式,请考虑缓存表达式的编译版本,以便可以重复使用它,而不是每次要使用它时都必须重新编译它。

第二,您选择使用表达式而不是仅仅在规则中使用Func是否有特定原因? 通常在创建这样的规则引擎时,我的规则类被定义为:

 public class Rule { public string Description {get; set;} public Func RuleToApply {get; set;} } 

鉴于此,我实例化我的规则集合如下:

  var rules = new List() { new Rule { Description = "OrderNumber Contains 55", RuleToApply = order => order.OrderNumber.Contains("55") } }; 

并且PassesRule变为:

 public bool PassesRules(List rules, T toInspect) { return rules.All(rule => rule(toInspect)); } 

这里的另一个优点是,通过使用字符串和动态构建表达式,我保持了类型安全和重构支持,而不是传递字符串和表达式。

如果要构建可重用的表达式解析器,请记住另一件事,确保在VB和C#中设置测试,因为它们并不总是在封面下生成相同的表达式树。 特别是,为字符串相等性添加VB测试(city =“London”)。 我见过无数的LINQ提供商忽略了这个简单的案例。