วิธีการใช้เอ็นจินกฎ


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

เนื่องจากคุณสมบัติของวัตถุเช่น 'อายุ' หรือ 'ชื่อผู้ใช้' ถูกเก็บไว้ในตารางพร้อมกับโอเปอเรเตอร์การเปรียบเทียบ 'great_than' และ 'เท่าเทียมกัน' ฉันจะทำสิ่งนี้ได้อย่างไร

C # เป็นภาษาที่พิมพ์แบบคงที่ดังนั้นไม่แน่ใจว่าจะก้าวไปข้างหน้าอย่างไร

คำตอบ:


390

ตัวอย่างนี้รวบรวมกฎเป็นโค้ดที่ใช้งานได้อย่างรวดเร็ว (โดยใช้ต้นไม้ Expression ) และไม่จำเป็นต้องมีคำสั่งสลับซับซ้อนใด ๆ :

(แก้ไข: ตัวอย่างการทำงานเต็มรูปแบบด้วยวิธีการทั่วไป )

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' แทน 'Greater_than' เป็นต้น - นี่เป็นเพราะ 'GreaterThan' เป็นชื่อ. NET สำหรับโอเปอเรเตอร์ดังนั้นเราจึงไม่ต้องการการทำแผนที่เพิ่มเติม

หากคุณต้องการชื่อที่กำหนดเองคุณสามารถสร้างพจนานุกรมที่ง่ายมากและเพียงแปลผู้ประกอบการทั้งหมดก่อนที่จะรวบรวมกฎ:

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

รหัสใช้ประเภทผู้ใช้เพื่อความเรียบง่าย คุณสามารถแทนที่ผู้ใช้ด้วยประเภท T ทั่วไปเพื่อให้คอมไพเลอร์กฎทั่วไปสำหรับวัตถุประเภทใด ๆ นอกจากนี้รหัสควรจัดการข้อผิดพลาดเช่นชื่อผู้ดำเนินการที่ไม่รู้จัก

โปรดทราบว่าสามารถสร้างรหัสได้ทันทีแม้กระทั่งก่อนที่จะมีการเปิดตัว Expression tree API โดยใช้ Reflection.Emit วิธีการ LambdaExpression.Compile () ใช้ Reflection.Emit ภายใต้หน้ากาก (คุณสามารถเห็นสิ่งนี้ได้โดยใช้ILSpy )


ฉันสามารถอ่านเพิ่มเติมเกี่ยวกับคำตอบของคุณเพื่อเรียนรู้คลาส / วัตถุ / ฯลฯ คุณมีในรหัสของคุณ? มันเป็นต้นไม้ที่แสดงออกส่วนใหญ่?
Blankman

4
คลาสทั้งหมดมาจาก namespace System.Linq. Expressions และทั้งหมดถูกสร้างขึ้นโดยใช้วิธีการจากโรงงานของคลาส Expression - ประเภท "Expression" ใน IDE ของคุณเพื่อเข้าถึงทั้งหมด อ่านเพิ่มเติมเกี่ยวกับต้นไม้ Expression ที่นี่msdn.microsoft.com/en-us/library/bb397951.aspx
Martin Konicek

3
@Martin ฉันจะหารายชื่อผู้ดำเนินการ. NET ที่ผ่านการรับรองได้ที่ไหน
Brian Graham

5
@Dark Slipstream คุณสามารถค้นหาได้ที่นี่ msdn.microsoft.com/en-us/library/bb361179.aspx ไม่ใช่ทั้งหมดที่เป็นนิพจน์บูลีน - ใช้เฉพาะบูลีนเท่านั้น (เช่น GreaterThan, NotEqual เป็นต้น)
Martin Konicek

1
@BillDaugherty กฎค่าคลาสอย่างง่ายพร้อมคุณสมบัติสามอย่างคือ MemberName, Operator, TargetValue ตัวอย่างเช่นกฎใหม่ ("อายุ", "GreaterThan", "20")
Martin Konicek

14

นี่คือรหัสบางส่วนที่คอมไพล์และใช้งาน โดยทั่วไปจะใช้พจนานุกรมสองพจนานุกรมโดยหนึ่งมีการแมปจากชื่อตัวดำเนินการไปยังฟังก์ชันบูลีนและอีกอันประกอบด้วยแผนที่จากชื่อคุณสมบัติของชนิดผู้ใช้กับ PropertyInfos ที่ใช้เรียกใช้คุณสมบัติ getter (ถ้าเป็นสาธารณะ) คุณผ่านอินสแตนซ์ผู้ใช้และค่าสามค่าจากตารางของคุณไปยังวิธีการใช้แบบคงที่

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

"Expression" ถูกคอมไพล์เป็นเนื้อความของวิธี "Test" เมื่อแอปพลิเคชันดำเนินการครั้งแรก ในขณะที่คุณสามารถดูคอลัมน์อื่น ๆ ในตารางก็ปรากฏขึ้นเป็นคุณสมบัติชั้นหนึ่งในกฎเพื่อให้นักพัฒนามีความยืดหยุ่นในการสร้างประสบการณ์สำหรับวิธีที่ผู้ใช้ได้รับแจ้งจากความล้มเหลวหรือความสำเร็จ

