Bagaimana cara mengimplementasikan mesin aturan?

205

Saya memiliki tabel db yang menyimpan yang berikut:

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

Sekarang katakan saya memiliki koleksi aturan ini:

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

Sekarang saya memiliki instance dari pengguna juga:

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

Bagaimana saya mengulangi aturan-aturan ini, dan menerapkan logika dan melakukan perbandingan dll?

if(user.age > 15)

if(user.username == "some_name")

Karena properti objek seperti 'usia' atau 'nama_pengguna' disimpan dalam tabel, bersama dengan operator perbandingan 'great_than' dan 'sama', bagaimana saya bisa melakukan ini?

C # adalah bahasa yang diketik secara statis, jadi tidak yakin bagaimana untuk maju.

Blankman
sumber

Jawaban:

391

Cuplikan ini mengkompilasi Aturan menjadi kode yang dapat dieksekusi cepat (menggunakan pohon Ekspresi ) dan tidak memerlukan pernyataan peralihan yang rumit:

(Edit: contoh kerja penuh dengan metode generik )

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

Anda kemudian dapat menulis:

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

Berikut ini adalah implementasi dari 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);
    }
}

Perhatikan bahwa saya menggunakan 'GreaterThan' daripada 'Greater_than' dll - ini karena 'GreaterThan' adalah nama .NET untuk operator, oleh karena itu kami tidak memerlukan pemetaan tambahan.

Jika Anda membutuhkan nama khusus, Anda dapat membuat kamus yang sangat sederhana dan hanya menerjemahkan semua operator sebelum menyusun aturan:

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

Kode menggunakan tipe Pengguna untuk kesederhanaan. Anda dapat mengganti Pengguna dengan tipe T generik untuk memiliki kompilator Aturan generik untuk semua jenis objek. Selain itu, kode harus menangani kesalahan, seperti nama operator yang tidak dikenal.

Perhatikan bahwa membuat kode dengan cepat dimungkinkan bahkan sebelum API pohon Ekspresi diperkenalkan, menggunakan Reflection.Emit. Metode LambdaExpression.Compile () menggunakan Reflection.Emit di bawah selimut (Anda dapat melihat ini menggunakan ILSpy ).

Martin Konicek
sumber
Di mana saya dapat membaca lebih lanjut tentang jawaban Anda untuk mempelajari kelas / objek / dll. Anda miliki di dalam kode Anda? Itu sebagian besar pohon ekspresi?
Blankman
4
Semua kelas berasal dari namespace System.Linq.Expressions, dan semua dibuat menggunakan metode pabrik dari kelas Ekspresi - ketik "Ekspresi." di IDE Anda untuk mengakses semuanya. Baca lebih lanjut tentang pohon Ekspresi di sini msdn.microsoft.com/en-us/library/bb397951.aspx
Martin Konicek
3
@ Martin di mana saya dapat menemukan daftar nama .NET operator yang memenuhi syarat?
Brian Graham
5
@Dark Slipstream Anda dapat menemukannya di sini msdn.microsoft.com/en-us/library/bb361179.aspx. Tidak semuanya adalah ekspresi boolean - hanya gunakan yang boolean (seperti GreaterThan, NotEqual, dll.).
Martin Konicek
1
@BillDaugherty Rule kelas nilai sederhana dengan tiga properti: MemberName, Operator, TargetValue. Misalnya, Aturan baru ("Usia", "GreaterThan", "20").
Martin Konicek
14

Berikut adalah beberapa kode yang mengkompilasi apa adanya dan melakukan pekerjaan. Pada dasarnya menggunakan dua kamus, yang berisi pemetaan dari nama operator ke fungsi boolean, dan yang lain berisi peta dari nama properti dari tipe Pengguna ke PropertyInfos yang digunakan untuk memanggil pengambil properti (jika publik). Anda melewatkan instance Pengguna, dan tiga nilai dari tabel Anda ke metode Terapkan statis.

class User
{
    public int Age { get; set; }
    public string UserName { get; set; }
}

class Operator
{
    private static Dictionary<string, Func<object, object, bool>> s_operators;
    private static Dictionary<string, PropertyInfo> s_properties;
    static Operator()
    {
        s_operators = new Dictionary<string, Func<object, object, bool>>();
        s_operators["greater_than"] = new Func<object, object, bool>(s_opGreaterThan);
        s_operators["equal"] = new Func<object, object, bool>(s_opEqual);

        s_properties = typeof(User).GetProperties().ToDictionary(propInfo => propInfo.Name);
    }

    public static bool Apply(User user, string op, string prop, object target)
    {
        return s_operators[op](GetPropValue(user, prop), target);
    }

    private static object GetPropValue(User user, string prop)
    {
        PropertyInfo propInfo = s_properties[prop];
        return propInfo.GetGetMethod(false).Invoke(user, null);
    }

    #region Operators

