C#动态构建表达式树(三)——表达式的组合

前言

在筛选数据的过程中,可能会有这样的情况:有一些查询条件是公共的,但是根据具体的传入参数可能需要再额外增加一个条件。对于这种问题一般有两种方法:

a. 在 Where 后再组合一个 Where,如:

  1. List<SOME_CLASS> dataList = dataList.Where(FILTER_1).Where(FILTER_2).ToList();

b. 将类型相同两个表达式组合起来(就是本文的主题了)

由于项目中既有框架的封装,查询时只能传入 Expression<Func<T, bool>> 类型的值,因此只能采用 b 方法。

最终

先给出研究后最后得出的方法,再分享自己踩坑的过程(仅以 And 操作为例,其它的大同小异)。

1. 代码

  1. public static Expression<Func<T, bool>> CombineLambda<T>(List<Expression<Func<T, bool>>> lambdas, List<Type> types)
  2. {
  3. if(typeof(T) != types[0])
  4. {
  5. throw new Exception("类型列表的第一个值必须为泛型 T 的类型");
  6. }
  7. List<ParameterExpression> parameterExpressions = new List<ParameterExpression>();
  8. // 从 'A' 开始,为每个类型指定一个形参
  9. for (int i = 0; i < types.Count; i++)
  10. {
  11. parameterExpressions.Add(Expression.Parameter(types[i], ((char)(65 + i)).ToString()));
  12. }
  13. CombineExpressionVisitor visitor = new CombineExpressionVisitor(parameterExpressions);
  14. // 新建一个原始条件 a => true,并逐个组合数组中的条件
  15. Expression<Func<T, bool>> result = Expression.Lambda<Func<T, bool>>(
  16. Expression.Constant(true), parameterExpressions[0]);
  17. for (int i = 0; i < lambdas.Count; i++)
  18. {
  19. Expression tmp = visitor.Visit(result.Body);
  20. Expression tmp1 = visitor.Visit(lambdas[i].Body);
  21. result = Expression.Lambda<Func<T, bool>>(Expression.AndAlso(tmp, tmp1), parameterExpressions[0]);
  22. }
  23. return result;
  24. /*
  25. ** 上面的 type[0](parameterExpressions[0]) 可能比较费解。由于我们的情况是嵌套的对
  26. ** 象,因此需要传入 “外层对象类型的 Paramter” 和 “内层对象类型的 Parameter”,
  27. ** parameterExpressions[0] 表示的是 外层对象类型的 Parameter,由它开始组成** 整个的筛选
  28. */
  29. }
  30. private class CombineExpressionVisitor : ExpressionVisitor
  31. {
  32. private List<ParameterExpression> peList;
  33. public CombineExpressionVisitor(List<ParameterExpression> list)
  34. {
  35. peList = list;
  36. }
  37. // 覆写 VisitParameter 方法,返回我们指定的统一 Parameter
  38. protected override Expression VisitParameter(ParameterExpression p)
  39. {
  40. return peList.Find(a => a.Type == p.Type);
  41. }
  42. public override Expression Visit(Expression node)
  43. {
  44. return base.Visit(node);
  45. }
  46. }

Lambda表达式由参数(Parameter)内容(Body)组成。主要的思路是 将给出的所有 Lambda 表达式的 参数(Parameter) 都改为 同一个参数(Parameter),再把内容(Body)组合起来

2. 验证

假设我们有这样的数据:

  1. [
  2. {
  3. "Name": "安柏",
  4. "Age": 25,
  5. "Weapons": [
  6. {
  7. "Name": "普通弓",
  8. "GetTime": "2021-05-03T12:00:00+08:00",
  9. "Description": "入门级武器,从无到有"
  10. }
  11. ]
  12. },
  13. {
  14. "Name": "行秋",
  15. "Age": 18,
  16. "Weapons": [
  17. {
  18. "Name": "单手剑",
  19. "GetTime": "2021-05-22T11:00:00+08:00",
  20. "Description": "刚刚够用的武器"
  21. }
  22. ]
  23. },
  24. {
  25. "Name": "可莉",
  26. "Age": 8,
  27. "Weapons": [
  28. {
  29. "Name": "嘟嘟可故事集",
  30. "GetTime": "2021-06-20T15:35:00+08:00",
  31. "Description": "特别合适的武器"
  32. },
  33. {
  34. "Name": "简单的书",
  35. "GetTime": "2021-05-03T16:47:00+08:00",
  36. "Description": "赠送的武器"
  37. }
  38. ]
  39. }
  40. ]

