Ich habe eine Regel-Engine erstellt, die einen anderen Ansatz verfolgt als in Ihrer Frage beschrieben, aber ich denke, Sie werden feststellen, dass sie viel flexibler ist als Ihr aktueller Ansatz.
Ihr aktueller Ansatz scheint sich auf eine einzelne Entität, "Benutzer", zu konzentrieren, und Ihre persistenten Regeln identifizieren "Eigenschaftsname", "Operator" und "Wert". Mein Muster speichert stattdessen den C # -Code für ein Prädikat (Func <T, bool>) in einer Spalte "Ausdruck" in meiner Datenbank. Im aktuellen Design frage ich mithilfe der Codegenerierung die "Regeln" aus meiner Datenbank ab und kompiliere eine Assembly mit "Regel" -Typen, jeweils mit einer "Test" -Methode. Hier ist die Signatur für die Schnittstelle, die in jeder Regel implementiert ist:
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; }
}
Der "Ausdruck" wird bei der ersten Ausführung der Anwendung als Hauptteil der "Test" -Methode kompiliert. Wie Sie sehen können, werden die anderen Spalten in der Tabelle ebenfalls als erstklassige Eigenschaften in der Regel angezeigt, sodass ein Entwickler flexibel eine Erfahrung erstellen kann, wie der Benutzer über Fehler oder Erfolg benachrichtigt wird.
Das Generieren einer In-Memory-Assembly ist ein einmaliges Ereignis während Ihrer Anwendung, und Sie erzielen einen Leistungsgewinn, wenn Sie bei der Bewertung Ihrer Regeln keine Reflexion verwenden müssen. Ihre Ausdrücke werden zur Laufzeit überprüft, da die Assembly nicht korrekt generiert wird, wenn ein Eigenschaftsname falsch geschrieben ist usw.
Die Mechanismen zum Erstellen einer In-Memory-Assembly sind wie folgt:
- Laden Sie Ihre Regeln aus der DB
- Durchlaufen Sie die Regeln und for-each. Schreiben Sie mit einem StringBuilder und einer gewissen String-Verkettung den Text, der eine Klasse darstellt, die von IDataRule erbt
- Kompilieren mit CodeDOM - weitere Informationen
Dies ist eigentlich recht einfach, da es sich bei diesem Code größtenteils um Eigenschaftsimplementierungen und Wertinitialisierung im Konstruktor handelt. Außerdem ist der einzige andere Code der Ausdruck.
HINWEIS: Aufgrund einer Einschränkung in CodeDOM gibt es eine Einschränkung, dass Ihr Ausdruck .NET 2.0 sein muss (keine Lambdas oder andere C # 3.0-Funktionen).
Hier ist ein Beispielcode dafür.
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
Darüber hinaus habe ich eine Klasse namens "DataRuleCollection" erstellt, die ICollection> implementiert hat. Dadurch konnte ich eine "TestAll" -Funktion und einen Indexer zum Ausführen einer bestimmten Regel nach Namen erstellen. Hier sind die Implementierungen für diese beiden Methoden.
/// <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);
}
MEHR CODE: Es gab eine Anfrage für den Code im Zusammenhang mit der Codegenerierung. Ich habe die Funktionalität in einer Klasse namens 'RulesAssemblyGenerator' gekapselt, die ich unten aufgeführt habe.
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;
}
}
}
}
Wenn Sie weitere Fragen, Kommentare oder Anfragen für weitere Codebeispiele haben, lassen Sie es mich wissen.