    static bool s_opGreaterThan(object o1, object o2)
    {
        if (o1 == null || o2 == null || o1.GetType() != o2.GetType() || !(o1 is IComparable))
            return false;
        return (o1 as IComparable).CompareTo(o2) > 0;
    }

    static bool s_opEqual(object o1, object o2)
    {
        return o1 == o2;
    }

    //etc.

    #endregion

    public static void Main(string[] args)
    {
        User user = new User() { Age = 16, UserName = "John" };
        Console.WriteLine(Operator.Apply(user, "greater_than", "Age", 15));
        Console.WriteLine(Operator.Apply(user, "greater_than", "Age", 17));
        Console.WriteLine(Operator.Apply(user, "equal", "UserName", "John"));
        Console.WriteLine(Operator.Apply(user, "equal", "UserName", "Bob"));
    }
}
Petar Ivanov
sumber
9

Saya membangun mesin aturan yang menggunakan pendekatan berbeda dari yang Anda uraikan dalam pertanyaan Anda, tetapi saya pikir Anda akan menemukan itu jauh lebih fleksibel daripada pendekatan Anda saat ini.

Pendekatan Anda saat ini tampaknya difokuskan pada satu entitas, "Pengguna", dan aturan Anda yang tetap mengidentifikasi "propertyname", "operator" dan "value". Pola saya, sebagai gantinya menyimpan kode C # untuk predikat (Func <T, bool>) dalam kolom "Ekspresi" di database saya. Dalam desain saat ini, menggunakan pembuatan kode saya menanyakan "aturan" dari database saya dan mengkompilasi perakitan dengan tipe "Aturan", masing-masing dengan metode "Uji". Berikut adalah tanda tangan untuk antarmuka yang diterapkan setiap Aturan:

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

"Ekspresi" dikompilasi sebagai tubuh dari metode "Uji" ketika aplikasi pertama kali dieksekusi. Seperti yang Anda lihat, kolom lain dalam tabel juga ditampilkan sebagai properti kelas satu pada aturan sehingga pengembang memiliki fleksibilitas untuk menciptakan pengalaman tentang bagaimana pengguna mendapat pemberitahuan tentang kegagalan atau kesuksesan.

Menghasilkan perakitan dalam memori adalah kejadian 1 kali selama aplikasi Anda dan Anda mendapatkan peningkatan kinerja dengan tidak harus menggunakan refleksi saat mengevaluasi aturan Anda. Ekspresi Anda diperiksa pada saat runtime karena majelis tidak akan menghasilkan dengan benar jika nama properti salah eja, dll.

Mekanisme pembuatan rakitan di dalam memori adalah sebagai berikut:

  • Muat aturan Anda dari DB
  • beralih pada aturan dan untuk masing-masing, menggunakan StringBuilder dan beberapa rangkaian string menulis Teks yang mewakili kelas yang mewarisi dari IDataRule
  • kompilasi menggunakan CodeDOM - info lebih lanjut

Ini sebenarnya cukup sederhana karena sebagian besar kode ini adalah implementasi properti dan inisialisasi nilai dalam konstruktor. Selain itu, satu-satunya kode lainnya adalah Ekspresi.
CATATAN: ada batasan bahwa ekspresi Anda harus. NET 2.0 (tidak ada fitur lambdas atau C # 3.0 lainnya) karena keterbatasan dalam CodeDOM.

Berikut ini beberapa contoh kode untuk itu.

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

Di luar ini saya membuat kelas yang saya sebut "DataRuleCollection", yang mengimplementasikan ICollection>. Ini memungkinkan saya untuk membuat kemampuan "TestAll" dan pengindeks untuk mengeksekusi aturan tertentu dengan nama. Berikut ini implementasi untuk kedua metode tersebut.

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

LEBIH KODE: Ada permintaan untuk kode yang terkait dengan Pembuatan Kode. Saya merangkum fungsionalitas dalam kelas yang disebut 'RulesAssemblyGenerator' yang telah saya sertakan di bawah ini.

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

Jika ada lain pertanyaan atau komentar atau permintaan untuk sampel kode lebih lanjut, biarkan aku tahu.

Glenn Ferrie
sumber
Anda benar bahwa mesin dapat dibuat lebih umum dan API CodeDOM jelas juga merupakan pilihan. Mungkin alih-alih kode "sb.AppendLine" yang tidak terlalu jelas, Anda dapat menunjukkan bagaimana tepatnya Anda menjalankan CodeDOM?
Martin Konicek
8

Refleksi adalah jawaban Anda yang paling fleksibel. Anda memiliki tiga kolom data, dan mereka perlu diperlakukan dengan cara yang berbeda:

  1. Nama bidang Anda Refleksi adalah cara untuk mendapatkan nilai dari nama bidang kode.

  2. Operator pembanding Anda. Seharusnya ada jumlah yang terbatas, jadi pernyataan kasus harus menanganinya dengan mudah. Terutama karena beberapa dari mereka (memiliki satu atau lebih) sedikit lebih kompleks.

  3. Nilai perbandingan Anda. Jika ini semua adalah nilai langsung maka ini mudah, meskipun Anda harus membagi beberapa entri. Namun, Anda juga bisa menggunakan refleksi jika itu adalah nama bidang juga.

Saya akan mengambil pendekatan yang lebih seperti:

    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)

