Як реалізувати двигун правил?


205

У мене є таблиця db, в якій зберігаються такі дані:

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' та 'enako', як я міг це зробити?

C # - це статично набрана мова, тому не знайте, як рухатись вперед.

Відповіді:


390

Цей фрагмент компілює правила у швидкий виконуваний код (використовуючи дерева виразів ) і не потребує складних операторів комутації:

(Редагувати: повний робочий приклад із загальним методом )

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' замість 'больш_зрозуміння' і т. Д. - це тому, що '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 - типу "Expression". у вашому IDE, щоб отримати доступ до всіх них. Детальніше про дерева виразів читайте тут msdn.microsoft.com/en-us/library/bb397951.aspx
Martin Konicek

3
@Martin, де можна знайти список кваліфікованих імен операторів .NET?
Брайан Грехем

5
@Dark Slipstream Ви можете їх знайти тут msdn.microsoft.com/en-us/library/bb361179.aspx. Не всі вони булеві вирази - використовуйте лише булеві (наприклад, GreaterThan, NotEqual тощо).
Мартін Конічек

1
@BillDaugherty Правило простого класу значень з трьома властивостями: MemberName, Operator, TargetValue. Наприклад, нові правила ("Вік", "ВеликийТам", "20").
Мартін Конічек

14

Ось якийсь код, який компілюється як є і виконує роботу. В основному використовуйте два словники, один містить відображення від імен операторів до булевих функцій, а інший, що містить карту від імен властивостей типу User до PropertyInfos, використовуваних для виклику отримувача властивостей (якщо є загальнодоступним). Ви передаєте екземпляр користувача та три значення з таблиці в статичний метод застосування.

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>) у стовпці "Вираз" у моїй базі даних. У поточному дизайні, використовуючи генерацію коду, я запитую "правила" зі своєї бази даних і складаю збірку з типами "Правило", кожне із методом "Тест". Ось підпис для інтерфейсу, який реалізується кожне правило:

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

"Вираз" складається як тіло методу "Тест" при першому виконанні програми. Як ви бачите, інші стовпці таблиці також з’являються як властивості першого класу в праві, так що розробник має гнучкість для створення досвіду того, як користувач отримує повідомлення про помилку чи успіх.

Створення збірки в пам'яті - це разове явище під час вашої програми, і ви отримуєте підвищення продуктивності, не використовуючи роздуми під час оцінки своїх правил. Ваші вирази перевіряються під час виконання, оскільки збірка не буде генеруватись правильно, якщо ім'я властивості неправильно написано тощо.

Механіка створення вбудованої пам’яті така:

  • Завантажте свої правила з БД
  • повторіть правила та для кожного, використовуючи StringBuilder та деяку конденсацію рядків, напишіть Text, що представляє клас, який успадковується від IDataRule
  • компілювати за допомогою CodeDOM - додаткова інформація

Це насправді досить просто, оскільки для більшості цей код є реалізацією властивостей та ініціалізацією значень у конструкторі. Крім того, єдиний інший код - це Expression.
ПРИМІТКА. Існує обмеження, що вираз повинен бути .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;
                }
            }
        }
    }

Якщо є якісь інші запитання, коментарі чи запити щодо подальших зразків коду, повідомте мене.


Ви праві, що двигун можна зробити більш загальним, і CodeDOM API, безумовно, також є варіантом. Може замість коду "sb.AppendLine", який не дуже зрозумілий, ви могли б показати, як саме ви викликаєте CodeDOM?
Мартін Конічек

8

Рефлексія - це ваша найбільш універсальна відповідь. У вас є три стовпці даних, і їх потрібно обробляти різними способами:

  1. Назва вашого поля. Відображення - це спосіб отримати значення з кодованого імені поля.

  2. Ваш оператор порівняння. Їх має бути обмеженою кількістю, тому випадок справи повинен вирішувати їх найлегше. Тим більше, що деякі з них (мають один або декілька) трохи складніші.

  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)

тощо.

Це дає вам гнучкість для додавання додаткових варіантів порівняння. Це також означає, що ви можете кодувати в методах порівняння будь-який тип перевірки, який ви хочете, і зробити їх настільки ж складними, як вам потрібно. Тут також є можливість порівнянняToTo оцінюватися як рекурсивний зворотний зворот до іншого рядка або як значення поля, яке можна зробити так:

             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 (ліміт) повернення? -1 0 або 1? Не бачив цього b4!
Бланкмен

1
@Blankman: Близько: менше нуля, нуля або більше нуля. IComparableвикористовується для порівняння речей. Ось такі документи: Метод IComparable.CompareTo .
Rick Sladkey

2
Я не розумію, чому ця відповідь була підкреслена. Це порушує багато принципів дизайну: "Скажи не запитувати" => правила повинні бути запропоновані кожному, щоб повернути результат. "Відкрити для розширення / закрити для модифікації" => будь-яке нове правило означає, що метод ApplyRules потребує модифікації. Плюс код важко зрозуміти з першого погляду.
Звернення

2
Дійсно, шлях найменшого опору рідко є найкращим шляхом. Будь ласка, подивіться та підтвердіть відповідь на відмінне дерево виразів
Rick Sladkey

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

Однак для більш просунутих сценаріїв, тут є посилання на алгоритм Рете , який насправді реалізований у багатьох системах комерційних правил, та ще одне посилання на NRuler , реалізацію цього алгоритму в C #.


3

Відповідь Мартіна була досить хорошою. Я фактично зробив двигун правил, який має таку ж ідею, як і його. І я був здивований, що це майже те саме. Я включив частину його коду, щоб дещо вдосконалити його. Хоча я зробив це для обробки більш складних правил.

Ви можете подивитися на Yare.NET

Або скачайте його в Nuget


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/programmatic-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
}

У мене є ще один клас, який компілює правилоExpression до одного 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);
        }

    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.