(原来你也玩原船,,,哦不是,原神啊)。主要有 “角色”“年龄”(我瞎编的)、“武器”(半真半编的)这几个字段,其中 “武器”是单独一个类,定义如下:

  1. /// <summary>
  2. /// 原神角色
  3. /// </summary>
  4. public class YuanshenRole
  5. {
  6. /// <summary>
  7. /// 名字
  8. /// </summary>
  9. public string Name { get; set; }
  10. /// <summary>
  11. /// 年龄
  12. /// </summary>
  13. public int Age { get; set; }
  14. /// <summary>
  15. /// 所拥有的武器
  16. /// </summary>
  17. public List<Weapon> Weapons { get; set; }
  18. }
  19. /// <summary>
  20. /// 武器
  21. /// </summary>
  22. public class Weapon
  23. {
  24. /// <summary>
  25. /// 名称
  26. /// </summary>
  27. public string Name { get; set; }
  28. /// <summary>
  29. /// 获得的时间
  30. /// </summary>
  31. public DateTime GetTime { get; set; }
  32. /// <summary>
  33. /// 描述
  34. /// </summary>
  35. public string Description { get; set; }
  36. }

我们要查找 Age 为 8, Name 为“可莉”,Weapons中存在 “嘟嘟可故事集” 的角色,分成两个表达式写,并组合起来:

  1. // 三个单独的条件
  2. Expression<Func<YuanshenRole, bool>> filter2 = x => x.Age == 8;
  3. Expression<Func<YuanshenRole, bool>> filter1 = x => x.Name == 可莉";
  4. Expression<Func<YuanshenRole, bool>> filter = x => x.Weapons.Any(y => y.Name == "嘟嘟可故事集");
  5. // 组合起来
  6. var finalFilter = CombineLambda(
  7. new List<Expression<Func<YuanshenRole, bool>>> { filter, filter1, filter2 },
  8. new List<Type> { typeof(YuanshenRole), typeof(Weapon) }
  9. );

CombineLambda 方法需要两个参数,一个是 表达式的集合,另一个是 表达式中 Parameter 类型的集合。

踩坑过程

1. 只拼接Lambda表达式的Body,不改变其参数

说起拼接,我的第一想法是这样的:

  1. // 错误的写法
  2. Expression<Func<YuanshenRole, bool>> finalFilter = Expression.Lambda<Func<YuanshenRole, bool>>(
  3. Expression.AndAlso(filter.Body, filter2.Body), // 即使取了表达式的 Body,意义也不对
  4. Expression.Parameter(typeof(YuanshenRole), "x")
  5. );

报错为:

从作用域“”引用了“UsageDemo.Program+YuanshenRole”类型的变量“x”,但该变量未定义”

在查找资料的时候看到了一个比喻:每个 Lambda 表达式就像是一个独轮车,把它们拼起来就相当于把两个独轮车拼成一个自行车。而上述的过程,我们看似把两个轮子拆下来,安装在了同一个车架子上,却没有改造两个轮子让其适应新的车架子(即,改变 “原有表达式 Body” 中引用 “原来Parameter” 的部分)

自然而然地,我们需要进行一番深入的改造。搜索网上的资料后发现,可以通过继承 ExpressionVisitor 类并覆写其中的部分方法,如下:

  1. class MyExpressionVisitor : ExpressionVisitor
  2. {
  3. public ParameterExpression _Parameter0 { get; set; }
  4. public MyExpressionVisitor(ParameterExpression Parameter0)
  5. {
  6. _Parameter0 = Parameter0;
  7. }
  8. protected override Expression VisitParameter(ParameterExpression p)
  9. {
  10. return _Parameter0;
  11. }
  12. public override Expression Visit(Expression node)
  13. {
  14. return base.Visit(node);
  15. }
  16. }

VisitParameter 方法在 Visit 时默认返回原 Parameter,通过覆写它可以达到改造 Body 中引用 “原来Parameter” 的效果

ExpressionVisitor 中的 Visit 方法感觉与常规方法思路完全不同,先挖个坑,以后再研究

