Comment implémenter un moteur de règles?

J’ai une table de firebase database qui stocke les éléments suivants:

RuleID objectProperty ComparisonOperator TargetValue 1 age 'greater_than' 15 2 username 'equal' 'some_name' 3 tags 'hasAtLeastOne' 'some_tag some_tag2' 

Maintenant, disons que j’ai une collection de ces règles:

 List rules = db.GetRules(); 

J’ai maintenant une instance d’un utilisateur également:

 User user = db.GetUser(....); 

Comment passer en revue ces règles et appliquer la logique et effectuer les comparaisons, etc.?

 if(user.age > 15) if(user.username == "some_name") 

Puisque la propriété de l’object comme ‘age’ ou ‘user_name’ est stockée dans la table, avec l’opérasortingce ‘great_than’ et ‘equal’, comment pourrais-je faire cela?

C # est un langage typé statiquement, donc pas sûr de savoir comment aller de l’avant.

Cet extrait de code comstack les règles dans un code exécutable rapide (à l’aide d’ arbres d’expression ) et ne nécessite aucune instruction de commutation compliquée:

(Modifier: exemple de travail complet avec méthode générique )

 public Func ComstackRule(Rule r) { var paramUser = Expression.Parameter(typeof(User)); Expression expr = BuildExpr(r, paramUser); // build a lambda function User->bool and comstack it return Expression.Lambda>(expr, paramUser).Comstack(); } 

Vous pouvez alors écrire:

 List rules = new List { new Rule ("Age", "GreaterThan", "20"), new Rule ( "Name", "Equal", "John"), new Rule ( "Tags", "Contains", "C#" ) }; // comstack the rules once var comstackdRules = rules.Select(r => ComstackRule(r)).ToList(); public bool MatchesAllRules(User user) { return comstackdRules.All(rule => rule(user)); } 

Voici l’implémentation de 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); } } 

Notez que j’ai utilisé ‘GreaterThan’ au lieu de ‘larger_than’, etc. – parce que ‘GreaterThan’ est le nom .NET de l’opérateur, nous n’avons donc pas besoin de mappage supplémentaire.

Si vous avez vraiment besoin de noms personnalisés, vous pouvez créer un dictionnaire très simple et traduire tous les opérateurs avant de comstackr les règles:

 var nameMap = new Dictionary { { "greater_than", "GreaterThan" }, { "hasAtLeastOne", "Contains" } }; 

Notez que le code utilise le type User pour plus de simplicité. Vous pouvez remplacer l’utilisateur par un type générique T pour avoir un compilateur de règles générique pour tous les types d’objects.

Notez également que la génération de code à la volée était possible avant même l’introduction de l’API des expressions, en utilisant Reflection.Emit. La méthode LambdaExpression.Comstack () utilise Reflection.Emit sous les couvertures (vous pouvez voir ceci en utilisant ILSpy ).