dll. dll

Ini memberi Anda fleksibilitas untuk menambahkan lebih banyak opsi untuk perbandingan. Ini juga berarti bahwa Anda dapat membuat kode dalam metode Perbandingan segala jenis validasi yang Anda inginkan, dan menjadikannya serumit yang Anda inginkan. Ada juga opsi di sini untuk CompareTo untuk dievaluasi sebagai panggilan rekursif ke saluran lain, atau sebagai nilai bidang, yang dapat dilakukan seperti:

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

Itu semua tergantung pada kemungkinan untuk masa depan ....

Schroedingers Cat
sumber
Dan Anda dapat menembolok rakitan / objek yang dipantulkan yang akan membuat kode Anda semakin berkinerja.
Mrchief
7

Jika Anda hanya memiliki sedikit properti dan operator, jalur paling tidak resistensi adalah hanya kode semua cek sebagai kasus khusus seperti ini:

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

Jika Anda memiliki banyak properti, Anda mungkin menemukan pendekatan berbasis tabel lebih enak. Dalam hal ini Anda akan membuat statis Dictionaryyang memetakan nama properti ke delegasi yang cocok, katakanlah,Func<User, object> ,.

Jika Anda tidak tahu nama properti pada waktu kompilasi, atau Anda ingin menghindari kasus khusus untuk setiap properti dan tidak ingin menggunakan pendekatan tabel, Anda dapat menggunakan refleksi untuk mendapatkan properti. Sebagai contoh:

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

Tetapi karena TargetValuemungkin a string, Anda harus berhati-hati untuk melakukan konversi jenis dari tabel aturan jika perlu.

Rick Sladkey
sumber
apa yang value.CompareTo (limit) kembali? -1 0 atau 1? Belum pernah melihat b4 itu!
Blankman
1
@ Bankman: Tutup: kurang dari nol, nol atau lebih besar dari nol. IComparabledigunakan untuk membandingkan sesuatu. Berikut adalah dokumen: Metode IComparable.CompareTo .
Rick Sladkey
2
Saya tidak mengerti mengapa jawaban ini dipilih. Itu melanggar banyak prinsip desain: "Katakan jangan tanya" => aturan masing-masing harus diminta untuk mengembalikan hasil. "Buka untuk ekstensi / ditutup untuk modifikasi" => aturan baru berarti metode ApplyRules perlu modifikasi. Plus kodenya sulit dipahami sekilas.
Appetere
2
Memang, jalan dengan perlawanan paling sedikit jarang merupakan jalan terbaik. Silakan lihat dan tingkatkan jawaban pohon ekspresi yang bagus.
Rick Sladkey
6

Bagaimana dengan pendekatan berorientasi tipe data dengan metode ekstensi:

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

Daripada Anda dapat mengevakuasi seperti ini:

var myResults = users.Where(u => roles.All(r => r.Match(u)));
Yann Olaf
sumber
4

Meskipun cara yang paling jelas untuk menjawab pertanyaan "Bagaimana mengimplementasikan mesin aturan? (Dalam C #)" adalah dengan mengeksekusi seperangkat aturan tertentu secara berurutan, ini secara umum dianggap sebagai implementasi yang naif (tidak berarti itu tidak bekerja :-)

Tampaknya ini "cukup baik" dalam kasus Anda karena masalah Anda tampaknya lebih pada "bagaimana menjalankan seperangkat aturan secara berurutan", dan pohon lambda / ekspresi (jawaban Martin) tentu saja merupakan cara paling elegan dalam masalah ini jika Anda dilengkapi dengan versi C # terbaru.

Namun untuk skenario yang lebih maju, berikut ini adalah tautan ke Algoritma Rete yang sebenarnya diimplementasikan dalam banyak sistem mesin aturan komersial, dan tautan lain ke NRuler , sebuah implementasi dari algoritma tersebut dalam C #.

Simon Mourier
sumber
3

Jawaban Martin cukup bagus. Saya sebenarnya membuat mesin aturan yang memiliki ide yang sama dengannya. Dan saya terkejut bahwa itu hampir sama. Saya telah memasukkan beberapa kodenya untuk memperbaikinya. Meskipun saya sudah membuatnya menangani aturan yang lebih kompleks.

Anda dapat melihat Yare.NET

Atau unduh di Nuget

aiapatag
sumber
2

Saya menambahkan implementasi untuk dan, atau di antara aturan, saya menambahkan class RuleExpression yang mewakili akar dari sebuah pohon yang dapat dijadikan sebagai aturan sederhana atau dapat berupa, dan, atau ekspresi biner di sana karena mereka tidak memiliki aturan dan memiliki ekspresi:

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
}

Saya memiliki kelas lain yang mengkompilasi ruleExpression menjadi satu 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
sumber