顺理成章地,踩了第二个坑QAQ

2. 只传入了 1 个 Parameter,而实际需要 2 个

根据踩坑1,写法应该为:

  1. Expression<Func<YuanshenRole, bool>> filter = x => x.Weapons.Any(y => y.Name == "嘟嘟可故事集");
  2. Expression<Func<YuanshenRole, bool>> filter2 = x => x.Age == 8;
  3. ParameterExpression pe = Expression.Parameter(typeof(YuanshenRole), "x");
  4. var visitor1 = new MyExpressionVisitor(pe);
  5. Expression bodyone1 = visitor1.Visit(filter.Body); // 此处需要调用我们自定类的 Visit 方法改造原 Parameter
  6. Expression bodytwo1 = visitor1.Visit(filter2.Body);
  7. Expression<Func<YuanshenRole, bool>> finalFilter =
  8. ExpressionLambda<Func<YuanshenRole, bool>>(Expression.AndAlso(bodyone1,bodytwo1), pe);
  9. dataList = dataList.AsQueryable().Where(finalFilter).ToList();

结果报错为:

没有为类型“UsageDemo.Program+YuanshenRole”定义属性“System.String Name””

发生甚么事了,我们的 filter 也没有获取 YuanshenRole 的 Name 属性啊。此处我确实愣了一下,然后发现其实 查找的是 Weapon 类型的 Name,而传入的是 YuanshenRole 类型的参数

