규칙 엔진을 구현하는 방법?


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

'연령'또는 'user_name'과 같은 객체의 속성이 비교 연산자 'great_than'및 'equal'과 함께 테이블에 저장되어 있으므로 어떻게해야합니까?

C #은 정적으로 입력 된 언어이므로 어떻게 진행해야할지 잘 모르겠습니다.

답변:


390

이 스 니펫 은 규칙을 빠른 실행 코드 ( Expression Tree 사용) 로 컴파일하며 복잡한 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);
    }
}

'greater_than'등 대신 'GreaterThan'을 사용했습니다. 이는 'GreaterThan'이 연산자의 .NET 이름이므로 추가 매핑이 필요하지 않기 때문입니다.

사용자 지정 이름이 필요한 경우 매우 간단한 사전을 작성하고 규칙을 컴파일하기 전에 모든 연산자를 번역하면됩니다.

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

코드는 단순성을 위해 User 유형을 사용합니다. User를 일반 유형 T로 바꾸면 모든 유형의 객체에 대한 일반 규칙 컴파일러 를 가질 수 있습니다. 또한 코드는 알 수없는 연산자 이름과 같은 오류를 처리해야합니다.

Reflection.Emit을 사용하여 Expression Tree API가 도입되기 전에도 즉시 코드를 생성 할 수있었습니다. LambdaExpression.Compile () 메서드는 커버 아래에서 Reflection.Emit을 사용합니다 ( ILSpy를 사용하여 볼 수 있음 ).


클래스 / 객체 등을 배우기위한 귀하의 답변에 대한 자세한 내용은 어디서 볼 수 있습니까? 당신은 당신의 코드에 있습니까? 주로 표현 나무입니까?
Blankman

4
모든 클래스는 네임 스페이스 System.Linq.Expressions에서 가져오고 Expression 클래스의 팩토리 메서드 ( "Expression"형식)를 사용하여 만들어집니다. IDE에서 모든 항목에 액세스하십시오. 식 트리에 대한 자세한 내용은 여기를 참조
Martin Konicek

3
@Martin 정규화 된 .NET 운영자 이름 목록을 어디에서 찾을 수 있습니까?
브라이언 그레이엄

5
@Dark Slipstream 여기에서 msdn.microsoft.com/en-us/library/bb361179.aspx를 찾을 수 있습니다. 그들 모두가 부울 표현식은 아니지만-부울 표현식 만 사용하십시오 (예 : GreaterThan, NotEqual 등).
Martin Konicek

1
@BillDaugherty MemberName, Operator, TargetValue의 세 가지 속성이있는 간단한 값 클래스를 규칙 화하십시오. 예를 들어, 새 규칙 ( "Age", "GreaterThan", "20")입니다.
Martin Konicek

14

다음은 그대로 컴파일하고 작업을 수행하는 코드입니다. 기본적으로 두 개의 사전을 사용하십시오. 하나는 연산자 이름에서 부울 함수로의 맵핑을 포함하고 다른 하나는 사용자 유형의 특성 이름에서 특성 게터 (공용 인 경우)를 호출하는 데 사용되는 PropertyInfo 로의 맵핑을 포함합니다. 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

귀하의 질문에 설명 된 것과 다른 접근 방식을 취하는 규칙 엔진을 만들었지 만 현재 접근 방식보다 훨씬 유연하다고 생각합니다.

현재 접근 방식은 단일 사용자 인 "사용자"에 중점을두고 있으며 영구 규칙은 "propertyname", "operator"및 "value"를 식별합니다. 내 패턴은 대신 데이터베이스의 "표현식"열에 조건 자에 대한 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; }
}

"Expression"은 응용 프로그램이 처음 실행될 때 "Test"메서드의 본문으로 컴파일됩니다. 보시다시피 테이블의 다른 열도 규칙에서 일류 속성으로 표시되므로 개발자는 사용자에게 실패 또는 성공을 알리는 방법에 대한 경험을 유연하게 만들 수 있습니다.

