• 在C#开发中使用第三方组件LambdaParser、DynamicExpresso、Z.Expressions,实现动态解析/求值字符串表达式


    在进行项目开发的时候,刚好需要用到对字符串表达式进行求值的处理场景,因此寻找了几个符合要求的第三方组件LambdaParser、DynamicExpresso、Z.Expressions,它们各自功能有所不同,不过基本上都能满足要求。它们都可以根据相关的参数进行字符串表达式的求值,本篇随笔介绍它们三者的使用代码,以及总结其中的一些经验。

    数学表达式求值应该是最常见的,一般我们在应用程序中如果需要计算,是需要对参数进行类型转换,然后在后台进行相应计算的。但是如果是计算一些符合的式子或者公式,特别是参数不一定的情况下,这个就比较麻烦。利用第三方组件,对表达式进行快速求值,可以满足我们很多实际项目上的需求,而且处理起来也很方便。

    这几个第三方组件,它们的GitHub或官网地址:

    https://github.com/nreco/lambdaparser

    https://github.com/dynamicexpresso/DynamicExpresso 

    https://eval-expression.net/eval-execute

    不过Z.Expressions是收费的,前两者都是免费的。

     

    我使用字符串表达式进行求值的场景,主要就是想对一个SQL条件的表达式,转换为普通的字符串表达式,然后根据对象的参数值,进行求值处理,这几个表达式求值组件都支持这样的操作,为了更好演示它们的使用效果及代码,我们专门创建了一个案例代码进行测试验证,确认满足我的实际需求。

      

    1、Z.Expressions.Eval 表达式解析

    Z.Expression.Eval是一个免费开源的(后续收费了),可扩展的,超轻量级的公式化语言解析执行工具包,可以在运行时解析C#表达式的开源免费组件。Z.Expressions从2.0开始支持了NetCore,但是收费的。参考地址:https://riptutorial.com/eval-expression/learn/100000/getting-started 或者 https://eval-expression.net/eval-execute

    在运行时解析C#表达式,例如一些工资或者成本核算系统,就需要在后台动态配置计算表达式,从而进行计算求值。

    下面对几个不同的案例代码进行介绍及输出结果验证

     匿名类型处理

    //匿名类型
    string expression = "a*2 + b*3 - 3";
    int result = Eval.Execute<int>(expression, new { a = 10, b = 5 });
    Console.WriteLine("{0} = {1}", expression, result); //a*2 + b*3 - 3 = 32

    指定参数

    //指定参数
    expression = "{0}*2 + {1}*3 - 3";
    result = Eval.Execute<int>(expression, 10, 5);
    Console.WriteLine("{0} = {1}", expression, result);//{0}*2 + {1}*3 - 3 = 32

    类对象

    复制代码
    //类对象
    expression = "a*2 + b*3 - 3";
    dynamic expandoObject = new ExpandoObject();
    expandoObject.a = 10;
    expandoObject.b = 5;
    
    result = Eval.Execute<int>(expression, expandoObject);
    Console.WriteLine("{0} = {1}", expression, result); //a*2 + b*3 - 3 = 32
    复制代码

    字典对象

    复制代码
    //字典对象
    expression = "a*2 + b*3 - 3";
    var values = new Dictionary<string, object>()
    {
        { "a", 10 },
        { "b", 5 }
    };
    
    result = Eval.Execute<int>(expression, values);
    Console.WriteLine("{0} = {1}", expression, result);//a*2 + b*3 - 3 = 32
    复制代码

    委托类型

    复制代码
    //委托类型1
    expression = "{0}*2 + {1}*3";
    var compiled = Eval.Compile<Func<int, int, int>>(expression);
    result = compiled(10, 15);
    Console.WriteLine("{0} = {1}", expression, result);//{0}*2 + {1}*3 = 65
    
    //委托类型2
    expression = "a*2 + b*3";
    compiled = Eval.Compile<Func<int, int, int>>(expression, "a", "b");
    result = compiled(10, 15);
    Console.WriteLine("{0} = {1}", expression, result);//a*2 + b*3 = 65
    复制代码

    字符串扩展支持

    复制代码
    //字符串扩展支持-匿名类型
    expression = "a*2 + b*3 - 3";
    result = expression.Execute<int>(new { a = 10, b = 5 });
    Console.WriteLine("{0} = {1}", expression, result);//a*2 + b*3 - 3 = 32
    
    //字符串扩展支持-字典类型
    expression = "a*2 + b*3 - 3";
    values = new Dictionary<string, object>()
    {
        { "a", 10 },
        { "b", 5 }
    };
    result = expression.Execute<int>(values);
    Console.WriteLine("{0} = {1}", expression, result);//a*2 + b*3 - 3 = 32
    复制代码

    可以看出,该组件提供了非常丰富的表达式运算求值处理方式。

     

    2、NReco.LambdaParser 表达式解析

    我看中这个组件的处理,主要是因为它能够传入参数是字典类型,这样我可以非常方便的传入各种类型的参数,并且这个组件比较接近SQL语法,可以设置利用常规的=代替表达式的==,这样对于SQL语句来说是方便的。

    它的案例代码如下所示。

    复制代码
    /// <summary>
    /// NReco.LambdaParser 表达式解析
    /// </summary>
    private void btnLamdaParser_Click(object sender, EventArgs e)
    {
        var lambdaParser = new NReco.Linq.LambdaParser();
    
        var dict = new Dictionary<string, object>();
        dict["pi"] = 3.14M;
        dict["one"] = 1M;
        dict["two"] = 2M;
        dict["test"] = "test";
        Console.WriteLine(lambdaParser.Eval("pi>one && 0<one ? (1+8)/3+1*two : 0", dict)); // --> 5
        Console.WriteLine(lambdaParser.Eval("test.ToUpper()", dict)); // --> TEST
    
    
        Console.WriteLine(lambdaParser.Eval("pi>one && 0<one ", dict)); // --> True
        Console.WriteLine(lambdaParser.Eval("test.ToUpper()", dict)); // --> TEST
    }
    复制代码

    同样它支持的算术符号操作有:+, -, *, /, %,以及常规的逻辑判断:==, !=, >, <, >=, <=,如果需要它允许把=作为==比较,那么设置属性 AllowSingleEqualSign  = true 即可,如下代码。

        var lambdaParser = new LambdaParser();
        lambdaParser.AllowSingleEqualSign = true;//可以使用 = 作为逻辑判断,如Title ="Leader",而不用Title =="Leader"
        var evalResult = lambdaParser.Eval(repalce, dict);

    该组件没有过多提供例子,不过它的例子提供的关键点,基本上都能实现我们实际的表达式求值处理要求了。 

     

    3、DynamicExpresso 表达式解析

    相对于LambdaParser的简洁、Z.Expressions收费处理,Dynamic Expresso 可以说是提供了一个非常强大的、免费开源的处理类库,它提供非常多的表达式求值的实现方式。

    简单的字符串表达式求值如下代码

    var interpreter = new Interpreter();
    var result = interpreter.Eval("8 / 2 + 2");

    但是一般我们需要传入一定的参数进行表达式求值的。

    var target = new Interpreter();
    double result = target.Eval<double>("Math.Pow(x, y) + 5",
         new Parameter("x", typeof(double), 10),
         new Parameter("y", typeof(double), 2));

    或者

    复制代码
    var interpreter = new Interpreter();
    var parameters = new[] {
        new Parameter("x", 23),
        new Parameter("y", 7)
    };
    Assert.AreEqual(30, interpreter.Eval("x + y", parameters));
    复制代码

    或者赋值指定的参数

    var target = new Interpreter().SetVariable("myVar", 23);
    Assert.AreEqual(23, target.Eval("myVar"));

    对于字典类型的处理,是我喜欢的方式,它的案例代码如下所示。

    复制代码
    var interpreter = new Interpreter();
    var dict = new Dictionary<string, object>();
    dict.Add("a", 1.0);
    dict.Add("b", 2);
    dict.Add("d", 4);
    dict.Add("e", 5);
    dict.Add("str", 'f');
    
    foreach (var v in dict)
    {
        object value = v.Value;
        int para = 0;
        if (int.TryParse(v.Value.ToString(), out para))
        {
            value = (float)para;
        }
        interpreter.SetVariable(v.Key, value);
    }
    Console.WriteLine(interpreter.Eval("a+b").ToString()); //3
    Console.WriteLine(interpreter.Eval("a/b").ToString()); //0.5
    Console.WriteLine(interpreter.Eval("a > b").ToString()); //False
    Console.WriteLine(interpreter.Eval("str == 'f'").ToString()); //True
    复制代码

    对于类的属性表达式查询,测试代码如下所示

    复制代码
        var customers = new List<Customer> {
            new Customer() { Name = "David", Age = 31, Gender = 'M' },
            new Customer() { Name = "Mary", Age = 29, Gender = 'F' },
            new Customer() { Name = "Jack", Age = 2, Gender = 'M' },
            new Customer() { Name = "Marta", Age = 1, Gender = 'F' },
            new Customer() { Name = "Moses", Age = 120, Gender = 'M' },
        };
        string whereExpression = "customer.Age > 18 && customer.Gender == 'F'";
    
        Func<Customer, bool> dynamicWhere = interpreter.ParseAsDelegate<Func<Customer, bool>>(whereExpression, "customer");
        Console.WriteLine(customers.Where(dynamicWhere).Count());//=> 1
    
    
        var customer_query = (new List<Customer> {
            new Customer() { Name = "David", Age = 31, Gender = 'M' },
            new Customer() { Name = "Mary", Age = 29, Gender = 'F' },
            new Customer() { Name = "Jack", Age = 2, Gender = 'M' },
            new Customer() { Name = "Marta", Age = 1, Gender = 'F' },
            new Customer() { Name = "Moses", Age = 120, Gender = 'M' },
        }).AsQueryable();
        whereExpression = "customer.Age > 18 && customer.Gender == 'F'";
    
        var expression = interpreter.ParseAsExpression<Func<Customer, bool>>(whereExpression, "customer");
        Console.WriteLine(customer_query.Where(expression).Count());//=> 1
    复制代码

     

    4、SQL条件语句的正则表达式和字符串求值处理

    前面介绍了几个表达式求值处理的组件,他们基本上都能够满足实际的求值处理,只是提供的功能有所侧重。

    我主要希望用它来对特定的表达式进行求布尔值,判断表达式是否满足条件的。

    例如对于sql条件语句:(Amount> 500 and Title ='Leader') or Age> 32, 以及一个字典对象的参数集合,我希望能够提取里面的Amount、Title、Leader、Age这样的键,然后给字典赋值,从而判断表达式的值。

    由于sql表达式和C#代码的表达式逻辑语法有所差异,我们需要替换and Or 为实际的&& || 字符,因此给定替换的正则表达式:\sand|\sor

    而我需要先提取条件语句的键值内容,然后获得指定的键参数,那么也要提供一个正则表达式:\w*[^>=<!'()\s] ,这个正则表达式主要就是提取特定的字符匹配。

     提取内容的C#代码逻辑如下所示。

    复制代码
            private void btnRegexExtract_Click(object sender, EventArgs e)
            {
                var source = this.txtSource.Text;
    
                //先替换部分内容 \sand|\sor
                source = Regex.Replace(source, this.txtReplaceRegex.Text, "");//替换表达式
                //增加一行记录主内容
                this.txtContent.Text += "替换正则表达式后内容:";
                this.txtContent.AppendText(Environment.NewLine);
                this.txtContent.Text += source;
                this.txtContent.AppendText(Environment.NewLine);
    
                //在匹配内容处理
                var regex = new Regex(this.txtRegex.Text);
                var matches = regex.Matches(source);
    
                //遍历获得每个匹配的内容
                var fieldList = new List<string>();
                int i = 0;
                foreach (Match match in matches)
                {
                    this.txtContent.AppendText(match.Value);
                    this.txtContent.AppendText(Environment.NewLine);
                    if (i++ % 2 == 0)
                    {
                        fieldList.Add(match.Value);
                    }
                }
                this.txtContent.AppendText("获得表达式键:");
                this.txtContent.AppendText(Environment.NewLine);
                this.txtContent.AppendText(fieldList.ToJson());
                this.txtContent.AppendText(Environment.NewLine);
    
                var repalce = ReplaceExpress(this.txtSource.Text);
                this.txtContent.AppendText("替换And=>&& or=>|| '=> \" 操作符后内容:");
                this.txtContent.AppendText(Environment.NewLine);
                this.txtContent.AppendText(repalce);
            }
    复制代码
    复制代码
            /// <summary>
            /// 替换And=>&& or=>|| '=> \" 操作符后内容
            /// </summary>
            /// <param name="source"></param>
            /// <returns></returns>
            private string ReplaceExpress(string source)
            {
                //操作符替换表达式
                var repalce = Regex.Replace(source, @"\sand\s", " && "); //and => &&
                repalce = Regex.Replace(repalce, @"\sor\s", " || "); //or => ||
                repalce = Regex.Replace(repalce, @"'", "\""); //'=> \"
    
                return repalce;
            }
    复制代码

    表达式处理结果如下所示

     它的逻辑代码如下。

    复制代码
            private void btnRunExpression_Click(object sender, EventArgs e)
            {
                //操作符替换表达式
                var repalce = ReplaceExpress(this.txtSource.Text);
                this.txtContent.Text = "替换And=>&& or=>|| '=> \" 操作符后内容:";
                this.txtContent.AppendText(Environment.NewLine);
                this.txtContent.Text += repalce;
                this.txtContent.AppendText(Environment.NewLine);
                this.txtContent.AppendText(Environment.NewLine);
    
                //(Amount> 500 and Title ='Leader') or Age> 32
                var dict = new Dictionary<string, object>();
                dict["Amount"] = 600;
                dict["Title"] = "Leader";
                dict["Age"] = 40;
                
                this.txtContent.AppendText("字典内容");
                foreach(var key in dict.Keys)
                {
                    this.txtContent.AppendText($"{key}:{dict[key]} ");
                }
                this.txtContent.AppendText(Environment.NewLine);
                this.txtContent.AppendText(Environment.NewLine);
    
                //var valComparer = new ValueComparer() { NullComparison = ValueComparer.NullComparisonMode.Sql };
                //var lambdaParser = new LambdaParser(valComparer);
                var lambdaParser = new LambdaParser();
                lambdaParser.AllowSingleEqualSign = true;//可以使用=作为判断,如Title ="Leader",而不用Title =="Leader"
                var express1 = "(Amount> 500 && Title = \"Leader\") or Age>30";
                var result1 = lambdaParser.Eval(express1, dict);
                this.txtContent.AppendText("LambdaParser 表达式处理:");
                this.txtContent.AppendText(Environment.NewLine);
                this.txtContent.AppendText(express1 + " => " + result1);
    
                var express2 = "( Amount> 500 && Title =\"leader\" )"; //字符串比较(''=> "")
                var result2 = lambdaParser.Eval(express2, dict);
                this.txtContent.AppendText(Environment.NewLine);
                this.txtContent.AppendText(express2 + " => " + result2);
    
                var express3 = "Amount> 500";
                var result3 = lambdaParser.Eval(express3, dict);
                this.txtContent.AppendText(Environment.NewLine);
                this.txtContent.AppendText(express3 + " => " + result3);
    
                var express4 = "Title = \"Leader\" "; //字符串比较(''=> "")
                var result4 = lambdaParser.Eval(express4, dict);
                this.txtContent.AppendText(Environment.NewLine);
                this.txtContent.AppendText(express4 + " => " + result4);
    
                this.txtContent.AppendText(Environment.NewLine);
                Console.WriteLine(lambdaParser.Eval("Title.ToString()", dict)); // --> Leader
    
                //DynamicExpresso 表达式解析处理
                this.txtContent.AppendText(Environment.NewLine);
                this.txtContent.AppendText(Environment.NewLine);
                this.txtContent.AppendText("DynamicExpresso 表达式解析处理:");
    
                var interpreter = new Interpreter();
                foreach (var v in dict)
                {
                    interpreter.SetVariable(v.Key, v.Value);
                }
                //express3 = "Amount> 500";
                var result33 = interpreter.Eval(express3);
                this.txtContent.AppendText(Environment.NewLine);
                this.txtContent.AppendText(express3 + " => " + result33);
    
                //使用''出错,字符串比较需要使用""
                try
                {
                    express4 = "Title == \"Leader\" ";
                    var result44 = interpreter.Eval(express4);
                    this.txtContent.AppendText(Environment.NewLine);
                    this.txtContent.AppendText(express4 + " => " + result44);
                }
                catch(Exception ex)
                {
                    this.txtContent.AppendText(Environment.NewLine);
                    this.txtContent.AppendText(express4 + ",解析出错 => " + ex.Message);
                }
    
                //var dict = new Dictionary<string, object>();
                //dict["Amount"] = 600;
                //dict["Title"] = "Leader";
                //dict["Age"] = 40;
                this.txtContent.AppendText(Environment.NewLine);
                this.txtContent.AppendText(Environment.NewLine);
                this.txtContent.AppendText("Z.Expressions.Eval 表达式解析:");
                var result333 = express3.Execute<bool>(dict);
                this.txtContent.AppendText(Environment.NewLine);
                this.txtContent.AppendText(express3 + " => " + result333);
    
                express4 = "Title == 'Leader'"; //Z.Expressions可以接受 ' 代替 "
                var result444 = express4.Execute<bool>(dict);
                this.txtContent.AppendText(Environment.NewLine);
                this.txtContent.AppendText(express4 + " => " + result444);
            }
    复制代码

    这样我们就可以转换SQL条件表达式为实际的C#表达式,并通过赋值参数,实现动态表达式的求值处理。

     

  • 相关阅读:
    ESP8266-Arduino编程实例-VCNL4010光传感器驱动
    申报消防设施设计乙级资质关于财务审计报告的要求
    【UDS】ISO14229之0x85服务
    计算机毕业设计Java江智能股票推荐系统(源码+系统+mysql数据库+lw文档)
    React入门笔记(一)
    【C++】运算符重载
    B+树索引(11)之索引挑选(上)
    java毕业设计成品源码网站javaweb企业财务|记账|账单管理系统
    WordPress重新安装的几种方法(2022年新版教程)
    python 之利用plt.legend()添加图例
  • 原文地址:https://www.cnblogs.com/wuhuacong/p/16399567.html