Jak zaimplementować silnik reguł?

205

Mam tabelę db, która przechowuje następujące elementy:

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

Teraz powiedz, że mam zbiór tych reguł:

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

Teraz mam również instancję użytkownika:

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

Jak miałbym przechodzić przez te reguły, stosować logikę i przeprowadzać porównania itp.?

if(user.age > 15)

if(user.username == "some_name")

Ponieważ właściwość obiektu, taka jak „wiek” lub „nazwa_użytkownika”, jest przechowywana w tabeli, wraz z operatorem porównującym „great_than” i „równy”, jak mogę to zrobić?

C # jest językiem wpisywanym statycznie, więc nie jestem pewien, jak iść naprzód.

Blankman
źródło

Odpowiedzi:

390

Ten fragment kodu kompiluje reguły w szybki kod wykonywalny (przy użyciu drzew wyrażeń ) i nie wymaga żadnych skomplikowanych instrukcji przełączania:

(Edycja: pełny przykład roboczy z metodą ogólną )

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

Następnie możesz napisać:

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

Oto implementacja 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);
    }
}

Zauważ, że użyłem „GreaterThan” zamiast „Greater_than” itd. - to dlatego, że „GreaterThan” to nazwa .NET dla operatora, dlatego nie potrzebujemy żadnego dodatkowego mapowania.

Jeśli potrzebujesz niestandardowych nazw, możesz zbudować bardzo prosty słownik i po prostu przetłumaczyć wszystkie operatory przed skompilowaniem reguł:

var nameMap = new Dictionary<string, string> {
    { "greater_than", "GreaterThan" },
    { "hasAtLeastOne", "Contains" }
};

Dla uproszczenia kod używa typu Użytkownik. Możesz zastąpić użytkownika ogólnym typem T, aby mieć ogólny kompilator reguł dla dowolnych typów obiektów. Ponadto kod powinien obsługiwać błędy, takie jak nieznana nazwa operatora.

Zauważ, że generowanie kodu w locie było możliwe nawet przed wprowadzeniem interfejsu API drzew wyrażeń za pomocą Reflection.Emit. Metoda LambdaExpression.Compile () używa Reflection.Emit pod okładkami (można to zobaczyć za pomocą ILSpy ).

Martin Konicek
źródło
Gdzie mogę przeczytać więcej o twojej odpowiedzi, aby dowiedzieć się o klasach / obiektach / itp. masz w swoim kodzie? To głównie drzewa ekspresyjne?
Blankman,
4
Wszystkie klasy pochodzą z przestrzeni nazw System.Linq.Expressions i wszystkie są tworzone przy użyciu metod fabrycznych klasy Expression - wpisz „Expression”. w swoim IDE, aby uzyskać dostęp do wszystkich. Przeczytaj więcej o drzewach wyrażeń tutaj msdn.microsoft.com/en-us/library/bb397951.aspx
Martin Konicek
3
@Martin, gdzie mogę znaleźć listę kwalifikowanych nazw operatorów .NET?
Brian Graham,
5
@Dark Slipstream Można je znaleźć tutaj msdn.microsoft.com/en-us/library/bb361179.aspx. Nie wszystkie z nich są wyrażeniami boolowskimi - używaj tylko wyrażeń boolowskich (takich jak GreaterThan, NotEqual itp.).
Martin Konicek,
1
@BillDaugherty Rządz prostą klasę wartości z trzema właściwościami: MemberName, Operator, TargetValue. Na przykład nowa reguła („Wiek”, „GreaterThan”, „20”).
Martin Konicek,
14

Oto kod, który kompiluje się tak, jak jest i wykonuje zadanie. Zasadniczo używaj dwóch słowników, jeden zawierający odwzorowanie nazw operatorów na funkcje boolowskie, a drugi zawierający odwzorowanie nazw właściwości typu User na PropertyInfos używanych do wywoływania getterera (jeśli jest publiczny). Przekazujesz instancję użytkownika i trzy wartości ze swojej tabeli do statycznej metody Apply.

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 Iwanow
źródło
9

Zbudowałem silnik reguł, który ma inne podejście niż przedstawione w pytaniu, ale myślę, że okaże się, że będzie on znacznie bardziej elastyczny niż obecne podejście.

