J'ai une table de base de données 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<Rule> rules = db.GetRules();
Maintenant, j'ai une instance d'un utilisateur aussi:
User user = db.GetUser(....);
Comment pourrais-je parcourir ces règles, appliquer la logique et effectuer les comparaisons, etc.?
if(user.age > 15)
if(user.username == "some_name")
Etant donné que la propriété de l'objet, telle que 'age' ou 'user_name', est stockée dans la table, avec l'opérateur de comparaison 'great_than' et 'égal', comment pourrais-je le faire?
C # est un langage statiquement typé, donc vous ne savez pas comment aller de l'avant.
Cet extrait compile les règles en un code exécutable rapide (à l'aide de 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. Par conséquent, nous n'avons pas besoin de mappage supplémentaire.
Si vous avez besoin de noms personnalisés, vous pouvez créer un dictionnaire très simple et simplement traduire 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 afin d’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 Arbres d'expression, à l'aide de Reflection.Emit. La méthode LambdaExpression.Compile () utilise Reflection.Emit sous les couvertures (vous pouvez le voir avec ILSpy ).
Voici un code qui compile tel quel et fait le travail. Utilisez essentiellement deux dictionnaires, l’un contenant un mappage des noms d’opérateurs vers des fonctions booléennes, et l’autre contenant une mappe des noms de propriétés du type User à PropertyInfos utilisé pour invoquer l’objet getter de la propriété (s’il est public). Vous transmettez l'instance User et les trois valeurs de votre table à la méthode Apply statique.
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"));
}
}
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 constaterez qu'il est beaucoup plus flexible que votre approche actuelle.
Votre approche actuelle semble être axée sur une seule entité, "Utilisateur", et vos règles persistantes identifient "nom_propriété", "opérateur" et "valeur". Mon modèle, à la place, stocke 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 compile un assembly avec les types "Règle", chacun avec une méthode "Test". Voici la signature de l'interface implémentée à 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; }
}
"Expression" est compilé en tant que corps de la méthode "Test" lors de la première exécution de l'application. Comme vous pouvez le constater, 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, ce qui permet au développeur de disposer d'une plus grande flexibilité pour créer une expérience indiquant comment 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 performance en évitant d'avoir à utiliser la réflexion lors de l'évaluation de vos règles. Vos expressions sont vérifiées au moment de l'exécution car l'assembly ne 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:
Ceci est en fait assez simple car pour la majorité, ce code est une implémentation de propriété et une initialisation de valeur dans le constructeur. En plus de cela, le seul autre code est l'expression.
REMARQUE: il existe une limitation selon laquelle votre expression doit être .NET 2.0 (pas de lambdas ni d'autres fonctionnalités de 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à 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 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);
}
MORE 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 incluse 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 d'échantillons de code supplémentaires, faites-le moi savoir.
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:
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é, de sorte qu'une déclaration de cas devrait les traiter plus facilement. D'autant que certains d'entre eux (en possède un ou plusieurs) est légèrement plus complexe.
Votre valeur de comparaison. Si toutes ces valeurs sont droites, cela est facile, bien que vous ayez à diviser les entrées multiples. Cependant, vous pouvez également utiliser la réflexion si ce sont aussi des noms de champs.
Je prendrais une approche plus semblable à:
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 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 souhaitée et les rendre aussi complexes que vous le souhaitez. Il existe également l'option ici pour que CompareTo soit évalué en tant qu'appel récursif vers une autre ligne, ou en tant que valeur de champ, ce qui pourrait se faire de la manière suivante:
return IsInComparison(value, EvaluateComparison(rule.CompareTo))
Tout dépend des possibilités pour l'avenir ....
Si vous ne possédez qu'une poignée de propriétés et d'opérateurs, la solution la moins résistante consiste à coder toutes les vérifications en tant que cas particuliers comme celui-ci:
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 basée sur les tables plus acceptable. Dans ce cas, vous créerez un Dictionary
statique qui mappera les noms de propriété sur les délégués correspondants, 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:
var value = user.GetType().GetProperty("age").GetValue(user, null);
Mais puisque TargetValue
est probablement un string
, vous devrez prendre soin de faire la conversion de type à partir de 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'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");
}
}
Alors vous pouvez évaluer comme ceci:
var myResults = users.Where(u => roles.All(r => r.Match(u)));
Bien que le moyen le plus évident de répondre à la question "Comment implémenter un moteur de règles? (En C #)" consiste à exécuter un ensemble de règles donné en séquence, il s'agit en général d'une implémentation naïve (ne signifie pas que cela ne fonctionne pas :-)
Il semble que c’est "assez bon" 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’arbre lambda/expression (la réponse de Martin) est certainement la manière la plus élégante sont équipés de versions C # récentes.
Cependant, pour des scénarios plus avancés, voici un lien vers Rete Algorithm 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 celui-ci. 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 ce soit presque pareil. J'ai inclus une partie de son code pour l'améliorer quelque peu. Bien que je l'aie fait pour gérer des règles plus complexes.
Vous pouvez regarder Yare.NET
Ou téléchargez-le dans Nuget
Pourquoi ne pas 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 programmation, voir WebLog de Stephen Kaufman
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 traité comme une simple règle ou pouvant être et, ou des expressions binaires car elles n'ont ni règle ni expression:
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 la règleExpression à 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);
}
}
J'ai un problème de casse dans la réponse de Martin Konicek, donc si vous voulez le rule.MemberName
pour ne pas être sensible à la casse, ajoutez simplement
var tProp = typeof(User).GetProperty(r.MemberName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance).PropertyType;