215 votes

Comment implémenter un moteur de règles?

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

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

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

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

Comment pourrais-je faire une boucle par ces règles, et d'appliquer la logique et effectuer les comparaisons etc?

if(user.age > 15)

if(user.username == "some_name")

Depuis la propriété de l'objet comme " âge " ou "nom_utilisateur" est stocké dans le tableau, la comparaison opérateur 'great_than" et "égalité", comment pourrais-je faire cela?

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

421voto

Martin Konicek Points 7999

Cet extrait de code compile le Règlement rapide du code exécutable (à l'aide de l'Expression des arbres) et n'a pas besoin compliqué instructions de commutation:

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

Ici est la mise en œuvre 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. - c'est parce que "GreaterThan' est la .NET nom de l'opérateur, donc nous n'avons pas besoin supplémentaire de la cartographie.

Si vous avez vraiment besoin de noms personnalisés, vous pouvez construire un système très simple dictionnaire et 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 de l'Utilisateur pour des raisons de simplicité. Vous pouvez remplacer l'Utilisateur avec un type générique T à avoir un générique Règle compilateur pour tous les types d'objets.

À noter également: la génération de code à la volée était possible, même avant de l'API des arbres d'Expression a été introduite, à l'aide de la Réflexion.En émettent. La méthode LambdaExpression.Compiler() utilise la Réflexion.Émettre sous les couvertures (vous pouvez voir ce à l'aide de ILSpy).

14voto

Petar Ivanov Points 29530

Voici un code qui compile 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 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"));
    }
}
 

8voto

Schroedingers Cat Points 2383

La réflexion est votre plus polyvalent de la réponse. Vous avez trois colonnes de données, et qu'elles doivent être traitées de différentes façons:

  1. Votre nom de champ. La réflexion est la façon d'obtenir la valeur d'un champ codé nom.

  2. Votre opérateur de comparaison. Il devrait y avoir un nombre limité de ces, si une instruction case doit les gérer plus facilement. D'autant plus que certains d'entre eux ( un ou plusieurs ) est légèrement plus complexe.

  3. Votre valeur de comparaison. Si elles sont toutes droites valeurs, alors c'est facile, même si vous devez diviser les différentes entrées. Cependant, vous pouvez également utiliser la réflexion, si elles sont des noms de champ.

Je voudrais prendre une approche plus du genre:

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

Il vous donne la flexibilité d'ajouter plus d'options à des fins de comparaison. Cela signifie également que vous pouvez code dans les méthodes de Comparaison avec n'importe quel type de validation que vous voulez, et de les rendre aussi complexe que vous le souhaitez. Il y a aussi l'option ici pour la CompareTo être évalué comme un appel récursif de retour pour une autre ligne, ou que la valeur d'un champ, ce qui pourrait être fait ainsi:

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

Tout dépend des possibilités pour l'avenir....

7voto

Rick Sladkey Points 23389

Si vous avez seulement une poignée de propriétés et les opérateurs, le chemin de moindre résistance est juste le code de tous les contrôles comme des cas particuliers 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 un grand nombre de propriétés, vous pouvez trouver une table de l'approche axée sur la plus agréable au goût. Dans ce cas, vous devrez créer un statique Dictionary que les cartes de propriété des noms de délégués correspondant, disons, Func<User, object>.

Si vous ne connaissez pas les noms des propriétés au moment de la compilation, ou vous voulez éviter spécial-cas pour chaque propriété et ne souhaitez 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 depuis TargetValue est probablement un string, vous aurez besoin de prendre soin de faire la conversion de type de la table des règles si nécessaire.

6voto

Yann Olaf Points 415

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

Que vous pouvez réévaluer comme ceci :

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X