Как реализовать механизм правил?
у меня есть таблица БД, которая хранит следующее:
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")
поскольку свойство объекта, такое как "возраст" или "имя_пользователя", хранится в таблице вместе с оператором сравнения "great_than" и "equal", как я могу это сделать сделать это?
C# является статически типизированным языком, поэтому не уверен, как идти вперед.
11 ответов:
этот фрагмент компилирует правила в быстрый исполняемый код (через деревья выражений) и не требует каких-либо сложных инструкций:
(Edit:полный рабочий пример с общим методом)
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, e.g. '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, e.g. 'Contains' -> 'u.Tags.Contains(some_tag)' return Expression.Call(left, method, right); } }
обратите внимание, что я использовал 'больше' вместо 'greater_than' и т. д. - вот потому что чем является .Нетто-имя оператора, поэтому мы не нуждаемся в каких-либо дополнительных карт.
Если вам действительно нужны пользовательские имена, вы можете построить очень простой словарь и просто перевести все операторы перед компиляцией правил:
var nameMap = new Dictionary<string, string> { { "greater_than", "GreaterThan" }, { "hasAtLeastOne", "Contains" } };
обратите внимание, что код использует тип User для простоты. Вы можете заменить пользователя универсальным типом T, чтобы иметь компилятор общих правил для любых типов объектов.
также Примечание: генерация кода на муха была возможна еще до того, как был введен API деревьев выражений, используя отражение.Испускают. Метод лямбда-выражения.Компилировать() использует отражение.Выбрасывают под чехлы (вы можете увидеть это с помощью ILSpy).
вот код, который компилируется и работает. В основном используйте два словаря, один из которых содержит сопоставление имен операторов с булевыми функциями, а другой содержит сопоставление имен свойств пользовательского типа с PropertyInfos, используемым для вызова метода получения свойств (если он открыт). Вы передаете экземпляр пользователя и три значения из таблицы в статический метод 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")); } }
Я построил механизм правил, который использует другой подход, чем вы изложили в своем вопросе, но я думаю, что вы найдете его гораздо более гибким, чем ваш текущий подход.
ваш текущий подход, похоже, сосредоточен на одном объекте "Пользователь", и ваши постоянные правила определяют "имя свойства", "оператор" и "значение". Вместо этого мой шаблон хранит код C# для предиката (Func
) в столбце "выражение" в моей базе данных. В текущей конструкции, используя код поколение я запрашиваю " правила "из своей базы данных и компилирую сборку с типами" правил", Каждый из которых имеет метод" Test". Вот подпись для интерфейса, в котором реализовано каждое правило: 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; } }
"выражение "компилируется как тело метода" Test " при первом выполнении приложения. Как вы можете видеть, другие столбцы в таблице также отображаются как первоклассные свойства в правиле, чтобы разработчик мог гибко создавать опыт для того, как пользователь получает уведомление об ошибке или успехе.
создание сборки в памяти-это 1-кратное появление во время вашего приложения, и вы получаете прирост производительности, не используя отражение при оценке ваших правил. Ваши выражения проверяются во время выполнения, так как сборка не будет генерироваться правильно, если имя свойства написано с ошибкой и т. д.
механика создания сборки в памяти выглядит следующим образом:
- загрузить правила из DB
- повторите правила и для каждого, используя StringBuilder и некоторую конкатенацию строк, напишите текст, представляющий класс, который наследует от IDataRule
- компиляция с использованием CodeDOM -- подробнее
это на самом деле довольно просто, потому что для большинства этот код реализации свойств и значение инициализации в конструкторе. Кроме того, единственным другим кодом является выражение.
Примечание: есть ограничение, что ваше выражение должно быть .NET 2.0 (без лямбд или других функций C# 3.0) из-за ограничения в CodeDOM.вот пример кода для этого.
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>. Это позволило мне создать возможность "TestAll" и индексатор для выполнения определенного правила по имени. Вот реализации для этих двух методы.
/// <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); }
еще код: был запрос на код, связанный с генерацией кода. Я инкапсулировал функциональность в классе под названием "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; } } } }
если они есть другое вопросы или комментарии или запросы для дальнейших образцов кода, дайте мне знать.
отражение ваш самый разносторонний ответ. У вас есть три столбца данных, и их нужно обрабатывать по-разному:
ваше имя поле. Отражение-это способ получить значение из кодированного имени Поля.
ваш оператор сравнения. Их должно быть ограниченное количество, поэтому оператор case должен обрабатывать их наиболее легко. Тем более что некоторые из них ( один или несколько ) немного больше сложный.
ваше значение сравнения. Если это все прямые значения, то это легко, хотя вам придется разделить несколько записей вверх. Однако вы также можете использовать отражение, если они также являются именами полей.
Я бы взял подход больше нравится:
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)
etc. так далее.
это дает вам гибкость для добавления дополнительных вариантов для сравнения. Это также означает, что вы можете кодировать в методах сравнения любого типа проверка, которую вы можете захотеть, и сделать их такими сложными, как вы хотите. Здесь также есть опция для CompareTo, которая будет оцениваться как рекурсивный вызов другой строки или как значение поля, которое может быть выполнено следующим образом:
return IsInComparison(value, EvaluateComparison(rule.CompareTo))
все зависит от возможностей на будущее....
если у вас есть только несколько свойств и операторов, путь наименьшего сопротивления состоит в том, чтобы просто закодировать все проверки как особые случаи, такие как:
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; }
если у вас есть много свойств, вы можете найти в таблице подход более приемлемым. В этом случае вы создадите статический
Dictionary
что сопоставляет имена свойств для делегатов, соответствующих, скажем,Func<User, object>
.если вы не знаете имена свойств во время компиляции, или вы хотите, чтобы избежать специальные случаи для каждого свойства и не хотите использовать табличный подход, вы можете использовать отражение для получения свойств. Например:
var value = user.GetType().GetProperty("age").GetValue(user, null);
но так как
TargetValue
наверноеstring
, вам нужно будет позаботиться о том, чтобы сделать преобразование типов из таблицы правил, если это необходимо.
Как насчет ориентированного на тип данных подхода с методом расширения:
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#)" вопрос заключается в последовательном выполнении заданного набора правил, это в целом рассматривается как наивная реализация (не означает, что она не работает: -)
кажется, что это "достаточно хорошо" в вашем случае, потому что ваша проблема больше похожа на "как запустить набор правил в последовательности", а дерево лямбда/выражение (ответ Мартина), безусловно, является самым элегантным способом в этом вопросе, если вы оснащены с последними версиями C#.
однако для более сложных сценариев, вот ссылка на Алгоритм Rete это фактически реализовано во многих коммерческих системах управления правилами, и еще одна ссылка на NRuler реализация этого алгоритма на C#.
ответ Мартина был довольно хорош. Я на самом деле сделал движок правил, который имеет ту же идею, что и его. И я был удивлен, что это почти то же самое. Я включил некоторые из его кода, чтобы несколько улучшить его. Хотя я сделал это, чтобы справиться с более сложными правилами.
вы можете посмотреть на Yare.NET
или скачать его в Nuget
Как насчет использования механизма бизнес-правил?
вы можете выполнять правила рабочего процесса Windows без рабочего процесса смотрите блог Гая Бурштейна: http://blogs.microsoft.co.il/blogs/bursteg/archive/2006/10/11/RuleExecutionWithoutWorkflow.aspx
а чтобы программно создать свои правила, смотрите Веблог Стивена Кауфмана
я добавил реализацию и или между правилами я добавил класс RuleExpression, которые представляют корень дерева, которое может быть листом простое правило или может быть и, или двоичные выражения там для них нет правила и есть выражения:
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 в one
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); } }