Dieses Snippet kompiliert die Regeln in schnell ausführbaren Code (unter Verwendung von Ausdrucksbäumen ) und benötigt keine komplizierten switch-Anweisungen:
(Bearbeiten: voll funktionsfähiges Beispiel mit generischer Methode )
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();
}
Sie können dann schreiben:
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));
}
Hier ist die Implementierung von 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);
}
}
Beachten Sie, dass ich 'GreaterThan' anstelle von 'Greater_than' usw. verwendet habe. Dies liegt daran, dass 'GreaterThan' der .NET-Name für den Operator ist und wir daher keine zusätzliche Zuordnung benötigen.
Wenn Sie benutzerdefinierte Namen benötigen, können Sie ein sehr einfaches Wörterbuch erstellen und einfach alle Operatoren übersetzen, bevor Sie die Regeln kompilieren:
var nameMap = new Dictionary<string, string> {
{ "greater_than", "GreaterThan" },
{ "hasAtLeastOne", "Contains" }
};
Der Code verwendet der Einfachheit halber den Typ Benutzer. Sie können User durch einen generischen Typ T ersetzen, um einen generischen Regel-Compiler für alle Objekttypen zu haben. Außerdem sollte der Code Fehler wie unbekannte Operatornamen behandeln.
Beachten Sie, dass das Generieren von Code im laufenden Betrieb bereits vor Einführung der API für Ausdrucksbäume mithilfe von Reflection.Emit möglich war. Die Methode LambdaExpression.Compile () verwendet Reflection.Emit unter dem Deckmantel (Sie können dies mit ILSpy sehen ).
Hier ist ein Code, der so kompiliert wird, wie er ist und den Job erledigt. Verwenden Sie grundsätzlich zwei Wörterbücher, eines mit einer Zuordnung von Operatornamen zu booleschen Funktionen und eines mit einer Zuordnung von den Eigenschaftsnamen des Benutzertyps zu PropertyInfos, die zum Aufrufen des Property Getter (falls öffentlich) verwendet werden. Sie übergeben die Benutzerinstanz und die drei Werte aus Ihrer Tabelle an die statische Apply-Methode.
quelle
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:
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:
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.
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.
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.
Wenn Sie weitere Fragen, Kommentare oder Anfragen für weitere Codebeispiele haben, lassen Sie es mich wissen.
quelle
Reflexion ist Ihre vielseitigste Antwort. Sie haben drei Datenspalten, die unterschiedlich behandelt werden müssen:
Ihr Feldname. Reflexion ist der Weg, um den Wert aus einem codierten Feldnamen zu erhalten.
Ihr Vergleichsoperator. Es sollte eine begrenzte Anzahl von diesen geben, daher sollte eine case-Anweisung sie am einfachsten handhaben. Zumal einige von ihnen (mit einem oder mehreren) etwas komplexer sind.
Ihr Vergleichswert. Wenn dies alles gerade Werte sind, ist dies einfach, obwohl Sie die mehreren Einträge aufteilen müssen. Sie können jedoch auch Reflection verwenden, wenn es sich auch um Feldnamen handelt.
Ich würde eher folgendermaßen vorgehen:
usw. usw.
Sie haben die Flexibilität, weitere Vergleichsoptionen hinzuzufügen. Dies bedeutet auch, dass Sie innerhalb der Vergleichsmethoden jede gewünschte Typüberprüfung codieren und sie so komplex gestalten können, wie Sie möchten. Hier besteht auch die Möglichkeit, dass CompareTo als rekursiver Rückruf an eine andere Zeile oder als Feldwert ausgewertet wird. Dies kann folgendermaßen erfolgen:
Es hängt alles von den Möglichkeiten für die Zukunft ab ....
quelle
Wenn Sie nur eine Handvoll Eigenschaften und Operatoren haben, besteht der Weg des geringsten Widerstands darin, alle Prüfungen als Sonderfälle wie diesen zu codieren:
Wenn Sie viele Eigenschaften haben, ist ein tabellengesteuerter Ansatz möglicherweise schmackhafter. In diesem Fall würden Sie eine Statik erstellen
Dictionary
, die Eigenschaftsnamen Delegaten zuordnet, die beispielsweise übereinstimmenFunc<User, object>
.Wenn Sie die Namen der Eigenschaften zur Kompilierungszeit nicht kennen oder Sonderfälle für jede Eigenschaft vermeiden und den Tabellenansatz nicht verwenden möchten, können Sie Reflection verwenden, um Eigenschaften abzurufen. Beispielsweise:
Da
TargetValue
es sich jedoch wahrscheinlich um eine handeltstring
, müssen Sie bei Bedarf darauf achten, die Typkonvertierung aus der Regeltabelle durchzuführen.quelle
IComparable
wird zum Vergleichen verwendet. Hier sind die Dokumente: IComparable.CompareTo-Methode .Was ist mit einem datentyporientierten Ansatz mit einer Erweiterungsmethode:
Dann können Sie so ausweichen:
quelle
Obwohl die naheliegendste Möglichkeit zur Beantwortung der Frage "Implementieren einer Regelengine? (In C #)" darin besteht, einen bestimmten Regelsatz nacheinander auszuführen, wird dies im Allgemeinen als naive Implementierung angesehen (bedeutet nicht, dass dies nicht funktioniert :-)
Es scheint in Ihrem Fall "gut genug" zu sein, weil Ihr Problem eher darin besteht, "wie man eine Reihe von Regeln nacheinander ausführt", und der Lambda / Ausdrucksbaum (Martins Antwort) ist sicherlich die eleganteste Art in dieser Angelegenheit, wenn Sie sind mit aktuellen C # -Versionen ausgestattet.
Für fortgeschrittenere Szenarien gibt es hier jedoch einen Link zum Rete-Algorithmus , der tatsächlich in vielen kommerziellen Regelenginesystemen implementiert ist, und einen weiteren Link zu NRuler , einer Implementierung dieses Algorithmus in C #.
quelle
Martins Antwort war ziemlich gut. Ich habe tatsächlich eine Regel-Engine erstellt, die die gleiche Idee hat wie seine. Und ich war überrascht, dass es fast das gleiche ist. Ich habe einige seiner Codes eingefügt, um sie etwas zu verbessern. Obwohl ich es geschafft habe, komplexere Regeln zu handhaben.
Sie können sich Yare.NET ansehen
Oder laden Sie es in Nuget herunter
quelle
Wie wäre es mit der Workflow-Regel-Engine?
Sie können Windows-Workflow-Regeln ohne Workflow ausführen. Weitere Informationen finden Sie im Blog von Guy Burstein: http://blogs.microsoft.co.il/blogs/bursteg/archive/2006/10/11/RuleExecutionWithoutWorkflow.aspx
Informationen zum programmgesteuerten Erstellen Ihrer Regeln finden Sie im WebLog von Stephen Kaufman
http://blogs.msdn.com/b/skaufman/archive/2006/05/15/programmatic-create-windows-workflow-rules.aspx
quelle
Ich habe die Implementierung für und oder zwischen Regeln hinzugefügt. Ich habe die Klasse RuleExpression hinzugefügt, die die Wurzel eines Baums darstellt.
Ich habe eine andere Klasse, die den ruleExpression zu einer kompiliert
Func<T, bool>:
quelle