簡體   English   中英

通過在 C# 中注入 object 值(即使用反射)來解析字符串公式

[英]Parsing a string formula by injecting object values (i.e. using reflection) in C#

我需要解析一個可以包含對象和屬性的字符串格式的公式。

例子:

"Person1.Age > 20""Person1.Age > RentalCar5.MinimumAge"

其中Person1Person類型的 object , RentalCar5是 RentalCar 類型的RentalCar

我有對象,所以可以檢查公式是否包含"Person1"並知道它是與特定Name屬性匹配的Person object,所以我需要確定Age屬性值,然后將整個"Person1.Age"部分替換為年齡,比如 21 歲。

注意:我將能夠基於迭代我擁有的對象來推斷類型,因此可以檢查 formulaString.Contains((obj1 as Person).Name)。

完成此操作后,很容易將公式解析為"21 >20"

有什么想法或想法嗎?

那里有許多第 3 方 C# 公式評估庫,但同樣的事情可以通過使用Roslyn C# 編譯器的動態腳本來實現。

雖然此解決方案在技術上並未按要求大量使用反射,並且它非常固定於指定的輸入類型,但它只是向您展示方法的入門。 您可以使用反射來解決或推斷此問題的某些方面,或者您可以從公式本身真正動態地生成腳本。

不是 100% 基於公式,而是創建一個固定的 class 包裝器來執行公式並表示所有有效或可能的輸入是一種更簡單的方法。 您可以直接控制約束,但公式現在可以采用任何形式的比較邏輯或運算符,只要它符合 class 結構和有效的 c# 語法。

使公式更抽象也很有幫助, Person1確實特定於單個上下文,您通常不會為每個人構造單獨的公式,而是您的公式可能應該采用以下形式:

"Person.Age > RentalCar.MinimumAge"

然后在執行上下文中,您可以使用公式作為單個業務規則來評估所選PersonRentalCar的任何組合。

IMO 避免使用 model 或鍵入名稱作為公式中的變量名稱,通過使用業務域名、在業務 model 中描述它們的名稱,而不是數據 Z20F35E630DAF44DFA4C3F68F539 來為它們提供上下文。 它可以避免什么是類型引用和什么是實例之間的沖突

"Driver.Age > Car.MinimumAge"

有關此過程的詳細背景信息,請閱讀對如何使用反射以編程方式創建 class 庫 DLL?

首先以長格式寫出 class 包裝器,該包裝器具有單一方法,其中注入了用戶的公式。 這些是原始帖子的要求:

  1. 返回 boolean 值
  2. 稱為Person1的字段/屬性/參數,它是Person的一種類型
  3. 字段/屬性/參數稱為RentalCar5 ,它是RentalCar的一種類型

以下腳本使用 model 的參數作為輸入,這將起作用,但我更喜歡使用實例Properties ,因為我發現它簡化了界面、腳本處理和調試過程,尤其是當有一個共同的上下文但有多個公式時。 這個例子只是為了讓你繼續

namespace DynamicScriptEngine
{
    public class PersonScript
    {
        public bool Evaluate(Person Driver, RentalCar Car)
        {
            return
            #region Dynamic Script
    
            Driver.Age > Car.MinimumAge
    
            #endregion Dynamic Script
            ;
        }
    }
}

現在,我們只編譯那個 class,調用方法並得到結果。 以下代碼將使用注入的公式生成上述 class 並返回響應。

編排

調用此方法以從需要結果的上下文中傳入參數和公式。

public static bool EvaluateFormula(Person Driver, RentalCar Car, string formula)
{
    string nameSpace = "DynamicScriptEngine";
    string className = "PersonScript";
    string methodName = "Evaluate";

    List<Type> knownTypes = new List<Type> { typeof(Person), typeof(RentalCar) };

    // 1. Generate the code
    string script = BuildScript(nameSpace, knownTypes, className, methodName, formula);
    // 2. Compile the script
    // 3. Load the Assembly
    Assembly dynamicAssembly = CompileScript(script);
    // 4. Execute the code
    object[] arguments = new object[] { person, rentalCar };
    bool result = ExecuteScript(dynamicAssembly, nameSpace, className, methodName, arguments);

    return result;
}

用法

這是如何調用上述方法來評估公式的示例:

static void Main(string[] args) { // 創建輸入條件 Person person1 = new Person { Name = "Person1", Age = 21 }; RentalCar car5 = new RentalCar { Name = "RentalCar1", MinimumAge = 20 }; RentalCar car1 = new RentalCar { Name = "RentalCar5", MinimumAge = 25 };

    // Evaluate the formulas
    Console.WriteLine("Compare Driver: {0}", person1);
    Console.WriteLine();
    string formula = "Driver.Age > 20";
    Console.WriteLine("Formula: {0} => {1}", formula, EvaluateFormula(person1, null, formula));

    Console.WriteLine();
    Console.WriteLine("Compare against Car: {0}", car5);
    formula = "Driver.Age > Car.MinimumAge";
    Console.WriteLine("Formula: {0} => {1}", formula, EvaluateFormula(person1, car5, formula));

    Console.WriteLine();
    Console.WriteLine("Compare against Car: {0}", car1);
    formula = "Driver.Age > Car.MinimumAge";
    Console.WriteLine("Formula: {0} => {1}", formula, EvaluateFormula(person1, car1, formula));
}

