Как реализовать механизм правил?

205

У меня есть таблица БД, в которой хранится следующее:

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

Теперь скажите, что у меня есть коллекция этих правил:

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

Теперь у меня есть экземпляр пользователя также:

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

Как бы я повторил эти правила, применил логику, провел сравнения и т. Д.?

if(user.age > 15)

if(user.username == "some_name")

Поскольку в объекте хранятся такие свойства объекта, как age или user_name, а также оператор сравнения great_than и equal, как я могу это сделать?

C # - статически типизированный язык, поэтому не уверен, как двигаться дальше.

Бланкмэн
источник

Ответы:

391

Этот фрагмент компилирует Правила в быстрый исполняемый код (используя деревья выражений ) и не требует каких-либо сложных операторов switch:

(Изменить: полный рабочий пример с универсальным методом )

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

Вы можете написать:

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

Вот реализация 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);
    }
}

Обратите внимание, что я использовал «GreaterThan» вместо «great_than» и т. Д. - это потому, что «GreaterThan» - это имя .NET для оператора, поэтому нам не нужно никакого дополнительного сопоставления.

Если вам нужны собственные имена, вы можете создать очень простой словарь и просто перевести все операторы перед составлением правил:

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

Код использует тип User для простоты. Вы можете заменить пользователя универсальным типом T, чтобы иметь универсальный компилятор правил для любых типов объектов. Кроме того, код должен обрабатывать ошибки, такие как неизвестное имя оператора.

Обратите внимание, что генерация кода на лету была возможна еще до того, как был представлен API деревьев выражений, с использованием Reflection.Emit. Метод LambdaExpression.Compile () использует Reflection.Emit под обложками (вы можете увидеть это с помощью ILSpy ).

Мартин Коничек
источник
Где я могу прочитать больше о вашем ответе, чтобы узнать классы / объекты / и т.д. у вас в вашем коде есть? Это в основном деревья выражения?
Бланкмен
4
Все классы происходят из пространства имен System.Linq.Expressions, и все они создаются с использованием фабричных методов класса Expression - типа «Выражение». в вашей IDE, чтобы получить доступ ко всем из них. Подробнее о деревьях выражений можно прочитать
Мартин Коничек,
3
@ Мартин, где я могу найти список подходящих имен операторов .NET?
Брайан Грэм
5
@ Dark Slipstream Вы можете найти их здесь msdn.microsoft.com/en-us/library/bb361179.aspx. Не все из них являются логическими выражениями - используйте только логические выражения (такие как GreaterThan, NotEqual и т. Д.).
Мартин Коничек
1
@BillDaugherty Правило простого класса значений с тремя свойствами: MemberName, Operator, TargetValue. Например, новое правило («Age», «GreaterThan», «20»).
Мартин Коничек
14

Вот код, который компилируется как есть и выполняет свою работу. В основном используются два словаря, один из которых содержит сопоставление имен операторов с логическими функциями, а другой содержит сопоставление имен свойств типа User с PropertyInfos, используемым для вызова метода получения свойств (если он общедоступен). Вы передаете экземпляр User и три значения из своей таблицы статическому методу 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"));
    }
}
Петар иванов
источник
9

Я создал механизм правил, который использует иной подход, чем тот, который вы описали в своем вопросе, но я думаю, вы найдете его гораздо более гибким, чем ваш нынешний подход.

Ваш текущий подход, кажется, сфокусирован на одной сущности «Пользователь», а ваши постоянные правила определяют «имя свойства», «оператор» и «значение». Вместо этого мой шаблон хранит код C # для предиката (Func <T, bool>) в столбце «Выражение» в моей базе данных. В текущем проекте, используя генерацию кода, я запрашиваю «правила» из моей базы данных и компилирую сборку с типами «Правило», каждый с методом «Test». Вот подпись для интерфейса, который реализуется каждым правилом:

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

«Выражение» компилируется как тело метода «Test» при первом запуске приложения. Как вы можете видеть, другие столбцы в таблице также отображаются как первоклассные свойства правила, так что разработчик может гибко настраивать способ получения пользователем уведомления о сбое или успехе.

Генерация сборки в памяти происходит один раз во время вашего приложения, и вы получаете выигрыш в производительности, не используя рефлексию при оценке ваших правил. Ваши выражения проверяются во время выполнения, поскольку сборка не будет генерироваться правильно, если имя свойства введено с ошибкой и т. Д.

Механика создания сборки в памяти заключается в следующем:

  • Загрузите ваши правила из БД
  • перебираем правила и for-each, используя StringBuilder и некоторую конкатенацию строк, пишем Text, представляющий класс, который наследуется от IDataRule
  • компилировать с использованием CodeDOM - больше информации

