Wie implementiere ich eine Regelengine?

205

Ich habe eine DB-Tabelle, in der Folgendes gespeichert ist:

RuleID  objectProperty ComparisonOperator  TargetValue
1       age            'greater_than'             15
2       username       'equal'             'some_name'
3       tags           'hasAtLeastOne'     'some_tag some_tag2'

Angenommen, ich habe eine Sammlung dieser Regeln:

List<Rule> rules = db.GetRules();

Jetzt habe ich auch eine Instanz eines Benutzers:

User user = db.GetUser(....);

Wie würde ich diese Regeln durchlaufen und die Logik anwenden und die Vergleiche usw. durchführen?

if(user.age > 15)

if(user.username == "some_name")

Da die Eigenschaft des Objekts wie 'Alter' oder 'Benutzername' zusammen mit den Vergleichsoperatoren 'great_than' und 'gleich' in der Tabelle gespeichert ist, wie könnte ich dies tun?

C # ist eine statisch typisierte Sprache, daher nicht sicher, wie es weitergehen soll.

Blankman
quelle

Antworten:

390

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 ).

Martin Konicek
quelle
Wo kann ich mehr über Ihre Antwort lesen, um die Klassen / Objekte / etc. Zu lernen? Sie haben in Ihrem Code? Es sind meistens Ausdrucksbäume?
Blankman
4
Alle Klassen stammen aus dem Namespace System.Linq.Expressions und werden mit Factory-Methoden der Expression-Klasse vom Typ "Expression" erstellt. in Ihrer IDE, um auf alle zuzugreifen. Lesen Sie mehr über Ausdrucksbäume hier msdn.microsoft.com/en-us/library/bb397951.aspx
Martin Konicek
3
@Martin Wo finde ich eine Liste qualifizierter .NET-Operatornamen?
Brian Graham
5
@Dark Slipstream Sie finden sie hier msdn.microsoft.com/en-us/library/bb361179.aspx. Nicht alle von ihnen sind boolesche Ausdrücke - verwenden Sie nur die booleschen (wie GreaterThan, NotEqual usw.).
Martin Konicek
1
@BillDaugherty Regel eine einfache Wertklasse mit drei Eigenschaften: MemberName, Operator, TargetValue. Zum Beispiel neue Regel ("Alter", "Größer als", "20").
Martin Konicek
14

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.

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"));
    }
}
Petar Ivanov
quelle
9

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.

Glenn Ferrie
quelle
Sie haben Recht, dass die Engine allgemeiner gestaltet werden kann und die CodeDOM-API definitiv auch eine Option ist. Vielleicht könnten Sie anstelle des nicht sehr klaren "sb.AppendLine" -Codes zeigen, wie genau Sie das CodeDOM aufrufen?
Martin Konicek
8

Reflexion ist Ihre vielseitigste Antwort. Sie haben drei Datenspalten, die unterschiedlich behandelt werden müssen:

  1. Ihr Feldname. Reflexion ist der Weg, um den Wert aus einem codierten Feldnamen zu erhalten.

  2. 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.

  3. 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:

    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)

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:

             return IsInComparison(value, EvaluateComparison(rule.CompareTo))

Es hängt alles von den Möglichkeiten für die Zukunft ab ....

Schröder Cat
quelle
Und Sie können Ihre reflektierten Assemblys / Objekte zwischenspeichern, wodurch Ihr Code noch leistungsfähiger wird.
Mrchief
7

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:

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;
}

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 übereinstimmen Func<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:

var value = user.GetType().GetProperty("age").GetValue(user, null);

Da TargetValuees sich jedoch wahrscheinlich um eine handelt string, müssen Sie bei Bedarf darauf achten, die Typkonvertierung aus der Regeltabelle durchzuführen.

Rick Sladkey
quelle
Was gibt value.CompareTo (limit) zurück? -1 0 oder 1? Hab das b4 noch nicht gesehen!
Blankman
1
@Blankman: Schließen: kleiner als Null, Null oder größer als Null. IComparablewird zum Vergleichen verwendet. Hier sind die Dokumente: IComparable.CompareTo-Methode .
Rick Sladkey
2
Ich verstehe nicht, warum diese Antwort positiv bewertet wurde. Es verstößt gegen viele Gestaltungsprinzipien: "Tell Don't Ask" => Die Regeln sollten jeweils aufgefordert werden , ein Ergebnis zurückzugeben. "Zur Erweiterung öffnen / zur Änderung geschlossen" => Jede neue Regel bedeutet, dass die ApplyRules-Methode geändert werden muss. Außerdem ist der Code auf einen Blick schwer zu verstehen.
Appetere
2
In der Tat ist der Weg des geringsten Widerstands selten der beste Weg. Bitte sehen und bewerten Sie die ausgezeichnete Antwort des Ausdrucksbaums.
Rick Sladkey
6

Was ist mit einem datentyporientierten Ansatz mit einer Erweiterungsmethode:

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");
    }
}

Dann können Sie so ausweichen:

var myResults = users.Where(u => roles.All(r => r.Match(u)));
Yann Olaf
quelle
4

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 #.

Simon Mourier
quelle
3

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

aiapatag
quelle
2

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.

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
}

Ich habe eine andere Klasse, die den ruleExpression zu einer kompiliert 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);
        }

    }
Max.Futerman
quelle