Output

演示輸出

代碼生成器

此方法將圍繞公式生成代碼包裝器,以便可以將其編譯為獨立程序集以供執行。

重要的是,我們包括動態腳本執行可能需要的命名空間和/或別名,您應該考慮將輸入類型的命名空間作為最低限度包括在內。

private static string BuildScript(string nameSpace, List<Type> knownTypes, string className, string methodName, string formula)
{
    StringBuilder code = new StringBuilder();
    code.AppendLine("using System;");
    code.AppendLine("using System.Linq;");
    // extract the usings from the list of known types
    foreach(var ns in knownTypes.Select(x => x.Namespace).Distinct())
    {
        code.AppendLine($"using {ns};");
    }

    code.AppendLine();
    code.AppendLine($"namespace {nameSpace}");
    code.AppendLine("{");
    code.AppendLine($"    public class {className}");
    code.AppendLine("    {");
    // NOTE: here you could define the inputs are properties on this class, if you wanted
    //       You might also evaluate the parameter names somehow from the formula itself
    //       But that adds another layer of complexity, KISS
    code.Append($"        public bool {methodName}(");
    code.Append("Person Driver, ");
    code.Append("RentalCar Car");
    code.AppendLine(")");
    code.AppendLine("        {");
    code.Append("        return ");

    // NOTE: Here we insert the actual formula
    code.Append(formula);

    code.AppendLine(";");
    code.AppendLine("        }");
    code.AppendLine("    }");
    code.AppendLine("}");
    return code.ToString();
}

匯編

private static Assembly CompileScript(string script)
{
    SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(script);

    // use "mytest.dll" if you want, random works well enough
    string assemblyName = System.IO.Path.GetRandomFileName();
    List<string> dlls = new List<string> {
    typeof(object).Assembly.Location,
    typeof(Enumerable).Assembly.Location,
    // NOTE: Include the Assembly that the Input Types exist in!
    //       And any other references, I just enumerate the working folder and load them all, but it's up to you.
    typeof(Person).Assembly.Location
};
    MetadataReference[] references = dlls.Distinct().Select(x => MetadataReference.CreateFromFile(x)).ToArray();

    CSharpCompilation compilation = CSharpCompilation.Create(
        assemblyName,
        syntaxTrees: new[] { syntaxTree },
        references: references,
        options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

    // Now we actually compile the script, this includes some very crude error handling, just to show you can
    using (var ms = new MemoryStream())
    {
        EmitResult result = compilation.Emit(ms);

        if (!result.Success)
        {
            IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic =>
                diagnostic.IsWarningAsError ||
                diagnostic.Severity == DiagnosticSeverity.Error);

            List<string> errors = new List<string>();
            foreach (Diagnostic diagnostic in failures)
            {
                //errors.AddDistinct(String.Format("{0} : {1}", diagnostic.Id, diagnostic.Location, diagnostic.GetMessage()));
                errors.Add(diagnostic.ToString());
            }

            throw new ApplicationException("Compilation Errors: " + String.Join(Environment.NewLine, errors));
        }
        else
        {
            ms.Seek(0, SeekOrigin.Begin);
            return Assembly.Load(ms.ToArray());
        }
    }
}

執行

這是實際執行生成的腳本的方法,這個方法很通用, Orchestration Logic會為我們准備好參數。 如果您使用的是實例屬性,那么這里的代碼會稍微復雜一些。

private static bool ExecuteScript(Assembly assembly, string nameSpace, string className, string methodName, object[] arguments)
{
    var appType = assembly.GetType($"{nameSpace}.{className}");
    object app = Activator.CreateInstance(appType);
    MethodInfo method = appType.GetMethod(methodName);

    object result = method.Invoke(app, arguments);
    return (bool)result;
}

這只是一個基本示例,這樣的解決方案具有很大的功能,但您需要注意它可能會在您的代碼中引入很多漏洞。 您可以采取一些措施來緩解安全問題,理想情況下,您應該在存儲公式時對其進行驗證和/或清理,並在不同的上下文或容器中執行它


一個例子

// Install-Package 'Microsoft.CodeAnalysis.CSharp.Scripting'
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;

