C#动态表达式树应用,在使用workflow-core开源框架的时候,发现加载json流程模板就使用了这个技术

67 阅读6分钟

在使用workflow-core开源框架的时候,发现加载json流程模板就使用了这个技术。了解了一下,比较强大,不过阅读起来和书写起来没那么直观。配合ai整理了一下相关概念,方便学习。 C# 中的动态表达式树是一项强大的元编程特性,它允许你将代码逻辑表示为可遍历和操作的数据结构,并在运行时动态生成和编译代码。下面的表格汇总了其核心应用场景,帮助你快速了解它能解决什么问题。

应用场景核心要解决的问题关键技术手段
动态查询构建应对用户界面(UI)上复杂且多变的筛选、排序需求,避免硬编码大量if/else语句。动态拼接 WhereOrderBy等查询条件的表达式树节点。
高性能反射替代解决传统反射(如 PropertyInfo.GetValue/MethodInfo.Invoke)在频繁调用时性能开销大的问题。将属性访问、方法调用等逻辑编译成强类型委托并缓存。
可配置业务规则引擎将易变的业务规则(如折扣策略)从代码中解耦,实现无需重新部署即可修改业务逻辑。将文本或配置规则解析并组合成表达式树,编译执行。
ORM 框架查询转换将 C# 的 LINQ 查询(如 p => p.Age > 18)转换为底层数据库的 SQL 语句。LINQ 提供程序(如 Entity Framework)解析表达式树结构并翻译成 SQL。
动态代码生成与 AOP在运行时生成全新的类或方法,或在现有方法执行前后注入通用逻辑(如日志、性能监控)。使用 Expression.Block等构建复杂逻辑块,实现方法拦截或动态类型创建。

💻 动态查询构建

在桌面应用或数据管理系统中,经常需要让用户通过界面自由组合查询条件(例如,“查找所有价格高于100元且库存大于10的商品”)。如果为每种可能的组合都编写硬代码,将导致代码臃肿且难以维护。

表达式树允许你像拼接“乐高积木”一样,根据用户的选择动态构建查询逻辑。基本步骤通常包括:

  1. 定义参数​:创建代表数据实体的参数表达式,如 ParameterExpression productParam = Expression.Parameter(typeof(Product), "p");
  2. 构建条件单元​:为每个筛选条件(如“价格高于100”)构建相应的二元表达式,例如 Expression.GreaterThan(Expression.Property(productParam, "Price"), Expression.Constant(100))
  3. 组合条件​:根据用户选择的逻辑运算符(“并且”、“或者”),使用 Expression.AndAlsoExpression.OrElse方法将多个条件表达式连接起来。
  4. 编译与执行​:将最终组合好的表达式树编译成 Func<T, bool>类型的委托,并应用于 IQueryable数据源进行查询。

这种方式不仅灵活,而且通过与 IQueryable接口配合,可以被 Entity Framework 等 ORM 转换为高效的 SQL 语句,避免了从数据库拉取全部数据再到内存中过滤的性能损耗。

⚡ 高性能反射替代

反射是 C# 中强大的机制,但频繁使用 MethodInfo.InvokePropertyInfo.GetValue/SetValue会带来显著的性能开销,因为每次调用都需要进行参数验证、装箱拆箱等操作。

表达式树提供了一种“编译一次,运行万次”的高性能解决方案。其核心思想是:​通过表达式树生成一个强类型的委托,并将这个委托缓存起来重复使用。这样,动态调用的开销就从每次调用时的反射开销,变为了一次性的编译开销,后续每次执行的性能都接近直接编写的原生代码。

例如,需要动态访问一个对象的某个属性时,可以这样做:

// 传统反射方式,性能较差
var value = propertyInfo.GetValue(myObject);

// 使用表达式树优化
// 1. 构建表达式树: (obj) => ((MyObject)obj).PropertyName
ParameterExpression param = Expression.Parameter(typeof(object), "obj");
MemberExpression propertyAccess = Expression.Property(Expression.Convert(param, typeof(MyObject)), "PropertyName");
Func<object, object> getter = Expression.Lambda<Func<object, object>>(propertyAccess, param).Compile();

// 2. 缓存 getter 委托,后续可高效调用
var fastValue = getter(myObject);

这种方法在框架开发中非常常见,例如一些序列化库或映射工具(如 AutoMapper)内部就采用类似技术来提升性能。

⚙️ 可配置业务规则引擎

在企业级应用中,业务规则(如“如果客户是VIP且订单金额超过500元,则享受95折”)经常变化。如果将这些规则硬编码在程序里,每次修改都需要开发人员介入、重新编译和部署整个应用。

利用表达式树,可以构建一个灵活的可配置规则引擎。

  1. 规则配置​:业务人员可以通过 UI 界面或配置文件(如 JSON/XML)来定义规则。
  2. 规则解析与编译​:程序在启动时或检测到配置变更时,将这些文本规则解析成表达式树结构,并编译成委托。例如,将规则“Order.Total > 500”解析为 Expression.GreaterThan(Expression.Property(orderParam, "Total"), Expression.Constant(500))
  3. 执行与缓存​:当需要评估规则时,直接调用编译好的委托,传入业务对象(如订单)即可得到结果。编译后的委托会被缓存,确保高效执行。

这使得应用程序能够快速响应业务变化,实现了业务逻辑与核心代码的解耦。

🛠️ 进阶应用场景

除了上述常见场景,表达式树还有一些更高级的用途:

  • ORM 框架的基石​:像 Entity Framework 和 Dapper 这样的 ORM 库,其核心功能就是将 LINQ 查询转换为 SQL。它们通过解析你编写的 Lambda 表达式(本质上是表达式树)的结构,理解你的查询意图,然后生成针对特定数据库的 SQL 命令。没有表达式树,这种“语言集成查询”的能力就无法实现。
  • 动态代码生成与 AOP​:使用 Expression.Block可以创建包含变量声明和多个语句的代码块,从而实现更复杂的动态逻辑。这可以用于在运行时动态创建新的类型,或者实现面向切面编程(AOP),例如在不修改原有代码的情况下,为方法动态添加日志记录、性能监控或事务管理等横切关注点。

💡 使用建议与注意事项

虽然表达式树功能强大,但在使用时也需要考虑以下几点:

  • 性能权衡​:表达式树的编译过程(Compile方法)有一定开销。因此,最佳实践是将编译后的委托缓存起来,避免重复编译,这样才能发挥其性能优势。
  • 代码复杂度​:手动构建表达式树的 API 相对底层和复杂,代码可读性不如普通的 C# 代码。建议仅在确实需要动态生成的场景下使用,对于静态逻辑,直接编写 Lambda 表达式即可。
  • 调试难度​:调试动态生成的表达式树比调试静态代码要困难。

希望这份详细的场景介绍能帮助你更好地理解和运用 C# 的动态表达式树。如果你对某个具体场景的实现细节有进一步的兴趣,我们可以继续深入探讨。