因此在覆写的 Visit 方法中不能只返回一个类型,而要根据实际情况返回。稍加封装就有了文本最开始的代码。

  1. private class CombineExpressionVisitor : ExpressionVisitor
  2. {
  3. ......
  4. public CombineExpressionVisitor(List<ParameterExpression> list)
  5. {
  6. peList = list;
  7. }
  8. ......

这也是为什么 Visit 类的构造函数要传入 List<ParameterExpression> 的原因。

后记

最一开始我觉得合并两个表达式应该是个很简单的操作,可能一个方法就搞定了,没想到他不讲武德,让我搞了这么长时间。我大 E 了啊,没有闪。希望这些代码以后耗子喂汁,不要再搞这样的聪明,小聪明啊,谢谢朋友们!

参考

LINQ系列(7)——表达式树之EXPRESSIONVISITOR

合并两个 Lambda 表达式(此文中还介绍了通过 Invoke 方法来达到上述目的,但不适用于 IQueryable 类型的操作)

C#中合并两个lambda表达式

C#中利用Expression表达式树进行多个Lambda表达式合并

C# 知识回顾 - 表达式树 Expression Trees

C#动态构建表达式树(三)——表达式的组合的更多相关文章

  1. 【C# 表达式树 三】ExpressionType 节点类型种类

    // // 摘要: // 描述表达式目录树的节点的节点类型. public enum ExpressionType { // // 摘要: // 加法运算,如 a + b,针对数值操作数,不进行溢出检 ...

  2. LinQ实战学习笔记(三) 序列,查询操作符,查询表达式,表达式树

    序列 延迟查询执行 查询操作符 查询表达式 表达式树 (一) 序列 先上一段代码, 这段代码使用扩展方法实现下面的要求: 取进程列表,进行过滤(取大于10M的进程) 列表进行排序(按内存占用) 只保留 ...

  3. [.net 面向对象程序设计进阶] (7) Lamda表达式(三) 表达式树高级应用

    [.net 面向对象程序设计进阶] (7) Lamda表达式(三) 表达式树高级应用 本节导读:讨论了表达式树的定义和解析之后,我们知道了表达式树就是并非可执行代码,而是将表达式对象化后的数据结构.是 ...

  4. 【C#表达式树 开篇】 Expression Tree - 动态语言

    .NET 3.5中新增的表达式树(Expression Tree)特性,第一次在.NET平台中引入了"逻辑即数据"的概念.也就是说,我们可以在代码里使用高级语言的形式编写一段逻辑, ...

  5. 深入学习C#匿名函数、委托、Lambda表达式、表达式树类型——Expression tree types

    匿名函数 匿名函数(Anonymous Function)是表示“内联”方法定义的表达式.匿名函数本身及其内部没有值或者类型,但是可以转换为兼容的委托或者表达式树类型(了解详情).匿名函数转换的计算取 ...

  6. C#特性-表达式树

    表达式树ExpressionTree   表达式树基础 转载需注明出处:http://www.cnblogs.com/tianfan/ 刚接触LINQ的人往往觉得表达式树很不容易理解.通过这篇文章我希 ...

  7. C#编程(六十六)----------表达式树总结

    表达式树总结 基础 表达式树提供了一个将可执行代码转换成数据的方法.如果你要在执行代码之前修改或转换此代码,那么它是很有用的.有其是当你要将C#代码----如LINQ查询表达式转换成其他代码在另一个程 ...

  8. C#高级编程六十六天----表达式树总结【转】

    https://blog.csdn.net/shanyongxu/article/details/47257139 表达式树总结 基础 表达式树提供了一个将可执行代码转换成数据的方法.如果你要在执行代 ...

  9. .NET Core表达式树的梳理

    最近要重写公司自己开发的ORM框架:其中有一部分就是查询的动态表达式:于是对这方面的东西做了一个简单的梳理 官网的解释: 表达式树以树形数据结构表示代码,其中每一个节点都是一种表达式,比如方法调用和  ...

  10. 追根溯源之Linq与表达式树

    一.什么是表达式树?   首先来看下官方定义(以下摘录自巨硬官方文档)   表达式树表示树状数据结构中的代码,其中每个节点都是表达式,例如,方法调用或诸如的二进制操作x < y.   您可以编译 ...

随机推荐

  1. ReentrantLock 中的 4 个坑!

    JDK 1.5 之前 synchronized 的性能是比较低的,但在 JDK 1.5 中,官方推出一个重量级功能 Lock,一举改变了 Java 中锁的格局.JDK 1.5 之前当我们谈到锁时,只能 ...

  2. [TensorFow2.0]-MNIST手写数字识别

    本人人工智能初学者,现在在学习TensorFlow2.0,对一些学习内容做一下笔记.笔记中,有些内容理解可能较为肤浅.有偏差等,各位在阅读时如有发现问题,请评论或者邮箱(右侧边栏有邮箱地址)提醒. 若 ...

  3. DL基础补全计划(六)---卷积和池化

    PS:要转载请注明出处,本人版权所有. PS: 这个只是基于<我自己>的理解, 如果和你的原则及想法相冲突,请谅解,勿喷. 前置说明   本文作为本人csdn blog的主站的备份.(Bl ...

  4. .net core 响应的json数据驼峰显示问题。

    在.net core webapi中,默认响应的json数据是以驼峰显示的,即首字母小写的方式.如果让其正常显示,只需要在全局配置即可.代码如下图: 配置之后,响应数据就不会再以驼峰的形式展示了.而是 ...

  5. remote: Support for password authentication was removed

    周末提交代码,把代码push到github上,控制台报了下面的错误: remote: Support for password authentication was removed on August ...

  6. 基于ssm的电影售票选座管理系统基于Java的电影网站的网页设计与制作源码

    注意:此项目只截图部分功能,可评论区咨询查看项目全部功能演示! 1.开发环境 开发语言: 后台框架:SSM(Spring+SpringMVC+Mybatis) 前端技术:HTML+CSS+JavaSc ...

  7. windows中抓取hash小结(上)

    我上篇随笔说到了内网中横向移动的几种姿势,横向移动的前提是获取了具有某些权限的用户的明文密码或hash,正愁不知道写点啥,那就来整理一下这个"前提"-----如何在windows系 ...

  8. DVWA靶场之File Inclusion(文件包含)通关

    文件包含,未经过严格过滤,将一些恶意构造带入了包含函数,可以使用一些包含函数来包含一些其他乱七八糟的东西,要么导致任意文件读取,要么命令执行 文件包含包括远程文件包含(RFI)和本地文件包含(LFI) ...

  9. 题解 Connect

    传送门 各种骗分无果,特殊性质还手残写挂了-- 首先完全图上直接输出边权 \(\times (n-2)\) 就行了,然而我脑残乘的 \(n-1\) 看数据范围肯定是状压,但是压边肯定炸了,考虑压点 因 ...

  10. FPGA学习过程(二)

    项目:数码管动态显示时间 首先建立一个计时一秒的模块,作为数码管显示的需要 module timer_s( input wire clk, input wire rst_n, output wire ...