인 메모리 어셈블리 생성은 응용 프로그램에서 1 회 발생하며 규칙을 평가할 때 리플렉션을 사용하지 않아도 성능이 향상됩니다. 속성 이름의 철자가 틀린 경우 어셈블리가 올바르게 생성되지 않으므로 런타임에식이 확인됩니다.

인 메모리 어셈블리를 만드는 메커니즘은 다음과 같습니다.

  • DB에서 규칙을로드하십시오.
  • StringBuilder와 일부 문자열 연결을 사용하여 규칙과 각 규칙을 반복하고 IDataRule에서 상속되는 클래스를 나타내는 Text를 작성하십시오.
  • CodeDOM을 사용하여 컴파일- 추가 정보

대부분의 경우이 코드는 생성자의 속성 구현 및 값 초기화이므로 실제로는 매우 간단합니다. 그 외에도 다른 코드는 Expression입니다.
참고 : CodeDOM의 제한으로 인해식이 .NET 2.0이어야합니다 (람다 나 다른 C # 3.0 기능 없음).

다음은 이에 대한 샘플 코드입니다.

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

이 외에도 ICollection>을 구현 한 "DataRuleCollection"이라는 클래스를 만들었습니다. 이를 통해 "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을 정확히 호출하는 방법을 보여줄 수 있습니까?
Martin Konicek

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)

비교 옵션을 추가 할 수있는 유연성을 제공합니다. 또한 비교 방법 내에서 원하는 유형 유효성 검사를 코딩하고 원하는대로 복잡하게 만들 수 있습니다. 또한 CompareTo를 다른 회선으로 재귀 호출로 평가하거나 필드 값으로 평가할 수있는 옵션도 있습니다.

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

그것은 모두 미래의 가능성에 달려 있습니다 ....


또한 반영된 어셈블리 / 객체를 캐시하여 코드 성능을 향상시킬 수 있습니다.
Mr.

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 : 닫기 : 0보다 작거나 0보다 크거나 0보다 큽니다. IComparable사물을 비교하는 데 사용됩니다. 문서는 다음과 같습니다. IComparable.CompareTo Method .
Rick Sladkey 2016 년

2
왜이 답변이 투표를했는지 이해할 수 없습니다. 그것은 많은 설계 원칙을 위반합니다 : "묻지 말고"=> 규칙은 각각 결과를 반환 하도록 요청 해야합니다 . "확장을 위해 열기 / 수정을 위해 닫기"=> 새 규칙은 ApplyRules 메소드를 수정해야 함을 의미합니다. 또한 코드는 한 눈에 이해하기 어렵습니다.
Appetre

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 # 버전이 장착되어 있습니다.

그러나 고급 시나리오의 경우 실제로 많은 상용 규칙 엔진 시스템에서 구현 되는 Rete Algorithm에 대한 링크 와 C #에서 해당 알고리즘을 구현 하는 NRuler 에 대한 다른 링크가 있습니다.


3

마틴의 대답은 꽤 좋았습니다. 나는 실제로 그의 것과 같은 생각을 가진 규칙 엔진을 만들었습니다. 그리고 나는 거의 동일하다는 것에 놀랐습니다. 나는 그의 코드 중 일부를 포함시켜 약간 향상시켰다. 더 복잡한 규칙을 처리하도록 만들었지 만

Yare.NET을 볼 수 있습니다

또는 Nuget 에서 다운로드하십시오.


2

워크 플로 규칙 엔진을 사용하는 것은 어떻습니까?

워크 플로없이 Windows 워크 플로 규칙을 실행할 수 있습니다. Guy Burstein의 블로그 ( http://blogs.microsoft.co.il/blogs/bursteg/archive/2006/10/11/RuleExecutionWithoutWorkflow.aspx)를 참조하십시오.

프로그래밍 방식으로 규칙을 작성하려면 Stephen Kaufman의 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);
        }

    }
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.