การสร้างแอสเซมบลีในหน่วยความจำเป็นสิ่งที่เกิดขึ้น 1 ครั้งในระหว่างแอปพลิเคชันของคุณและคุณจะได้รับประสิทธิภาพที่เพิ่มขึ้นโดยไม่ต้องใช้การสะท้อนเมื่อประเมินกฎของคุณ นิพจน์ของคุณถูกตรวจสอบที่รันไทม์เนื่องจากแอสเซมบลีจะไม่สร้างอย่างถูกต้องหากสะกดชื่อคุณสมบัติ ฯลฯ

กลไกการสร้างแอสเซมบลีในหน่วยความจำมีดังนี้:

  • โหลดกฎของคุณจากฐานข้อมูล
  • วนซ้ำกฎและสำหรับแต่ละตัวโดยใช้ StringBuilder และการต่อสายอักขระบางตัวเขียนข้อความที่แสดงคลาสที่สืบทอดมาจาก IDataRule
  • รวบรวมโดยใช้ CodeDOM - ข้อมูลเพิ่มเติม

นี่เป็นจริงค่อนข้างง่ายเพราะส่วนใหญ่รหัสนี้เป็นการใช้งานทรัพย์สินและการเริ่มต้นค่าในตัวสร้าง นอกจากนั้นโค้ดอื่น ๆ เท่านั้นคือ Expression
หมายเหตุ: มีข้อ จำกัด ว่านิพจน์ของคุณต้องเป็น. NET 2.0 (ไม่มี lambdas หรือคุณสมบัติ 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 ได้อย่างไร
Martin Konicek

8

Reflection เป็นคำตอบที่หลากหลายที่สุดของคุณ คุณมีข้อมูลสามคอลัมน์และพวกเขาจำเป็นต้องได้รับการปฏิบัติในรูปแบบที่แตกต่างกัน:

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

หากคุณมีคุณสมบัติมากมายคุณอาจพบว่าวิธีการทำงานแบบใช้ตารางเป็นที่พอใจมากกว่า ในกรณีที่คุณจะสร้างคงที่แมปชื่อคุณสมบัติที่จะได้รับมอบหมายตรงกับการพูดDictionaryFunc<User, object>

หากคุณไม่ทราบชื่อของคุณสมบัติในเวลารวบรวมหรือคุณต้องการหลีกเลี่ยงกรณีพิเศษสำหรับแต่ละคุณสมบัติและไม่ต้องการใช้วิธีตารางคุณสามารถใช้การสะท้อนเพื่อรับคุณสมบัติ ตัวอย่างเช่น:

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

แต่เนื่องจากTargetValueอาจเป็นstringคุณจะต้องระมัดระวังในการแปลงประเภทจากตารางกฎหากจำเป็น


value.CompareTo (จำกัด ) ส่งคืนอย่างไร -1 0 หรือ 1 ไม่เห็นว่า b4!
Blankman

1
@Bankman: ปิด: น้อยกว่าศูนย์, ศูนย์หรือมากกว่าศูนย์ IComparableใช้สำหรับเปรียบเทียบสิ่งต่างๆ นี่คือเอกสารที่มี: วิธี IComparable.CompareTo
Rick Sladkey

2
ฉันไม่เข้าใจว่าทำไมคำตอบนี้ถึงได้รับการโหวต มันละเมิดหลักการออกแบบมากมาย: "บอกอย่าถาม" => กฎแต่ละข้อควรได้รับการขอให้ส่งคืนผลลัพธ์ "Open สำหรับส่วนขยาย / ปิดสำหรับการแก้ไข" => กฎใหม่ใด ๆ หมายถึงวิธีการ 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 # เวอร์ชันล่าสุด

อย่างไรก็ตามสำหรับสถานการณ์ขั้นสูงเพิ่มเติมนี่คือลิงค์ไปยังRete Algorithmที่นำมาใช้จริงในระบบกลไกกฎเชิงพาณิชย์จำนวนมากและอีกลิงค์หนึ่งไปยังNRulerซึ่งเป็นการใช้งานอัลกอริทึมใน C #


3

คำตอบของมาร์ตินค่อนข้างดี ฉันสร้างเครื่องมือสร้างกฏที่มีความคิดเหมือนกับเขา และฉันก็แปลกใจว่ามันเกือบจะเหมือนกัน ฉันได้รวมรหัสบางส่วนของเขาไว้เพื่อปรับปรุง แม้ว่าฉันจะทำมันเพื่อจัดการกฎที่ซับซ้อนมากขึ้น

คุณสามารถดูYare.NET

หรือดาวน์โหลดในNuget


2

วิธีการเกี่ยวกับการใช้เอ็นจินเวิร์กโฟลว์กฎ

คุณสามารถดำเนินการกฎเวิร์กโฟลว์ของ Windows โดยไม่ต้องมีเวิร์กโฟลว์ดูบล็อกของ Guy Burstein: http://blogs.microsoft.co.il/blogs/bursteg/archive/2006/10/11/RuleExecutionWithoutWorkflow.aspx

และเมื่อต้องการสร้างกฎของคุณโดยทางโปรแกรมให้ดูที่ WebLog ของ Stephen Kaufman

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
}

ฉันมีคลาสอื่นที่รวบรวมกฎการแสดงออกถึงหนึ่ง 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.