На самом деле это довольно просто, потому что для большинства этот код является реализацией свойств и инициализацией значения в конструкторе. Кроме того, единственным другим кодом является выражение.
ПРИМЕЧАНИЕ: есть ограничение, что ваше выражение должно быть .NET 2.0 (без лямбда-выражений или других функций C # 3.0) из-за ограничения в CodeDOM.

Вот пример кода для этого.

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

Помимо этого я создал класс под названием «DataRuleCollection», который реализовал ICollection>. Это позволило мне создать возможность «TestAll» и индексатор для выполнения определенного правила по имени. Вот реализации для этих двух методов.

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

БОЛЬШЕ КОДА: Был запрос на код, связанный с генерацией кода. Я инкапсулировал функциональность в классе под названием «RulesAssemblyGenerator», который я включил ниже.

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

Если есть какие-либо другие вопросы или комментарии или запросы на дополнительные примеры кода, дайте мне знать.

Гленн Ферри
источник
Вы правы в том, что движок можно сделать более общим, и API CodeDOM, безусловно, также является опцией. Может быть, вместо кода "sb.AppendLine", который не очень понятен, вы можете показать, как именно вы вызываете CodeDOM?
Мартин Коничек
8

Отражение - ваш самый универсальный ответ. У вас есть три столбца данных, и они должны обрабатываться по-разному:

  1. Ваше имя поля. Отражение - это способ получить значение из кодированного имени поля.

  2. Ваш оператор сравнения. Их должно быть ограниченное количество, поэтому оператор case должен обрабатывать их наиболее легко. Тем более, что некоторые из них (имеет один или несколько из них) немного сложнее.

  3. Ваше значение сравнения. Если это все прямые значения, то это легко, хотя вам придется разделить несколько записей. Однако вы также можете использовать отражение, если они также являются именами полей.

Я бы выбрал такой подход, как:

    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)

и т. д.

Это дает вам гибкость для добавления дополнительных параметров для сравнения. Это также означает, что вы можете кодировать в методах сравнения любой тип проверки, который вам может потребоваться, и делать их настолько сложными, насколько вам нужно. Здесь также есть опция для сравнения CompareTo как рекурсивного обратного вызова к другой строке или значения поля, что можно сделать следующим образом:

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

Все зависит от возможностей на будущее ....

Кот шредингеров
источник
И вы можете кэшировать ваши отраженные сборки / объекты, которые сделают ваш код еще более производительным.
Mrchief
7

Если у вас есть только несколько свойств и операторов, путь наименьшего сопротивления состоит в том, чтобы просто закодировать все проверки как особые случаи, подобные этому:

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

Если у вас много свойств, вы можете найти подход, основанный на использовании таблиц, более приемлемым. В этом случае вы должны создать статический Dictionaryобъект, который отображает имена свойств на делегатов, соответствующих, скажем,Func<User, object> .

Если вы не знаете имен свойств во время компиляции или хотите избежать особых случаев для каждого свойства и не хотите использовать табличный подход, вы можете использовать отражение для получения свойств. Например:

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

Но с тех пор TargetValue это, вероятно string, вам нужно позаботиться о преобразовании типов из таблицы правил, если это необходимо.

Рик Сладки
источник
что возвращает value.CompareTo (limit)? -1 0 или 1? Не видел этого b4!
Blankman
1
@Blankman: Close: меньше нуля, ноль или больше нуля. IComparableиспользуется для сравнения вещей. Вот документы: Метод IComparable.CompareTo .
Рик Сладки
2
Я не понимаю, почему за этот ответ проголосовали. Это нарушает многие принципы разработки: «Скажи, не спрашивай» => правила должны запрашиваться для возврата результата. «Открыто для расширения / закрыто для модификации» => любое новое правило означает, что метод ApplyRules нуждается в модификации. Плюс код сложно понять с первого взгляда.
Аппетит
2
Действительно, путь наименьшего сопротивления редко является лучшим путем. Пожалуйста, посмотрите и подтвердите отличный ответ дерева выражений.
Рик Сладки
6

Как насчет подхода, ориентированного на тип данных, с методом расширения:

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

Чем вы можете оценить, как это:

var myResults = users.Where(u => roles.All(r => r.Match(u)));
Ян Олаф
источник
4

Хотя наиболее очевидный способ ответить на вопрос «Как реализовать механизм правил? (В C #)») - это выполнить заданный набор правил в последовательности, это обычно рассматривается как наивная реализация (не означает, что она не работает :-)

Кажется, это «достаточно хорошо» в вашем случае, потому что ваша проблема, скорее, заключается в том, «как запустить набор правил в последовательности», и лямбда / дерево выражений (ответ Мартина), безусловно, самый элегантный способ в этом отношении, если вы оснащены последними версиями C #.

Однако для более сложных сценариев здесь приведена ссылка на алгоритм Rete, который фактически реализован во многих коммерческих системах обработчиков правил, и другая ссылка на NRuler , реализацию этого алгоритма в C #.

Саймон Мурье
источник
3

Ответ Мартина был довольно хорошим. Я на самом деле создал движок правил, который имеет ту же идею, что и его. И я был удивлен, что это почти то же самое. Я включил часть его кода, чтобы несколько улучшить его. Хотя я сделал это для обработки более сложных правил.

Вы можете посмотреть на Yare.NET

Или загрузите его в Nuget

aiapatag
источник
2

Как насчет использования механизма правил рабочего процесса?

Вы можете выполнять правила рабочего процесса Windows без рабочего процесса, см. Блог Гая Бурштейна: http://blogs.microsoft.co.il/blogs/bursteg/archive/2006/10/11/RuleExecutionWithoutWorkflow.aspx

и чтобы программно создать свои правила, см. WebLog Стивена Кауфмана

http://blogs.msdn.com/b/skaufman/archive/2006/05/15/programmatically-create-windows-workflow-rules.aspx

Кевин Бертон
источник
2

Я добавил реализацию для и, или между правилами, я добавил класс RuleExpression, представляющий корень дерева, который может быть простым правилом или может быть и, или двоичные выражения, поскольку они не имеют правила и имеют выражения:

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
}

У меня есть другой класс, который компилирует ruleExpression в один 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
источник