Twoje obecne podejście wydaje się koncentrować na pojedynczej jednostce, „Użytkownik”, a twoje trwałe reguły identyfikują „nazwa właściwości”, „operator” i „wartość”. Zamiast tego mój wzór przechowuje kod C # dla predykatu (Func <T, bool>) w kolumnie „Wyrażenie” w mojej bazie danych. W obecnym projekcie, używając generowania kodu, odpytuję „reguły” z mojej bazy danych i kompiluję zestaw z typami „Reguł”, każdy z wykorzystaniem metody „Test”. Oto podpis interfejsu implementowanego dla każdej reguły:

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

„Wyrażenie” jest kompilowane jako treść metody „Test” podczas pierwszego uruchomienia aplikacji. Jak widać, inne kolumny w tabeli są również wyświetlane jako pierwszorzędne właściwości reguły, dzięki czemu programista ma swobodę tworzenia sposobu, w jaki użytkownik zostanie powiadomiony o niepowodzeniu lub sukcesie.

Generowanie zestawu w pamięci jest jednorazowym wystąpieniem podczas aplikacji i zyskujesz na wydajności, ponieważ nie musisz używać refleksji podczas oceny reguł. Wyrażenia są sprawdzane w czasie wykonywania, ponieważ zestaw nie generuje się poprawnie, jeśli nazwa właściwości jest błędnie napisana itp.

Mechanizmy tworzenia zestawu w pamięci są następujące:

  • Załaduj swoje reguły z bazy danych
  • iteruj po regułach i dla każdego z nich, używając StringBuilder i jakiejś konkatenacji ciągów napisz tekst reprezentujący klasę, która dziedziczy z IDataRule
  • skompiluj używając CodeDOM - więcej informacji

Jest to w rzeczywistości dość proste, ponieważ dla większości ten kod jest implementacją właściwości i inicjowaniem wartości w konstruktorze. Poza tym jedynym innym kodem jest Expression.
UWAGA: istnieje ograniczenie, że twoje wyrażenie musi być .NET 2.0 (bez lambdas lub innych funkcji C # 3.0) z powodu ograniczenia w CodeDOM.

Oto przykładowy kod do tego.

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

Poza tym stworzyłem klasę o nazwie „DataRuleCollection”, która zaimplementowała ICollection>. Umożliwiło mi to utworzenie funkcji „TestAll” i modułu indeksującego do wykonywania określonej reguły według nazwy. Oto implementacje tych dwóch metod.

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

WIĘCEJ KODU: Zgłoszono żądanie kodu związanego z generowaniem kodu. Zamknąłem funkcjonalność w klasie o nazwie „RulesAssemblyGenerator”, którą zamieściłem poniżej.

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

Jeśli są jakieś inne pytania lub komentarze i prośby o dalsze przykłady kodu, daj mi znać.

Glenn Ferrie
źródło
Masz rację, że silnik może być bardziej ogólny, a API CodeDOM jest zdecydowanie opcją. Może zamiast niezbyt jasnego kodu „sb.AppendLine” możesz pokazać, jak dokładnie wywołujesz CodeDOM?
Martin Konicek,
8

Refleksja to twoja najbardziej wszechstronna odpowiedź. Masz trzy kolumny danych i należy je traktować na różne sposoby:

  1. Twoja nazwa pola. Odbicie to sposób na uzyskanie wartości z zakodowanej nazwy pola.

  2. Twój operator porównania. Powinna być ich ograniczona liczba, więc zestawienie przypadków powinno z łatwością sobie z nimi poradzić. Zwłaszcza, że ​​niektóre z nich (ma jeden lub więcej) są nieco bardziej złożone.

  3. Twoja wartość porównawcza. Jeśli wszystkie są prostymi wartościami, jest to łatwe, chociaż będziesz musiał podzielić wiele wpisów w górę. Możesz także użyć refleksji, jeśli są to również nazwy pól.

Przyjąłbym bardziej podejście:

    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)

itd itd.

Daje to elastyczność dodawania większej liczby opcji porównania. Oznacza to również, że możesz zakodować w metodach porównania dowolny typ walidacji, jaki chcesz, i uczynić je tak złożonymi, jak chcesz. Istnieje również opcja, aby narzędzie CompareTo było oceniane jako rekurencyjne wywołanie zwrotne do innej linii lub jako wartość pola, co można wykonać w następujący sposób:

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

