首页 > 解决方案 > 通过在 C# 中注入对象值(即使用反射)来解析字符串公式

问题描述

我需要解析一个可以包含对象和属性的字符串格式的公式。

例子:

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

wherePerson1将是 type的对象PersonRentalCar5type 的对象RentalCar

我有对象,因此可以检查公式包含"Person1"并知道它是Person与该特定Name属性匹配的对象,因此我需要确定Age属性值,然后将整个"Person1.Age"部分替换为年龄,例如 21。

注意:我将能够基于迭代我拥有的对象来推断类型,因此可以检查 formulaString.Contains((obj1 as Person).Name)。

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

有什么想法或想法吗?

标签: c#linqreflection

解决方案


那里有许多第 3 方 C# 公式评估库,但同样的事情可以通过使用Roslyn C# 编译器的动态脚本来实现。

虽然此解决方案在技术上并未按要求大量使用反射,并且它非常固定于指定的输入类型,但它只是向您展示方法的入门。您可以使用反射来解决或推断此问题的某些方面,或者您可以从公式本身真正动态地生成脚本。

不是 100% 基于公式,而是创建一个固定的类包装器来执行公式并表示所有有效或可能的输入是一种更简单的方法。您可以直接控制约束,但公式现在可以采用任何形式的比较逻辑或运算符,只要它符合类结构和有效的 c# 语法。

使公式更抽象也很有帮助,Person1实际上特定于单个上下文,您通常不会为每个人构建单独的公式,而是您的公式可能应该采用以下形式:

"Person.Age > RentalCar.MinimumAge"

然后在执行上下文中,您可以使用公式作为单个业务规则来评估 selectedPerson和的任何组合RentalCar

IMO 避免使用模型或类型名称作为公式中的变量名称,通过使用业务域名(在业务模型中描述它们的名称,而不是数据模型)为它们提供上下文。它可以避免什么是类型引用和什么是实例之间的冲突

"Driver.Age > Car.MinimumAge"

有关此过程的全面背景,请阅读对如何使用反射以编程方式创建类库 DLL?

首先以长格式写出一个类包装器,该类包装器具有一个方法,其中注入了用户的公式。这些是原始帖子的要求:

  1. 返回一个布尔值
  2. 字段/属性/参数称为Person1,它是一种Person
  3. 称为RentalCar5的字段/属性/参数是一种RentalCar

以下脚本使用参数对输入进行建模,这将起作用,但我更喜欢使用实例Properties,因为我发现它简化了界面、脚本处理和调试过程,尤其是在具有共同上下文但有多个公式的情况下。这个例子只是为了让你继续

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

现在,我们只需编译该类,调用方法并获取结果。以下代码将使用注入的公式生成上述类并返回响应。

编排

调用此方法以从需要结果的上下文中传入参数和公式。

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

输出

演示输出

代码生成器

此方法将围绕公式生成代码包装器,以便可以将其编译为独立程序集以供执行。

重要的是,我们包括动态脚本执行可能需要的命名空间和/或别名,您应该考虑将输入类型的命名空间作为最低限度包括在内。

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

推荐阅读