namespace ConsoleApp2
{
    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }

        public override string ToString()
        {
            return $"Person: {Name} Age: {Age}";
        }
    }

    public class RentalCar
    {
        public string Name { get; set; }
        public int MinimumAge { get; set; }

        public override string ToString()
        {
            return $"RentalCar: {Name} MinimumAge: {MinimumAge}";
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // create the input conditions
            Person person1 = new Person { Name = "Person1", Age = 21 };
            RentalCar car5 = new RentalCar { Name = "RentalCar1", MinimumAge = 20 };
            RentalCar car1 = new RentalCar { Name = "RentalCar5", MinimumAge = 25 };

            // Evaluate the formulas
            Console.WriteLine("Compare Driver: {0}", person1);
            Console.WriteLine();
            string formula = "Driver.Age > 20";
            Console.WriteLine("Formula: {0} => {1}", formula, EvaluateFormula(person1, null, formula));

            Console.WriteLine();
            Console.WriteLine("Compare against Car: {0}", car5);
            formula = "Driver.Age > Car.MinimumAge";
            Console.WriteLine("Formula: {0} => {1}", formula, EvaluateFormula(person1, car5, formula));

            Console.WriteLine();
            Console.WriteLine("Compare against Car: {0}", car1);
            formula = "Driver.Age > Car.MinimumAge";
            Console.WriteLine("Formula: {0} => {1}", formula, EvaluateFormula(person1, car1, formula));
        }

        public static bool EvaluateFormula(Person driver, RentalCar car, string formula)
        {
            string nameSpace = "DynamicScriptEngine";
            string className = "PersonScript";
            string methodName = "Evaluate";

            List<Type> knownTypes = new List<Type> { typeof(Person), typeof(RentalCar) };

            // 1. Generate the code
            string script = BuildScript(nameSpace, knownTypes, className, methodName, formula);
            // 2. Compile the script
            // 3. Load the Assembly
            Assembly dynamicAssembly = CompileScript(script);
            // 4. Execute the code
            object[] arguments = new object[] { driver, car };
            bool result = ExecuteScript(dynamicAssembly, nameSpace, className, methodName, arguments);

            return result;
        }

        private static string BuildScript(string nameSpace, List<Type> knownTypes, string className, string methodName, string formula)
        {
            StringBuilder code = new StringBuilder();
            code.AppendLine("using System;");
            code.AppendLine("using System.Linq;");
            // extract the usings from the list of known types
            foreach(var ns in knownTypes.Select(x => x.Namespace).Distinct())
            {
                code.AppendLine($"using {ns};");
            }

            code.AppendLine();
            code.AppendLine($"namespace {nameSpace}");
            code.AppendLine("{");
            code.AppendLine($"    public class {className}");
            code.AppendLine("    {");
            // NOTE: here you could define the inputs are properties on this class, if you wanted
            //       You might also evaluate the parameter names somehow from the formula itself
            //       But that adds another layer of complexity, KISS
            code.Append($"        public bool {methodName}(");
            code.Append("Person Driver, ");
            code.Append("RentalCar Car");
            code.AppendLine(")");
            code.AppendLine("        {");
            code.Append("        return ");

            // NOTE: Here we insert the actual formula
            code.Append(formula);

            code.AppendLine(";");
            code.AppendLine("        }");
            code.AppendLine("    }");
            code.AppendLine("}");
            return code.ToString();
        }

        private static Assembly CompileScript(string script)
        {
            SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(script);

            // use "mytest.dll" if you want, random works well enough
            string assemblyName = System.IO.Path.GetRandomFileName();
            List<string> dlls = new List<string> {
            typeof(object).Assembly.Location,
            typeof(Enumerable).Assembly.Location,
            // NOTE: Include the Assembly that the Input Types exist in!
            //       And any other references, I just enumerate the working folder and load them all, but it's up to you.
            typeof(Person).Assembly.Location
        };
            MetadataReference[] references = dlls.Distinct().Select(x => MetadataReference.CreateFromFile(x)).ToArray();

            CSharpCompilation compilation = CSharpCompilation.Create(
                assemblyName,
                syntaxTrees: new[] { syntaxTree },
                references: references,
                options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

            // Now we actually compile the script, this includes some very crude error handling, just to show you can
            using (var ms = new MemoryStream())
            {
                EmitResult result = compilation.Emit(ms);

                if (!result.Success)
                {
                    IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic =>
                        diagnostic.IsWarningAsError ||
                        diagnostic.Severity == DiagnosticSeverity.Error);

                    List<string> errors = new List<string>();
                    foreach (Diagnostic diagnostic in failures)
                    {
                        //errors.AddDistinct(String.Format("{0} : {1}", diagnostic.Id, diagnostic.Location, diagnostic.GetMessage()));
                        errors.Add(diagnostic.ToString());
                    }

                    throw new ApplicationException("Compilation Errors: " + String.Join(Environment.NewLine, errors));
                }
                else
                {
                    ms.Seek(0, SeekOrigin.Begin);
                    return Assembly.Load(ms.ToArray());
                }
            }
        }

        private static bool ExecuteScript(Assembly assembly, string nameSpace, string className, string methodName, object[] arguments)
        {
            var appType = assembly.GetType($"{nameSpace}.{className}");
            object app = Activator.CreateInstance(appType);
            MethodInfo method = appType.GetMethod(methodName);

            object result = method.Invoke(app, arguments);
            return (bool)result;
        }
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM