[C#.NET 拾遗补漏]13:动态构建LINQ查询表达式
最近工作中遇到一个这样的需求:在某个列表查询功能中,可以选择某个数字列(如商品单价、当天销售额、当月销售额等),再选择 小于或等于
和 大于或等于
,再填写一个待比较的数值,对数据进行查询过滤。
如果只有一两个这样的数字列,那么使用 Entity Framework Core 可以这么写 LINQ 查询:
public Task> GetProductsAsync(string propertyToFilter, MathOperator mathOperator, decimal value)
{
var query = _context.Products.AsNoTracking();
query = propertyToFilter switch
{
"Amount1" when mathOperator == MathOperator.LessThanOrEqual => query.Where(x => x.Amount1 <= value),
"Amount1" when mathOperator == MathOperator.GreaterThanOrEqual => query.Where(x => x.Amount1 >= value),
"Amount2" when mathOperator == MathOperator.LessThanOrEqual => query.Where(x => x.Amount2 <= value),
"Amount2" when mathOperator == MathOperator.GreaterThanOrEqual => query.Where(x => x.Amount2 >= value),
_ => throw new ArgumentException($"不支持 {propertyToFilter} 列作为数字列查询", nameof(propertyToFilter))
};
return query.ToListAsync();
}
如果固定只有一两个数字列且将来也不会再扩展,这样写简单粗暴,也没什么问题。
但如果有几十个数字列,这样使用 swith
模式匹配的写法就太恐怖了,代码大量重复。很自然地,我们得想办法根据属性名动态创建 Where
方法的参数。它的参数类型是:Expression
,是一个表达式参数。
要知道如何动态创建一个类似 Expression
类型的表达式实例,就要知道如何拆解表达式树。
对于本示例,以 x => x.Amount1 <= value
表达式实例为例,它的表达式树是这样的:
然后我们可以按照此表达式树结构来构建我们的 LINQ 表达式:
public Task> GetProductsAsyncV2(string propertyToFilter, MathOperator mathOperator, decimal value)
{
var query = _context.Products.AsNoTracking();
var paramExp = Expression.Parameter(typeof(Product));
var memberExp = Expression.PropertyOrField(paramExp, propertyToFilter);
var valueExp = Expression.Constant(value);
var compareExp = mathOperator == MathOperator.LessThanOrEqual ?
Expression.LessThanOrEqual(memberExp, valueExp) :
Expression.GreaterThanOrEqual(memberExp, valueExp);
var lambda = Expression.Lambdabool>>(compareExp, paramExp);
return query.Where(lambda).ToListAsync();
}
每个 Expression.XXX
静态方法返回的都是一个以 Expression
为基类的实例,代表一个表达式。不同的表达式又可以组成一个新的表达式,直到得到我们需要的 Lambda 表达式。这样就形成了一种树形结构,我们称为表达式树。知道如何把一个最终的查询表达式拆解成表达式树,我们就容易动态构建此查询表达式。
得到一个表达式后,我们还可以动态编译并调用该表达式,比如上面示例得到的 lambda
变量,是一个Expression
类型,调用其 Compile
方法,可以得到 Func
类型的委托。
...
var toTestProduct = new Product { Amount1 = 100, Amount2 = 200 };
Funcbool> func = lambda.Compile();
var result = func(toTestProduct);
Console.WriteLine($"The product's {propertyToFilter} is to {mathOperator} {value}.");
// Output: The product's Amount1 is LessThanOrEqual to 150.
你可以通过研究 Expression
类来了解更多动态构建表达式的方法。
动态构建 LINQ 表达式对于不能在编译时建立查询,只能在运行时建立查询的场景很有用。但它的缺点也很明显,不易维护、不易阅读、不易调试。如果最终的表达式执行出错,很难通过调试来发现具体是构建中的那一步写错了,只能凭自己的理解和经验查找错误。所以,如非必须,一般不推荐动态构建 LINQ 查询表达式。
.NET Core实战项目之CMS 第一章 入门篇-开篇及总体规划
【.NET Core微服务实战-统一身份认证】开篇及目录索引
Redis基本使用及百亿数据量中的使用技巧分享(附视频地址及观看指南)
.NET Core中的一个接口多种实现的依赖注入与动态选择看这篇就够了
用abp vNext快速开发Quartz.NET定时任务管理界面