有时候需要把某些动态条件保存为字符串的形式,然后在运行的时候加载并将这些字符串形式的条件转换为lambda表达式,继而进行过滤操作或者校验条件是否为真。这样一个是便于用户自行配置条件,另外也便于逻辑和配置的分离。比如一些股票软件里面的公式编辑器,用户可以在软件提供的一些基础指标上,通过一些运算表达式生成新的指标;再比如规则引擎RulesEngine,可以通过提供一些UI工具比如RulesEngineEditor ,供配置人员或者用户生成一些规则,然后将这些规则持久化起来,在另外的一些系统中加载这些规则然后执行,一些流行的低代码平台也是类似做法。

如果要手动的将字符串形式的条件表达式解析为lambda表达式需要做很多工作,这非常类似于在刚学编程时,实现一个类似于“计算四则运算表达式的值”的功能。如果再复杂一些,可能需要用到词法分析、语法分析这些编译器里面用到的功能。在C#中,可以运用之前介绍的表达式树ExpressionTree来一定程度上简化这些操作,但要手动解析,仍然十分繁琐。幸好,我们有一些现成的工具和方法使用,这里介绍两种:一是使用Roslyn的Microsoft.CodeAnalysis.Scripting,另外就是System.Linq.Dynamic.Core。在具体介绍使用方法之前,先详细介绍一下需要将字符串解析为lambda表达式的使用场景。

为什么需要将字符串解析为lambda表达式


这里详细举两个例子,一个是DataView里面的RowFilter,一个是RulesEngine里面的规则配置:

DataView的RowFilter


在WinForm中,将DataGridView控件绑定到DataTable上,如果要根据条件筛选数据,只需要在DataTable的DefaultView的RowFilter中输入条件表达式字符串即可,比如:

FinishTable.DefaultView.RowFilter=“([代码] = '601127' )  and  ([数量] > '100000' )  and  ([状态] = '卖出' )”

它是如何实现的呢?查看DataTable的代码可以看到,在DataView的RowFilter属性的Set方法内部,通过使用给定的条件表达式字符串,实例化了一个DataExpression对象,在该对象内部再通过一个名为ExpressionParser的类将上述的字符串做了解析,这个类是核心,总共有1000多行代码。

因为RowFilter可以接受条件表达式形成的字符串,且其中可以带一些逻辑,所以可以开发一些UI界面,来方便用户根据条件来筛选,然后只需要将用户所选中的条件,拼接成字符串最后放到RowFilter属性种即可。配置UI界面,可以做成这样:

是不是非常友好。更进一步,可以把各种条件保存起来起一个别名,作为一个筛选条件。下次只要点击这个别名就可以筛选出需要的数据,非常灵活便捷。

RowFilter为了实现直接使用条件表达式字符串,它在内部做了非常多的工作。除此之外,如果将DataGridView绑定DataTable,那么这个DataGridView就默认支持点击列的表头来进行排序,这内部同样默默做了很多工作。

现在假设,如果不使用DataGridView绑定DataTable的方式(DataTable可能过于重型,在后面我会写文章说明),而是使用推荐的绑定到BindingList<T>实体的这种方式,那么诸如这些排序、筛选的功能都是需要自己实现的,如果要灵活实现诸如上面的筛选功能,我们自己该如何实现?这里跳不过的一关就是将条件表达式字符串转换为lambda表达式。

规则引擎


另外一个例子就是规则引擎,比如微软出品的RulesEngine, 它举了一个例子,比如有如下表示折扣的2条规则:

[
  {
    "WorkflowName": "Discount",
    "Rules": [
      {
        "RuleName": "GiveDiscount10",
        "SuccessEvent": "10",
        "ErrorMessage": "One or more adjust rules failed.",
        "ErrorType": "Error",
        "RuleExpressionType": "LambdaExpression",
        "Expression": "input1.country == \"india\" AND input1.loyaltyFactor <= 2 AND input1.totalPurchasesToDate >= 5000"
      },
      {
        "RuleName": "GiveDiscount20",
        "SuccessEvent": "20",
        "ErrorMessage": "One or more adjust rules failed.",
        "ErrorType": "Error",
        "RuleExpressionType": "LambdaExpression",
        "Expression": "input1.country == \"india\" AND input1.loyaltyFactor >= 3 AND input1.totalPurchasesToDate >= 10000"
      }
    ]
  }
]