Wszystko zależy od możliwości na przyszłość ....

Schroedingers Cat
źródło
Możesz buforować swoje odbite zespoły / obiekty, dzięki czemu Twój kod będzie jeszcze bardziej wydajny.
Mrchief
7

Jeśli masz tylko kilka właściwości i operatorów, ścieżką najmniejszego oporu jest po prostu zakodowanie wszystkich kontroli jako specjalnych przypadków takich jak ten:

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

Jeśli masz wiele właściwości, może być łatwiejsze podejście oparte na tabeli. W takim przypadku należy utworzyć statyczną Dictionary, która mapuje nazwy właściwości delegatom pasującymi powiedzmy Func<User, object>.

Jeśli nie znasz nazw właściwości w czasie kompilacji lub chcesz uniknąć specjalnych przypadków dla każdej właściwości i nie chcesz używać podejścia tabelowego, możesz użyć refleksji, aby uzyskać właściwości. Na przykład:

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

Ale ponieważ TargetValueprawdopodobnie jest to string, musisz koniecznie wykonać konwersję typu z tabeli reguł, jeśli to konieczne.

Rick Sladkey
źródło
co zwraca wartość value.CompareTo (limit)? -1 0 czy 1? Nie widziałem tego b4!
Blankman
1
@Blankman: Zamknij: mniej niż zero, zero lub więcej niż zero. IComparablesłuży do porównywania rzeczy. Oto dokumentacja: IComparable.CompareTo Method .
Rick Sladkey
2
Nie rozumiem, dlaczego ta odpowiedź została poddana pod głosowanie. Narusza to wiele zasad projektowania: „Powiedz, nie pytaj” => zasady powinny zostać poproszone o zwrócenie wyniku. „Otwarte na rozszerzenie / zamknięte na modyfikację” => każda nowa reguła oznacza, że ​​metoda ApplyRules wymaga modyfikacji. Ponadto kod jest trudny do zrozumienia na pierwszy rzut oka.
Appetere,
2
Rzeczywiście, ścieżka najmniejszego oporu rzadko jest najlepszą ścieżką. Proszę zobaczyć i głosować na doskonałą odpowiedź drzewa wyrażeń.
Rick Sladkey
6

Co z podejściem zorientowanym na typ danych z metodą rozszerzenia:

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

Następnie możesz ewaluować w ten sposób:

var myResults = users.Where(u => roles.All(r => r.Match(u)));
Yann Olaf
źródło
4

Chociaż najbardziej oczywistym sposobem odpowiedzi na pytanie „Jak zaimplementować silnik reguł? (W języku C #)” jest wykonanie danego zestawu reguł w sekwencji, ogólnie uważa się to za naiwną implementację (nie oznacza to, że nie działa :-)

Wydaje się, że w twoim przypadku jest „wystarczająco dobry”, ponieważ twoim problemem jest raczej „jak uruchomić zestaw reguł w sekwencji”, a drzewo lambda / wyrażenie (odpowiedź Martina) jest z pewnością najbardziej eleganckim sposobem, jeśli są wyposażone w najnowsze wersje C #.

Jednak w przypadku bardziej zaawansowanych scenariuszy znajduje się łącze do algorytmu Rete, który jest faktycznie zaimplementowany w wielu komercyjnych systemach silnika reguł, oraz inne łącze do NRuler , implementacji tego algorytmu w języku C #.

Simon Mourier
źródło
3

Odpowiedź Martina była całkiem dobra. Stworzyłem silnik reguł, który ma taki sam pomysł jak jego. Byłem zaskoczony, że jest prawie tak samo. Dołączyłem trochę jego kodu, aby go nieco poprawić. Chociaż zrobiłem to, aby obsługiwać bardziej złożone reguły.

Możesz spojrzeć na Yare.NET

Lub pobierz go w Nuget

aiapatag
źródło
2

Dodałem implementację dla i, lub pomiędzy regułami, dodałem klasę RuleExpression, która reprezentuje korzeń drzewa, które może być liściem, jest prostą regułą lub może być i, lub wyrażenia binarne, ponieważ nie mają reguły i mają wyrażenia:

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
}

Mam inną klasę, która kompiluje regułę Wyrażenie do jednej 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.Ferman
źródło