c# regle - Comment implémenter un moteur de règles?





java criteria (10)


La réflexion est votre réponse la plus polyvalente. Vous avez trois colonnes de données, et elles doivent être traitées de différentes manières:

  1. Votre nom de domaine La réflexion 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 le plus facilement. Surtout que certains d'entre eux (a un ou plusieurs de) est légèrement plus complexe.

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

Je voudrais prendre 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 plus de flexibilité pour ajouter plus d'options de comparaison. Cela signifie également que vous pouvez coder dans les méthodes de comparaison toute validation de type que vous pourriez vouloir, 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 valeur de champ, ce qui pourrait être fait comme:

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

Tout dépend des possibilités pour le futur ....

J'ai une table db 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, dis que j'ai une collection de ces règles:

List<Rule> rules = db.GetRules();

Maintenant j'ai une instance d'un utilisateur aussi:

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

Comment pourrais-je boucler 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'objet comme 'age' ou 'user_name' est stockée dans la table, avec l'opérateur de comparaison 'great_than' et 'equal', comment pourrais-je faire cela?

C # est un langage typé statiquement, donc je ne sais pas comment aller de l'avant.




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 qui peut être leaf la règle simple ou peut être et, ou les expressions binaires là car ils n'ont pas de règle et ont des expressions:

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
}

J'ai une autre classe qui compile le ruleExpression à un 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);
        }

    }



Cet extrait compile les règles en code exécutable rapide (à l'aide d' arborescences Expression ) et ne nécessite aucune instruction de commutateur complexe:

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

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();
}

Vous pouvez alors écrire:

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));
}

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, 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);
    }
}

Notez que j'ai utilisé 'GreaterThan' au lieu de 'greater_than' etc. - parce que 'GreaterThan' est le nom .NET pour l'opérateur, donc nous n'avons pas besoin de mapping supplémentaire.

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

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

Notez que le code utilise le type Utilisateur pour plus de simplicité. Vous pouvez remplacer l'utilisateur par un type générique T pour avoir un compilateur de règle générique pour tous les types d'objets.

Notez également: générer du code à la volée était possible avant même que l'API Expression ne soit introduite, en utilisant Reflection.Emit. La méthode LambdaExpression.Compile () utilise Reflection.Emit sous les couvertures (vous pouvez le voir en utilisant ILSpy ).




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

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"));
    }
}



Si vous n'avez qu'une poignée de propriétés et d'opérateurs, le chemin de moindre résistance consiste simplement à coder toutes les vérifications comme des cas spéciaux comme ceci:

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;
}

Si vous avez beaucoup de propriétés, vous pouvez trouver une approche axée sur les tables plus acceptable. Dans ce cas, vous devez créer un Dictionary statique qui mappe les noms de propriété aux délégués correspondant, par exemple, Func<User, object> .

Si vous ne connaissez pas les noms des propriétés au moment de la compilation, ou si vous voulez éviter les cas spéciaux pour chaque propriété et ne voulez pas utiliser l'approche de table, vous pouvez utiliser la réflexion pour obtenir des propriétés. Par exemple:

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

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




Bien que la façon la plus évidente de répondre à la question «Comment implémenter un moteur de règles? (En C #)» est d'exécuter un ensemble de règles en séquence, ceci est généralement considéré comme une implémentation naïve (cela ne veut pas dire que ça ne marche 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'arborescence lambda / expression (la réponse de Martin) est certainement la plus élégante si vous sont équipés de versions récentes en C #.

Cependant, pour des scénarios plus avancés, voici un lien vers l' algorithme Rete qui est en fait 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 plutôt 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 pareil. J'ai inclus une partie de son code pour l'améliorer quelque peu. 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




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 cela beaucoup plus flexible que votre approche actuelle.

Votre approche actuelle semble se concentrer sur une seule entité, «Utilisateur», et vos règles persistantes identifient «nom de propriété», «opérateur» et «valeur». Mon modèle, à la place, stocke le code C # pour un prédicat (Func <T, bool>) dans une colonne "Expression" dans ma base de données. Dans la conception actuelle, en utilisant la génération de code, j'interroge les "règles" de ma base de données et compile un assemblage avec des types "Rule", chacun avec une méthode "Test". Voici la signature de l'interface implémentée pour chaque règle:

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; }
}

L'expression est compilée comme le corps de la méthode "Test" lorsque l'application s'exécute pour la première fois. Comme vous pouvez le voir, les autres colonnes de la table sont également présentées en tant que propriétés de première classe sur la règle afin qu'un développeur ait la possibilité de créer une expérience sur la façon dont l'utilisateur est averti de l'échec ou du succès.

La génération d'un assemblage en mémoire est une occurrence unique au cours de votre application et vous obtenez un gain de performances en n'utilisant pas de réflexion lors de l'évaluation de vos règles. Vos expressions sont vérifiées lors de l'exécution, car l'assemblage 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 base de données
  • parcourir les règles et for-each, en utilisant un StringBuilder et une concaténation de chaînes, écrire le Text représentant une classe qui hérite de IDataRule
  • compiler en utilisant CodeDOM - plus d'infos

C'est en fait assez simple car pour la majorité ce code est implémentations de propriétés et initialisation de valeur dans le constructeur. En outre, le seul autre code est l'expression.
Remarque: il existe une limitation que votre expression doit être .NET 2.0 (pas de lambdas ou d'autres fonctionnalités C # 3.0) en raison d'une limitation dans CodeDOM.

Voici un exemple de code pour cela.

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

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

    /// <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);
    }

Plus de code: Il y avait une demande pour le code lié à la génération de code. J'ai encapsulé la fonctionnalité dans une classe appelée «RulesAssemblyGenerator» que j'ai inclus ci-dessous.

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;
                }
            }
        }
    }

S'il y a d' autres questions ou commentaires ou demandes pour d'autres échantillons de code, faites le moi savoir.




The string can be converted to byte array in few different ways, due to the following fact: .NET supports Unicode, and Unicode standardizes several difference encodings called UTFs. They have different lengths of byte representation but are equivalent in that sense that when a string is encoded, it can be coded back to the string, but if the string is encoded with one UTF and decoded in the assumption of different UTF if can be screwed up.

Also, .NET supports non-Unicode encodings, but they are not valid in general case (will be valid only if a limited sub-set of Unicode code point is used in an actual string, such as ASCII). Internally, .NET supports UTF-16, but for stream representation, UTF-8 is usually used. It is also a standard-de-facto for Internet.

Not surprisingly, serialization of string into an array of byte and deserialization is supported by the class System.Text.Encoding , which is an abstract class; its derived classes support concrete encodings: ASCIIEncoding and four UTFs ( System.Text.UnicodeEncoding supports UTF-16)

Ref this link.

For serialization to an array of bytes using System.Text.Encoding.GetBytes . For the inverse operation use System.Text.Encoding.GetChars . This function returns an array of characters, so to get a string, use a string constructor System.String(char[]) .
Ref this page.

Exemple:

string myString = //... some string

System.Text.Encoding encoding = System.Text.Encoding.UTF8; //or some other, but prefer some UTF is Unicode is used
byte[] bytes = encoding.GetBytes(myString);

//next lines are written in response to a follow-up questions:

myString = new string(encoding.GetChars(bytes));
byte[] bytes = encoding.GetBytes(myString);
myString = new string(encoding.GetChars(bytes));
byte[] bytes = encoding.GetBytes(myString);

//how many times shall I repeat it to show there is a round-trip? :-)




c# dynamic rule-engine