这些规则是保存在json文件里的,在Expression字段中,可以看到各种条件表达式,里面包含了一些逻辑。规则引擎可以直接加载这些配置文件,然后输入条件,就可以判断是否满足设定的规则。这种json的配置文件,可以通过另外的UI界面来可视化配置,比如RulesEngineEditor这个项目就提供了对json格式的规则文件的可视化编辑。

支持字符串表达式作为条件,可以将逻辑和表现分离,提供了很大的灵活性。要实现这一功能就需要将字符串解析为条件表达式。可以手动自己实现,但是需要费很多时间,这里简单介绍下两种方法。

将字符串解析为lambda表达式的简单实现


手动实现


先再假设有如下商品对象类:

public class Product
{
	public string Name { get; set; }
	public double Price { get; set; }
	public int Quantity { get; set; }
}

var products = new List<Product>
{
    new Product { Quantity = 10, Name = "苹果", Price = 5.02 },
    new Product { Quantity = 50, Name = "香蕉", Price = 5 },
    new Product { Quantity = 200, Name = "橙子", Price = 7.29 },
};

如果要手动实现,就需要先将表达式解析出来,这个表达式里可能包含括号、逻辑运算符(not and or)或者其它一些字段属性里的方法比如Contains等,难度其实很大,比如下面的简单的将一个等于字符串表达式,转换为lambda表达式就要费好大劲,这还是借助C#提供了Expression这个类的情况下。

var condition = "Name == 橙子";

// Parse the condition
var c = condition.Split(new string[] { "==" }, StringSplitOptions.None);
var propertyName = c[0].Trim();
var value = c[1].Trim();

// Create the lambda
var arg = Expression.Parameter(typeof(Product), "p");
var property = typeof(Product).GetProperty(propertyName);
var comparison = Expression.Equal(
Expression.MakeMemberAccess(arg, property),
Expression.Constant(value));
var lambda = Expression.Lambda<Func<Product, bool>>(comparison, arg).Compile();

可以看到,要创建从字符串到lambda表达式的转换,首先要解析字符串,然后再创建表达式。如果条件表达式字符串更复杂,解析难度就更高,同时定义创建表达式的那些步骤就越繁琐。后面介绍的这两种方法,可以很轻松解决这个问题。

Microsoft.CodeAnalysis.Scripting


第一种方法就是使用Roslyn这个编译器暴漏出来的一些分析功。要筛选出“数量大于等于10,且价格小于等于5 或者名称包含橙子”的商品来。使用LINQ非常简单:

var resultUsingLINQ = products.Where(product => (product.Quantity >= 10 && product.Price <= 5) || (product.Name.Contains("橙子")));

结果就筛选出了“香蕉”和“橙子”

现在假设这个条件有个工具可以生成,这个工具生成的条件类似上述Where里面的LINQ语句,它是一个字符串形式的,使用Rosyln提供的Microsoft.CodeAnalysis.Scripting功能,可以将这个字符串转换为lambda表达式,方法如下:

var productFilter = "product => ((product.Quantity >= 10)&&(product.Price <= 5))||(product.Name.Contains(\"橙子\"))";
var options = ScriptOptions.Default.AddReferences(typeof(Product).Assembly);
Func<Product, bool> filterExpression = await CSharpScript.EvaluateAsync<Func<Product, bool>>(productFilter, options);
var filterProducts = products.Where(filterExpression);
filterProducts.Dump();

以上实现,需要安装Microsoft.CodeAnalysis.Scripting这个nuget包。在表达式中,如果条件字段的类型是字符串,则需要使用引号引起来,因为字符串在引号中,所以这里需要使用转义符。在调用CSharpScript的EvaluateAsync方法时,首先需要指定要解析表达式里面的字段所在的类,可以通过定义ScriptionOptions来加载类所在的程序集。然后需要指定解析的结果,这里需要的是一个Func委托,这个委托的参数是Product类型,返回值是bool,这和LINQ里面Where表达式需要的参数一样。紧接着将该委托放到products的Where语句中即可。当然这里还要对EvaluateAsync的返回值结果进行一些校验。

