[c#] Comment implémenter un moteur de règles?



Answers

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

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.




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 ....




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




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

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

Que vous pouvez évauler comme ceci:

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



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

    }





Links