Cet extrait compile les règles en code exécutable rapide (en utilisant arbres d'expression ) et ne nécessite aucune instruction de commutateur compliquée:
(Edit: exemple de travail complet avec 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. - c'est parce que «GreaterThan» est le nom .NET de l'opérateur, nous n'avons donc pas besoin de mappage supplémentaire.
Si vous avez besoin de noms personnalisés, vous pouvez créer un dictionnaire très simple et traduire simplement tous les opérateurs avant de compiler les règles:
var nameMap = new Dictionary<string, string> {
{ "greater_than", "GreaterThan" },
{ "hasAtLeastOne", "Contains" }
};
Le code utilise le type Utilisateur pour plus de simplicité. Vous pouvez remplacer User par un type générique T pour avoir un compilateur de règles générique pour tous les types d'objets. En outre, le code doit gérer les erreurs, comme le nom d'opérateur inconnu.
Notez que la génération de code à la volée était possible avant même l'introduction de l'API Expression trees, à l'aide de Reflection.Emit. La méthode LambdaExpression.Compile () utilise Reflection.Emit sous les couvertures (vous pouvez le voir en utilisant ILSpy ).
Voici du code qui se compile tel quel et fait le travail. Utilisez essentiellement deux dictionnaires, l'un contenant un mappage des noms d'opérateur aux fonctions booléennes, et l'autre contenant un mappage des noms de propriété du type User à PropertyInfos utilisé pour appeler le getter de propriété (s'il est public). Vous passez l'instance d'utilisateur et les trois valeurs de votre table à la méthode Apply statique.
la source
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 le trouverez beaucoup plus flexible que votre approche actuelle.
Votre approche actuelle semble se concentrer sur une seule entité, "User", et vos règles persistantes identifient "propertyname", "operator" et "value". Mon modèle stocke à la place le code C # d'un prédicat (Func <T, bool>) dans une colonne "Expression" de 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 je compile un assemblage avec des types "Rule", chacun avec une méthode "Test". Voici la signature de l'interface qui est implémentée pour chaque règle:
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 du tableau 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 informé 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 performances en évitant d'avoir à utiliser la réflexion lors de l'évaluation de vos règles. Vos expressions sont vérifiées lors de l'exécution car l'assembly ne se générera pas 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:
C'est en fait assez simple car pour la plupart ce code est l'implémentation de propriétés et l'initialisation de valeur dans le constructeur. En plus de cela, 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.
Au-delà de cela, j'ai créé une classe que j'ai appelée "DataRuleCollection", qui implémentait ICollection>. Cela m'a permis de créer une capacité "TestAll" et un indexeur pour exécuter une règle spécifique par nom. Voici les implémentations de ces deux méthodes.
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.
S'il y a d' autres questions, commentaires ou demandes d'échantillons de code supplémentaires, faites-le moi savoir.
la source
La réflexion est votre réponse la plus polyvalente. Vous disposez de trois colonnes de données et elles doivent être traitées de différentes manières:
Votre nom de champ. La réflexion est le moyen d'obtenir la valeur d'un nom de champ codé.
Votre opérateur de comparaison. Il devrait y en avoir un nombre limité, donc une déclaration de cas devrait les gérer plus facilement. D'autant plus que certains d'entre eux (en possède un ou plusieurs) sont légèrement plus complexes.
Votre valeur de comparaison. Si ce sont toutes des valeurs droites, cela est facile, même si vous devrez diviser les multiples entrées. Cependant, vous pouvez également utiliser la réflexion s'il s'agit également de noms de champ.
Je prendrais une approche plus comme:
etc.
Il vous donne la possibilité d'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 souhaitez et les rendre aussi complexes que vous le souhaitez. Il y a aussi l'option ici pour que CompareTo soit évalué comme un rappel récursif vers une autre ligne, ou comme une valeur de champ, ce qui pourrait être fait comme:
Tout dépend des possibilités d'avenir ...
la source
Si vous ne disposez que d'une poignée de propriétés et d'opérateurs, le chemin de moindre résistance consiste à coder tous les contrôles dans des cas spéciaux comme celui-ci:
Si vous avez beaucoup de propriétés, vous pouvez trouver une approche basée sur une table plus acceptable. Dans ce cas, vous devez créer une statique
Dictionary
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 souhaitez éviter les cas spéciaux pour chaque propriété et ne souhaitez pas utiliser l'approche de table, vous pouvez utiliser la réflexion pour obtenir les propriétés. Par exemple:
Mais comme
TargetValue
c'est probablement unstring
, vous devrez prendre soin de faire une conversion de type à partir de la table de règles si nécessaire.la source
IComparable
est utilisé pour comparer les choses. Voici les documents: IComparable.CompareTo, méthode .Qu'en est-il d'une approche orientée type de données avec une méthode d'extension:
Que vous pouvez évaluer comme ceci:
la source
Bien que la façon la plus évidente de répondre à la question "Comment implémenter un moteur de règles? (En C #)" soit d'exécuter un ensemble de règles donné en séquence, cela est généralement considéré comme une implémentation naïve (cela ne signifie pas que cela ne fonctionne pas). :-)
Il semble que ce soit "assez bien" dans votre cas parce que 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 manière la plus élégante en la matière si vous sont équipés de versions C # récentes.
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 commerciaux, et un autre lien vers NRuler , une implémentation de cet algorithme en C #.
la source
La réponse de Martin était assez bonne. J'ai fait un moteur de règles qui a la même idée que le sien. Et j'ai été surpris que ce soit presque la même chose. J'ai inclus une partie de son code pour l'améliorer quelque peu. Même si je l'ai fait pour gérer des règles plus complexes.
Vous pouvez regarder Yare.NET
Ou téléchargez-le dans Nuget
la source
Que diriez-vous d'utiliser le moteur de règles de workflow?
Vous pouvez exécuter des 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 vos règles par programme, consultez le WebLog de Stephen Kaufman
http://blogs.msdn.com/b/skaufman/archive/2006/05/15/programmatically-create-windows-workflow-rules.aspx
la source
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 la feuille est une règle simple ou peut être et, ou des expressions binaires car elles n'ont pas de règle et ont des expressions:
J'ai une autre classe qui compile la règleExpression en une
Func<T, bool>:
la source