假设这里用的是products的Find方法,Find方法需要一个Predicate<T>的类型,它实际上就是一个对Func<T,bool>的包装,也很简单,只需要像下面这样指定需要的返回的委托类型即可。

Predicate<Product> filterExpressionV2 = await CSharpScript.EvaluateAsync<Predicate<Product>>(productFilter, options);
var filterProductsV2 = products.FindAll(filterExpressionV2);
filterProductsV2.Dump();

或者直接使用前面解析出来的Func<Product,bool>来实例化一个Predicate<Product>:

Predicate<Product> filterExpressionV3 = new Predicate<Product>(filterExpression);
var filterProductsV3 = products.FindAll(filterExpressionV2);
filterProductsV3.Dump();

另外一种方法是用同样是微软出品的System.Linq.Dynamic.Core.

System.Linq.Dynamic.Core


还是以上面的例子说明,首先需要安装System.Linq.Dynamic.Core这个Nuget包,先定义一个泛型类以及一个泛型方法:

public class Compiler<TParam>
{
	public static Func<TParam, TResult> Compile<TResult>(string body)
	{
		ParameterExpression prm = Expression.Parameter(typeof(TParam), typeof(TParam).Name);
		LambdaExpression exp = DynamicExpressionParser.ParseLambda(new[] { prm }, typeof(TResult), body);
		return (Func<TParam, TResult>)exp.Compile();
	}
}

使用Compiler泛型类时需要指定需要编译的类型,这里就是Product,在Compile泛型方法中,需要指定返回的类型,这里是bool型。

然后使用方法如下:

var productFilter = "((product.Quantity >= 10)&&(product.Price <= 5))||product.Name.Contains(\"橙子\")";
var filterFunc = Compiler<Product>.Compile<bool>(productFilter);
var filterProducts = products.Where(filterFunc);
filterProducts.Dump();

注意,这里的字符串表达式跟DataView的RowFliter是一样的了,它不是个一个lambda表达式,因为缺少类似“product=>”的前导说明。

当然,也可以直接在Where中输入lambda表达式的字符串,省去上面的手动把字符串编译为lambda的步骤。

var productFilterV2 = "product => ((product.Quantity >= 10)&&(product.Price <= 5))||(product.Name.Contains(\"橙子\"))";
var filterProductsV2 = products.AsQueryable().Where(productFilterV2);
filterProductsV2.Dump();

这个Where表达式里面支持string参数类型是在System.Linq.Dynamic.Core里面提供的扩展。对比前面,这里的字符串是一个正儿八经的lambda表达式字符串,里面有“product=>”前导字符。

以上我是在LINQPad里面验证的,它里面提供了Dump方法,很方便直接打印实体结果。

上面不管是使用Microsoft.CodeAnalysis.Scripting还是使用System.Linq.Dynamic.Core,输出的最终结果是一样的,在实践中初步发现System.Linq.Dynamic.Core相较Microsoft.CodeAnalysis.Scripting快一点,可能是因为前者不仅提供了类似字符串表达式解析为lambda表达式的功能,还提供了其它编译器内部的功能,后者就是对Linq表达式的扩展,更有针对性。

总结


将条件字符串动态解析并转换为lambda表达式在很多场景下非常有用,它一方面便于实现逻辑和配置的分离,另一方面也提供了更大的灵活性。在很多场景都能看到将条件表达式字符串保存起来然后动态解析执行的列子,比如股票软件里面的公式编辑器、比如规则引擎RulesEngine。通常,要手动实现将条件表达式字符串解析为lambda表达式是困难的,这会用到一些编译器里面类似诸如语法分词,词法分析的内容。 虽然在C#中,可以运用之前介绍的表达式树ExpressionTree来一定程序上简化这些操作,但要手动解析,仍然十分繁琐。本文站在巨人的肩膀上介绍两种方法,一是使用Roslyn的Microsoft.CodeAnalysis.Scripting,另外就是System.Linq.Dynamic.Core。它们可以非常方便的能够帮助我们将条件表达式字符串动态解析为lambda表达式,使得我们可以专注于条件表达式字符串的UI设计以及核心逻辑的开发。希望本文对您有所帮助,后面会介绍在DataGridView种绑定BindingList的时候使用条件表达式来过滤显示的实现方式,这里就用到了本文介绍的内容。

 

参考