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:
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 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:
- Chargez vos règles depuis la base de données
- itérer sur les règles et pour chacun, en utilisant un StringBuilder et une concaténation de chaînes, écrivez le texte représentant une classe qui hérite d'IDataRule
- compiler en utilisant CodeDOM - plus d'informations
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.
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 de 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, commentaires ou demandes d'échantillons de code supplémentaires, faites-le moi savoir.