如何实现规则引擎?
我有一个数据库表,存储以下内容:
RuleID objectProperty ComparisonOperator TargetValue 1 age 'greater_than' 15 2 username 'equal' 'some_name' 3 tags 'hasAtLeastOne' 'some_tag some_tag2'
现在说我有这些规则的集合:
List<Rule> rules = db.GetRules();
现在我也有一个用户的实例:
User user = db.GetUser(....);
我将如何循环这些规则,并应用逻辑和执行比较等?
if(user.age > 15) if(user.username == "some_name")
由于像'age'或'user_name'这样的对象的属性和比较操作符'great_than'和'equal'一起存储在表中,我怎么可能做到这一点呢?
C#是一个静态types的语言,所以不知道如何前进。
这段代码将规则编译成快速的可执行代码 (使用expression式树 ),不需要任何复杂的switch语句:
(编辑: 通用方法的完整工作示例 )
public Func<User, bool> CompileRule(Rule r) { var paramUser = Expression.Parameter(typeof(User)); Expression expr = BuildExpr(r, paramUser); // build a lambda function User->bool and compile it return Expression.Lambda<Func<User, bool>>(expr, paramUser).Compile(); }
然后你可以写:
List<Rule> rules = new List<Rule> { new Rule ("Age", "GreaterThan", "20"), new Rule ( "Name", "Equal", "John"), new Rule ( "Tags", "Contains", "C#" ) }; // compile the rules once var compiledRules = rules.Select(r => CompileRule(r)).ToList(); public bool MatchesAllRules(User user) { return compiledRules.All(rule => rule(user)); }
这里是BuildExpr的实现:
Expression BuildExpr(Rule r, ParameterExpression param) { var left = MemberExpression.Property(param, r.MemberName); var tProp = typeof(User).GetProperty(r.MemberName).PropertyType; ExpressionType tBinary; // is the operator a known .NET operator? if (ExpressionType.TryParse(r.Operator, out tBinary)) { var right = Expression.Constant(Convert.ChangeType(r.TargetValue, tProp)); // use a binary operation, eg 'Equal' -> 'u.Age == 15' return Expression.MakeBinary(tBinary, left, right); } else { var method = tProp.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(left, method, right); } }
请注意,我使用“GreaterThan”而不是“greater_than”等 – 这是因为“GreaterThan”是运算符的.NET名称,因此我们不需要任何额外的映射。
如果您确实需要自定义名称,则可以构build一个非常简单的字典,并在编译规则之前转换所有运算符:
var nameMap = new Dictionary<string, string> { { "greater_than", "GreaterThan" }, { "hasAtLeastOne", "Contains" } };
请注意,为简单起见,代码使用Usertypes。 您可以用普通typesTreplace用户,使其具有适用于任何types对象的通用规则编译器 。
另外请注意:即使在引入expression式树API之前,使用Reflection.Emit也可以即时生成代码。 LambdaExpression.Compile()方法在封面下使用Reflection.Emit(可以使用ILSpy查看 )。
下面是一些编译原理和做这个工作的代码。 基本上使用两个字典,一个包含从运算符名称到布尔函数的映射,另一个包含从用户types的属性名称到用于调用属性getter(如果为public)的PropertyInfos的映射。 将User实例和表中的三个值传递给静态的Apply方法。
class User { public int Age { get; set; } public string UserName { get; set; } } class Operator { private static Dictionary<string, Func<object, object, bool>> s_operators; private static Dictionary<string, PropertyInfo> s_properties; static Operator() { s_operators = new Dictionary<string, Func<object, object, bool>>(); s_operators["greater_than"] = new Func<object, object, bool>(s_opGreaterThan); s_operators["equal"] = new Func<object, object, bool>(s_opEqual); s_properties = typeof(User).GetProperties().ToDictionary(propInfo => propInfo.Name); } public static bool Apply(User user, string op, string prop, object target) { return s_operators[op](GetPropValue(user, prop), target); } private static object GetPropValue(User user, string prop) { PropertyInfo propInfo = s_properties[prop]; return propInfo.GetGetMethod(false).Invoke(user, null); } #region Operators static bool s_opGreaterThan(object o1, object o2) { if (o1 == null || o2 == null || o1.GetType() != o2.GetType() || !(o1 is IComparable)) return false; return (o1 as IComparable).CompareTo(o2) > 0; } static bool s_opEqual(object o1, object o2) { return o1 == o2; } //etc. #endregion public static void Main(string[] args) { User user = new User() { Age = 16, UserName = "John" }; Console.WriteLine(Operator.Apply(user, "greater_than", "Age", 15)); Console.WriteLine(Operator.Apply(user, "greater_than", "Age", 17)); Console.WriteLine(Operator.Apply(user, "equal", "UserName", "John")); Console.WriteLine(Operator.Apply(user, "equal", "UserName", "Bob")); } }
reflection是你最多才多艺的答案。 你有三列数据,需要用不同的方式处理:
-
你的字段名称。 reflection是从编码字段名称中获取值的方法。
-
您的比较运算符。 这些应该是有限的,所以一个案例陈述应该最容易处理。 特别是因为他们中的一些(有一个或多个)稍微复杂一些。
-
你的比较价值。 如果这些都是直接值,那么这很容易,但是你将会把多个条目分开。 但是,如果也是字段名,也可以使用reflection。
我会采取更多的方法:
var value = user.GetType().GetProperty("age").GetValue(user, null); //Thank you Rick! Saves me remembering it; switch(rule.ComparisonOperator) case "equals": return EqualComparison(value, rule.CompareTo) case "is_one_or_more_of" return IsInComparison(value, rule.CompareTo)
等等
它使您可以灵活地添加更多选项进行比较。 这也意味着你可以在比较方法中编写任何你想要的typesvalidation,并且使它们像你想的那样复杂。 在这里也可以selectCompareTo作为recursion调用返回到另一行,或者作为字段值,可以这样做:
return IsInComparison(value, EvaluateComparison(rule.CompareTo))
这一切都取决于未来的可能性….
我构build了一个规则引擎,采用与您在问题中概述的方法不同的方法,但是我认为您会发现它比您当前的方法更灵活。
您目前的方法似乎集中在一个单一的实体,“用户”,你的持续规则确定“属性名”,“运营商”和“价值”。 我的模式,而不是存储谓词(Func <T,BOOL>)在我的数据库中的“expression式”列的C#代码。 在当前devise中,使用代码生成我正在查询数据库中的“规则”,并用“规则”types编译程序集,每个程序都有一个“testing”方法。 以下是每个规则实现的接口的签名:
public interface IDataRule<TEntity> { /// <summary> /// Evaluates the validity of a rule given an instance of an entity /// </summary> /// <param name="entity">Entity to evaluate</param> /// <returns>result of the evaluation</returns> bool Test(TEntity entity); /// <summary> /// The unique indentifier for a rule. /// </summary> int RuleId { get; set; } /// <summary> /// Common name of the rule, not unique /// </summary> string RuleName { get; set; } /// <summary> /// Indicates the message used to notify the user if the rule fails /// </summary> string ValidationMessage { get; set; } /// <summary> /// indicator of whether the rule is enabled or not /// </summary> bool IsEnabled { get; set; } /// <summary> /// Represents the order in which a rule should be executed relative to other rules /// </summary> int SortOrder { get; set; } }
当应用程序首次执行时,“Expression”被编译为“Test”方法的主体。 正如您所看到的,表格中的其他列也显示为规则中的第一类属性,以便开发人员可以灵活地创build用户如何得到失败或成功通知的体验。
在您的应用程序中生成内存中的程序集是一次性事件,您在评估规则时无需使用reflection,从而获得性能提升。 在运行时检查expression式,因为如果属性名称拼写错误等,程序集将无法正确生成
创build内存组装的机制如下:
- 从数据库加载您的规则
- 迭代规则和for-each,使用StringBuilder和一些string连接编写表示从IDataRuleinheritance的类的Text
- 使用CodeDOM编译 – 更多信息
这实际上很简单,因为大部分代码是构造函数中的属性实现和值初始化。 除此之外,唯一的其他代码是expression式。
注意:由于CodeDOM的限制,您的expression式必须是.NET 2.0(不支持lambdaexpression式或其他C#3.0function)。
这是一些示例代码。
sb.AppendLine(string.Format("\tpublic class {0} : SomeCompany.ComponentModel.IDataRule<{1}>", className, typeName)); sb.AppendLine("\t{"); sb.AppendLine("\t\tprivate int _ruleId = -1;"); sb.AppendLine("\t\tprivate string _ruleName = \"\";"); sb.AppendLine("\t\tprivate string _ruleType = \"\";"); sb.AppendLine("\t\tprivate string _validationMessage = \"\";"); /// ... sb.AppendLine("\t\tprivate bool _isenabled= false;"); // constructor sb.AppendLine(string.Format("\t\tpublic {0}()", className)); sb.AppendLine("\t\t{"); sb.AppendLine(string.Format("\t\t\tRuleId = {0};", ruleId)); sb.AppendLine(string.Format("\t\t\tRuleName = \"{0}\";", ruleName.TrimEnd())); sb.AppendLine(string.Format("\t\t\tRuleType = \"{0}\";", ruleType.TrimEnd())); sb.AppendLine(string.Format("\t\t\tValidationMessage = \"{0}\";", validationMessage.TrimEnd())); // ... sb.AppendLine(string.Format("\t\t\tSortOrder = {0};", sortOrder)); sb.AppendLine("\t\t}"); // properties sb.AppendLine("\t\tpublic int RuleId { get { return _ruleId; } set { _ruleId = value; } }"); sb.AppendLine("\t\tpublic string RuleName { get { return _ruleName; } set { _ruleName = value; } }"); sb.AppendLine("\t\tpublic string RuleType { get { return _ruleType; } set { _ruleType = value; } }"); /// ... more properties -- omitted sb.AppendLine(string.Format("\t\tpublic bool Test({0} entity) ", typeName)); sb.AppendLine("\t\t{"); // ############################################################# // NOTE: This is where the expression from the DB Column becomes // the body of the Test Method, such as: return "entity.Prop1 < 5" // ############################################################# sb.AppendLine(string.Format("\t\t\treturn {0};", expressionText.TrimEnd())); sb.AppendLine("\t\t}"); // close method sb.AppendLine("\t}"); // close Class
除此之外,我做了一个名为“DataRuleCollection”的类,它实现了ICollection>。 这使我能够创build一个“TestAll”function和一个索引器来按名称执行特定的规则。 以下是这两种方法的实现。
/// <summary> /// Indexer which enables accessing rules in the collection by name /// </summary> /// <param name="ruleName">a rule name</param> /// <returns>an instance of a data rule or null if the rule was not found.</returns> public IDataRule<TEntity, bool> this[string ruleName] { get { return Contains(ruleName) ? list[ruleName] : null; } } // in this case the implementation of the Rules Collection is: // DataRulesCollection<IDataRule<User>> and that generic flows through to the rule. // there are also some supporting concepts here not otherwise outlined, such as a "FailedRules" IList public bool TestAllRules(User target) { rules.FailedRules.Clear(); var result = true; foreach (var rule in rules.Where(x => x.IsEnabled)) { result = rule.Test(target); if (!result) { rules.FailedRules.Add(rule); } } return (rules.FailedRules.Count == 0); }
更多代码:有与代码生成相关的代码请求。 我将这个function封装在一个名为“RulesAssemblyGenerator”的类中,我在下面包含这个类。
namespace Xxx.Services.Utils { public static class RulesAssemblyGenerator { static List<string> EntityTypesLoaded = new List<string>(); public static void Execute(string typeName, string scriptCode) { if (EntityTypesLoaded.Contains(typeName)) { return; } // only allow the assembly to load once per entityType per execution session Compile(new CSharpCodeProvider(), scriptCode); EntityTypesLoaded.Add(typeName); } private static void Compile(CodeDom.CodeDomProvider provider, string source) { var param = new CodeDom.CompilerParameters() { GenerateExecutable = false, IncludeDebugInformation = false, GenerateInMemory = true }; var path = System.Reflection.Assembly.GetExecutingAssembly().Location; var root_Dir = System.IO.Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "Bin"); param.ReferencedAssemblies.Add(path); // Note: This dependencies list are included as assembly reference and they should list out all dependencies // That you may reference in your Rules or that your entity depends on. // some assembly names were changed... clearly. var dependencies = new string[] { "yyyyyy.dll", "xxxxxx.dll", "NHibernate.dll", "ABC.Helper.Rules.dll" }; foreach (var dependency in dependencies) { var assemblypath = System.IO.Path.Combine(root_Dir, dependency); param.ReferencedAssemblies.Add(assemblypath); } // reference .NET basics for C# 2.0 and C#3.0 param.ReferencedAssemblies.Add(@"C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\System.dll"); param.ReferencedAssemblies.Add(@"C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.5\System.Core.dll"); var compileResults = provider.CompileAssemblyFromSource(param, source); var output = compileResults.Output; if (compileResults.Errors.Count != 0) { CodeDom.CompilerErrorCollection es = compileResults.Errors; var edList = new List<DataRuleLoadExceptionDetails>(); foreach (CodeDom.CompilerError s in es) edList.Add(new DataRuleLoadExceptionDetails() { Message = s.ErrorText, LineNumber = s.Line }); var rde = new RuleDefinitionException(source, edList.ToArray()); throw rde; } } } }
如果还有其他问题或意见或要求进一步的代码示例,请告诉我。
如果你只有less数几个属性和操作符,阻力最小的path就是把所有的检查都编码为这样的特殊情况:
public bool ApplyRules(List<Rule> rules, User user) { foreach (var rule in rules) { IComparable value = null; object limit = null; if (rule.objectProperty == "age") { value = user.age; limit = Convert.ToInt32(rule.TargetValue); } else if (rule.objectProperty == "username") { value = user.username; limit = rule.TargetValue; } else throw new InvalidOperationException("invalid property"); int result = value.CompareTo(limit); if (rule.ComparisonOperator == "equal") { if (!(result == 0)) return false; } else if (rule.ComparisonOperator == "greater_than") { if (!(result > 0)) return false; } else throw new InvalidOperationException("invalid operator"); } return true; }
如果你有很多属性,你可能会发现桌子驱动的方法更可口。 在这种情况下,您将创build一个静态Dictionary
,将属性名称映射到匹配的Func<User, object>
,比如Func<User, object>
。
如果您在编译时不知道属性的名称,或者想避免每个属性出现特殊情况并且不想使用表格方法,则可以使用reflection来获取属性。 例如:
var value = user.GetType().GetProperty("age").GetValue(user, null);
但是由于TargetValue
可能是一个string
,如果需要的话,您需要小心地从规则表中进行types转换。
带扩展方法的数据types定位方法如何:
public static class RoleExtension { public static bool Match(this Role role, object obj ) { var property = obj.GetType().GetProperty(role.objectProperty); if (property.PropertyType == typeof(int)) { return ApplyIntOperation(role, (int)property.GetValue(obj, null)); } if (property.PropertyType == typeof(string)) { return ApplyStringOperation(role, (string)property.GetValue(obj, null)); } if (property.PropertyType.GetInterface("IEnumerable<string>",false) != null) { return ApplyListOperation(role, (IEnumerable<string>)property.GetValue(obj, null)); } throw new InvalidOperationException("Unknown PropertyType"); } private static bool ApplyIntOperation(Role role, int value) { var targetValue = Convert.ToInt32(role.TargetValue); switch (role.ComparisonOperator) { case "greater_than": return value > targetValue; case "equal": return value == targetValue; //... default: throw new InvalidOperationException("Unknown ComparisonOperator"); } } private static bool ApplyStringOperation(Role role, string value) { //... throw new InvalidOperationException("Unknown ComparisonOperator"); } private static bool ApplyListOperation(Role role, IEnumerable<string> value) { var targetValues = role.TargetValue.Split(' '); switch (role.ComparisonOperator) { case "hasAtLeastOne": return value.Any(v => targetValues.Contains(v)); //... } throw new InvalidOperationException("Unknown ComparisonOperator"); } }
比你可以这样说:
var myResults = users.Where(u => roles.All(r => r.Match(u)));
虽然回答“如何实现规则引擎(在C#中)”问题的最明显的方法是按顺序执行一组给定的规则,但这通常被认为是一个天真的实现(并不意味着它不起作用:-)
在你的情况下,它似乎是“足够好”,因为你的问题似乎更多的是“如何按顺序运行一组规则”,而lambda /expression式树(Martin的答案)当然是最优雅的方式,如果你配备最新的C#版本。
然而对于更高级的场景,这里有一个Retealgorithm的链接,这个algorithm实际上在很多商业规则引擎系统中实现,另一个链接到NRuler ,这个algorithm在C#中的实现。
马丁的回答很好。 我实际上制定了一个和他的想法一样的规则引擎。 我很惊讶,这几乎是一样的。 我已经包含了一些他的代码来改进它。 虽然我已经做了处理更复杂的规则。
你可以看看Yare.NET
或者在Nuget下载
如何使用工作stream规则引擎?
您可以在没有工作stream的情况下执行Windows工作stream规则,请参阅Guy Burstein的博客: http : //blogs.microsoft.co.il/blogs/bursteg/archive/2006/10/11/RuleExecutionWithoutWorkflow.aspx
并以编程方式创build您的规则,请参阅Stephen Kaufman的WebLog
我添加实现的和,或规则之间我添加了类RuleExpression代表树的根,可以叶是简单的规则或可以和,或二进制expression式,因为他们没有规则和有expression式:
public class RuleExpression { public NodeOperator NodeOperator { get; set; } public List<RuleExpression> Expressions { get; set; } public Rule Rule { get; set; } public RuleExpression() { } public RuleExpression(Rule rule) { NodeOperator = NodeOperator.Leaf; Rule = rule; } public RuleExpression(NodeOperator nodeOperator, List<RuleExpression> expressions, Rule rule) { this.NodeOperator = nodeOperator; this.Expressions = expressions; this.Rule = rule; } } public enum NodeOperator { And, Or, Leaf }
我有另一个类编译ruleExpression到一个Func<T, bool>:
public static Func<T, bool> CompileRuleExpression<T>(RuleExpression ruleExpression) { //Input parameter var genericType = Expression.Parameter(typeof(T)); var binaryExpression = RuleExpressionToOneExpression<T>(ruleExpression, genericType); var lambdaFunc = Expression.Lambda<Func<T, bool>>(binaryExpression, genericType); return lambdaFunc.Compile(); } private static Expression RuleExpressionToOneExpression<T>(RuleExpression ruleExpression, ParameterExpression genericType) { if (ruleExpression == null) { throw new ArgumentNullException(); } Expression finalExpression; //check if node is leaf if (ruleExpression.NodeOperator == NodeOperator.Leaf) { return RuleToExpression<T>(ruleExpression.Rule, genericType); } //check if node is NodeOperator.And if (ruleExpression.NodeOperator.Equals(NodeOperator.And)) { finalExpression = Expression.Constant(true); ruleExpression.Expressions.ForEach(expression => { finalExpression = Expression.AndAlso(finalExpression, expression.NodeOperator.Equals(NodeOperator.Leaf) ? RuleToExpression<T>(expression.Rule, genericType) : RuleExpressionToOneExpression<T>(expression, genericType)); }); return finalExpression; } //check if node is NodeOperator.Or else { finalExpression = Expression.Constant(false); ruleExpression.Expressions.ForEach(expression => { finalExpression = Expression.Or(finalExpression, expression.NodeOperator.Equals(NodeOperator.Leaf) ? RuleToExpression<T>(expression.Rule, genericType) : RuleExpressionToOneExpression<T>(expression, genericType)); }); return finalExpression; } } public static BinaryExpression RuleToExpression<T>(Rule rule, ParameterExpression genericType) { try { Expression value = null; //Get Comparison property var key = Expression.Property(genericType, rule.ComparisonPredicate); Type propertyType = typeof(T).GetProperty(rule.ComparisonPredicate).PropertyType; //convert case is it DateTimeOffset property if (propertyType == typeof(DateTimeOffset)) { var converter = TypeDescriptor.GetConverter(propertyType); value = Expression.Constant((DateTimeOffset)converter.ConvertFromString(rule.ComparisonValue)); } else { value = Expression.Constant(Convert.ChangeType(rule.ComparisonValue, propertyType)); } BinaryExpression binaryExpression = Expression.MakeBinary(rule.ComparisonOperator, key, value); return binaryExpression; } catch (FormatException) { throw new Exception("Exception in RuleToExpression trying to convert rule Comparison Value"); } catch (Exception e) { throw new Exception(e.Message); } }
我在Martin Konicek答案中遇到了一个区分大小写的问题,所以如果您想要rule.MemberName
不区分大小写,只需添加
var tProp = typeof(User).GetProperty(r.MemberName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance).PropertyType;