Voici un code qui comstack tel quel et fait le travail. Fondamentalement, utilisez deux dictionnaires, l’un contenant un mappage des noms d’opérateur vers les fonctions booléennes, et un autre contenant une carte des noms de propriété du type User vers PropertyInfos utilisé pour appeler la propriété getter (si public). Vous transmettez l’instance d’utilisateur et les trois valeurs de votre table à la méthode statique static.

 class User { public int Age { get; set; } public ssortingng UserName { get; set; } } class Operator { private static Dictionary> s_operators; private static Dictionary s_properties; static Operator() { s_operators = new Dictionary>(); s_operators["greater_than"] = new Func(s_opGreaterThan); s_operators["equal"] = new Func(s_opEqual); s_properties = typeof(User).GetProperties().ToDictionary(propInfo => propInfo.Name); } public static bool Apply(User user, ssortingng op, ssortingng prop, object target) { return s_operators[op](GetPropValue(user, prop), target); } private static object GetPropValue(User user, ssortingng 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(ssortingng[] 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")); } } 

J’ai construit un moteur de règles qui adopte une approche différente de celle que vous avez décrite dans votre question, mais je pense que vous trouverez que celle-ci est beaucoup plus flexible que votre approche actuelle.

Votre approche actuelle semble être axée sur une seule entité, “User”, et vos règles persistantes identifient “propertyname”, “operator” et “value”. Mon modèle, à la place, stocke le code C # pour un prédicat (Func ) dans une colonne “Expression” de ma firebase database. Dans la conception actuelle, en utilisant la génération de code, j’interroge les “règles” de ma firebase database et comstack un assemblage avec les types “Rule”, chacun avec une méthode “Test”. Voici la signature de l’interface implémentée dans chaque règle:

 public interface IDataRule { ///  /// Evaluates the validity of a rule given an instance of an entity ///  /// Entity to evaluate /// result of the evaluation bool Test(TEntity entity); ///  /// The unique indentifier for a rule. ///  int RuleId { get; set; } ///  /// Common name of the rule, not unique ///  ssortingng RuleName { get; set; } ///  /// Indicates the message used to notify the user if the rule fails ///  ssortingng ValidationMessage { get; set; } ///  /// indicator of whether the rule is enabled or not ///  bool IsEnabled { get; set; } ///  /// Represents the order in which a rule should be executed relative to other rules ///  int SortOrder { get; set; } } 

La “Expression” est compilée en tant que corps de la méthode “Test” lors de la première exécution de l’application. Comme vous pouvez le voir, les autres colonnes de la table sont également apparues en tant que propriétés de première classe sur la règle, ce qui permet au développeur de créer une expérience sur la manière dont l’utilisateur est averti de l’échec ou du succès.

La génération d’un assembly en mémoire est une occurrence unique au cours de votre application et vous obtenez un gain de performance en évitant d’utiliser la reflection lors de l’évaluation de vos règles. Vos expressions sont vérifiées à l’exécution car l’assembly ne sera pas généré correctement si un nom de propriété est mal orthographié, etc.

Les mécanismes de création d’un assemblage en mémoire sont les suivants:

  • Chargez vos règles depuis la firebase database
  • itérer sur les règles et pour-chaque, en utilisant un SsortingngBuilder et une concaténation de chaînes écrivant le texte représentant une classe qui hérite de IDataRule
  • comstackr en utilisant CodeDOM – plus d’infos

C’est en fait assez simple car pour la plupart ce code est une implémentation de propriété et une initialisation de valeur dans le constructeur. De plus, le seul autre code est l’expression.
REMARQUE: il existe une limitation selon laquelle votre expression doit être .NET 2.0 (pas de fonctionnalités lambdas ou autres fonctionnalités C # 3.0) en raison d’une limitation de CodeDOM.

Voici un exemple de code pour cela.

 sb.AppendLine(ssortingng.Format("\tpublic class {0} : SomeCompany.ComponentModel.IDataRule<{1}>", className, typeName)); sb.AppendLine("\t{"); sb.AppendLine("\t\tprivate int _ruleId = -1;"); sb.AppendLine("\t\tprivate ssortingng _ruleName = \"\";"); sb.AppendLine("\t\tprivate ssortingng _ruleType = \"\";"); sb.AppendLine("\t\tprivate ssortingng _validationMessage = \"\";"); /// ... sb.AppendLine("\t\tprivate bool _isenabled= false;"); // constructor sb.AppendLine(ssortingng.Format("\t\tpublic {0}()", className)); sb.AppendLine("\t\t{"); sb.AppendLine(ssortingng.Format("\t\t\tRuleId = {0};", ruleId)); sb.AppendLine(ssortingng.Format("\t\t\tRuleName = \"{0}\";", ruleName.TrimEnd())); sb.AppendLine(ssortingng.Format("\t\t\tRuleType = \"{0}\";", ruleType.TrimEnd())); sb.AppendLine(ssortingng.Format("\t\t\tValidationMessage = \"{0}\";", validationMessage.TrimEnd())); // ... sb.AppendLine(ssortingng.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 ssortingng RuleName { get { return _ruleName; } set { _ruleName = value; } }"); sb.AppendLine("\t\tpublic ssortingng RuleType { get { return _ruleType; } set { _ruleType = value; } }"); /// ... more properties -- omitted sb.AppendLine(ssortingng.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 

Au-delà, j'ai créé une classe appelée "DataRuleCollection", qui implémentait ICollection>. Cela m'a permis de créer une fonctionnalité "TestAll" et un indexeur pour exécuter une règle spécifique par nom. Voici les implémentations pour ces deux méthodes.

  ///  /// Indexer which enables accessing rules in the collection by name ///  /// a rule name /// an instance of a data rule or null if the rule was not found. public IDataRule this[ssortingng ruleName] { get { return Contains(ruleName) ? list[ruleName] : null; } } // in this case the implementation of the Rules Collection is: // DataRulesCollection> 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); } 

PLUS CODE: Il y avait une demande pour le code lié à la génération de code. J'ai encapsulé les fonctionnalités dans une classe appelée "RulesAssemblyGenerator" que j'ai incluse ci-dessous.

 namespace Xxx.Services.Utils { public static class RulesAssemblyGenerator { static List EntityTypesLoaded = new List(); public static void Execute(ssortingng typeName, ssortingng scriptCode) { if (EntityTypesLoaded.Contains(typeName)) { return; } // only allow the assembly to load once per entityType per execution session Comstack(new CSharpCodeProvider(), scriptCode); EntityTypesLoaded.Add(typeName); } private static void Comstack(CodeDom.CodeDomProvider provider, ssortingng source) { var param = new CodeDom.ComstackrParameters() { 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 ssortingng[] { "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 comstackResults = provider.ComstackAssemblyFromSource(param, source); var output = comstackResults.Output; if (comstackResults.Errors.Count != 0) { CodeDom.ComstackrErrorCollection es = comstackResults.Errors; var edList = new List(); foreach (CodeDom.ComstackrError s in es) edList.Add(new DataRuleLoadExceptionDetails() { Message = s.ErrorText, LineNumber = s.Line }); var rde = new RuleDefinitionException(source, edList.ToArray()); throw rde; } } } } 

Si vous avez d' autres questions, commentaires ou demandes d'échantillons de code, faites-le moi savoir.

La reflection est votre réponse la plus polyvalente. Vous avez trois colonnes de données et vous devez les traiter de différentes manières:

  1. Votre nom de champ La reflection est le moyen d’obtenir la valeur d’un nom de champ codé.

  2. Votre opérateur de comparaison Il devrait y en avoir un nombre limité, donc une déclaration de cas devrait les traiter plus facilement. Surtout que certains d’entre eux (ont un ou plusieurs) sont légèrement plus complexes.

  3. Votre valeur de comparaison. Si ce sont toutes des valeurs droites, c’est facile, bien que vous ayez divisé les entrées multiples. Cependant, vous pouvez également utiliser la reflection si ce sont des noms de champs.

Je prendrais une approche plus comme:

  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.

Cela vous donne la flexibilité d’append plus d’options pour la comparaison. Cela signifie également que vous pouvez coder dans les méthodes de comparaison toute validation de type souhaitée et les rendre aussi complexes que vous le souhaitez. Il y a aussi l’option ici pour que CompareTo soit évalué comme un appel récursif à une autre ligne, ou comme une valeur de champ, qui pourrait être faite comme:

  return IsInComparison(value, EvaluateComparison(rule.CompareTo)) 

Tout dépend des possibilités pour l’avenir ….

Si vous ne disposez que d’une poignée de propriétés et d’opérateurs, la moindre résistance consiste à coder tous les contrôles comme des cas spéciaux comme celui-ci:

 public bool ApplyRules(List 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; } 

Si vous avez beaucoup de propriétés, vous pouvez trouver une approche basée sur la table plus acceptable. Dans ce cas, vous créez un Dictionary statique qui mappe les noms de propriétés aux delegates correspondant, par exemple, à Func .

Si vous ne connaissez pas les noms des propriétés au moment de la compilation ou si vous souhaitez éviter les cas particuliers pour chaque propriété et ne souhaitez pas utiliser l’approche de table, vous pouvez utiliser la reflection pour obtenir des propriétés. Par exemple:

 var value = user.GetType().GetProperty("age").GetValue(user, null); 

Mais comme TargetValue est probablement une ssortingng , vous devrez prendre soin de faire la conversion de type depuis la table des règles si nécessaire.

Qu’en est-il d’une approche orientée type de données avec une méthode d’extention:

 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(ssortingng)) { return ApplySsortingngOperation(role, (ssortingng)property.GetValue(obj, null)); } if (property.PropertyType.GetInterface("IEnumerable",false) != null) { return ApplyListOperation(role, (IEnumerable)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 ApplySsortingngOperation(Role role, ssortingng value) { //... throw new InvalidOperationException("Unknown ComparisonOperator"); } private static bool ApplyListOperation(Role role, IEnumerable value) { var targetValues = role.TargetValue.Split(' '); switch (role.ComparisonOperator) { case "hasAtLeastOne": return value.Any(v => targetValues.Contains(v)); //... } throw new InvalidOperationException("Unknown ComparisonOperator"); } } 

Que vous pouvez évacuer comme ceci:

 var myResults = users.Where(u => roles.All(r => r.Match(u))); 

Bien que la manière la plus évidente de répondre à la question “Comment implémenter un moteur de règles? (En C #)” consiste à exécuter un ensemble donné de règles en séquence, cela est généralement considéré comme une implémentation naïve (cela ne fonctionne pas) 🙂

Il semble que ce soit “assez bon” dans votre cas, car votre problème semble plutôt être “comment exécuter un ensemble de règles en séquence”, et l’arbre lambda / expression (la réponse de Martin) est certainement le plus élégant sont équipés de versions récentes C #.

Cependant, pour les scénarios plus avancés, voici un lien vers l’ algorithme Rete implémenté dans de nombreux systèmes de moteur de règles commerciales, et un autre lien vers NRuler , une implémentation de cet algorithme en C #.

La réponse de Martin était assez bonne. J’ai en fait créé un moteur de règles qui a la même idée que le sien. Et j’ai été surpris que c’est presque la même chose. J’ai inclus une partie de son code pour l’améliorer. Bien que je l’ai fait pour gérer des règles plus complexes.

Vous pouvez regarder Yare.NET

Ou téléchargez-le dans Nuget

Que diriez-vous d’utiliser le moteur de règles de workflow?

Vous pouvez exécuter les règles de workflow Windows sans workflow, voir le blog de Guy Burstein: http://blogs.microsoft.co.il/blogs/bursteg/archive/2006/10/11/RuleExecutionWithoutWorkflow.aspx

et pour créer par programmation vos règles, voir WebLog de Stephen Kaufman

http://blogs.msdn.com/b/skaufman/archive/2006/05/15/programmatically-create-windows-workflow-rules.aspx

J’ai ajouté l’implémentation pour et, ou entre les règles J’ai ajouté la classe RuleExpression qui représente la racine d’un arbre pouvant être feuille. La règle est simple ou peut être et / ou expressions binarys car elles n’ont pas de règle et ont des expressions:

 public class RuleExpression { public NodeOperator NodeOperator { get; set; } public List Expressions { get; set; } public Rule Rule { get; set; } public RuleExpression() { } public RuleExpression(Rule rule) { NodeOperator = NodeOperator.Leaf; Rule = rule; } public RuleExpression(NodeOperator nodeOperator, List expressions, Rule rule) { this.NodeOperator = nodeOperator; this.Expressions = expressions; this.Rule = rule; } } public enum NodeOperator { And, Or, Leaf } 

J’ai une autre classe qui comstack le ruleExpression à un Func:

  public static Func ComstackRuleExpression(RuleExpression ruleExpression) { //Input parameter var genericType = Expression.Parameter(typeof(T)); var binaryExpression = RuleExpressionToOneExpression(ruleExpression, genericType); var lambdaFunc = Expression.Lambda>(binaryExpression, genericType); return lambdaFunc.Comstack(); } private static Expression RuleExpressionToOneExpression(RuleExpression ruleExpression, ParameterExpression genericType) { if (ruleExpression == null) { throw new ArgumentNullException(); } Expression finalExpression; //check if node is leaf if (ruleExpression.NodeOperator == NodeOperator.Leaf) { return RuleToExpression(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(expression.Rule, genericType) : RuleExpressionToOneExpression(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(expression.Rule, genericType) : RuleExpressionToOneExpression(expression, genericType)); }); return finalExpression; } } public static BinaryExpression RuleToExpression(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.ConvertFromSsortingng(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); } } 

J’ai un problème de sensibilité à la casse dans Martin Konicek, alors si vous voulez que la rule.MemberName ne soit pas sensible à la casse, ajoutez simplement

 var tProp = typeof(User).GetProperty(r.MemberName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance).PropertyType;