表达式树(Expression Trees)-动态构建表达式树

  • ~20.45K 字
  1. 1. 代码动态构建表达式树
    1. 1.1. 构建表达式树常用工厂方法
    2. 1.2. 用ExpressionTreeToString简化动态构建表达式树
  2. 2. 让构建”动态”起来
    1. 2.1. 场景示例:动态查询条件
    2. 2.2. 实现动态表达式构建器
    3. 2.3. 使用动态构建的表达式
    4. 2.4. 移除冗余的 True 条件
    5. 2.5. 动态构建的优势总结
    6. 2.6. 性能注意事项

上篇post介绍了表达式树,本节介绍如何动态构建表达式树。

代码动态构建表达式树

表达式树(Expression Trees)这一文章中我们编写的代码都是让C#编译器把Lambda表达式转换表达式树,这些表达式是硬编码的。

我们通过代码编写表达式:Expression<Func<Book,bool>> e = b =>b.Price > 5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 手动构建:b => b.Price > 5
static Expression<Func<Book, bool>> BuildExpressionManually()
{
// 1. 创建参数节点:b(类型Book)
ParameterExpression paramB = Expression.Parameter(typeof(Book), "b");

// 2. 创建左子节点:b.Price
MemberExpression priceMember = Expression.MakeMemberAccess(paramB, typeof(Book).GetProperty("Price"));

// 3. 创建右子节点:常量5
ConstantExpression constant5 = Expression.Constant(5m, typeof(decimal));

// 4. 创建主体节点:b.Price > 5(大于运算)
BinaryExpression greaterThanExpr = Expression.GreaterThan(
left: priceMember, // 左表达式:b.Price
right: constant5 // 右表达式:5m
);

// 5. 创建Lambda表达式:b => (b.Price > 5)
Expression<Func<Book, bool>> e = Expression.Lambda<Func<Book, bool>>(
body: greaterThanExpr, // Lambda的主体
parameters: paramB // Lambda的参数(b)
);

return e;
}

class Book
{
public string Title { get; set; }
public decimal Price { get; set; }
}

//验证1
private static void ConstructExpressions()
{
var e = BuildExpressionManually();
Console.WriteLine($"手动构建的表达式:{e}");
}

//验证2
private static void UseBuildExpressionManually()
{
List<Book> books = new List<Book>()
{
new Book { Title = "C# 入门", Price = 6 },
new Book { Title = "LINQ 实战", Price = 2 },
new Book { Title = "数据结构与算法", Price = 8 },
};

var e = BuildExpressionManually();

// 编译表达式树为委托
var predicate = e.Compile();

var b = books.Where(predicate);

foreach (var book in b)
{
Console.WriteLine($"符合条件的图书:{book.Title},价格:{book.Price}");
}
}

构建表达式树常用工厂方法

Expression工厂方法
  1. 基础节点构建(参数、常量、默认值)

    工厂方法 功能描述 示例代码 节点类型
    Expression.Parameter 创建参数节点(Lambda 形参) var b = Expression.Parameter(typeof(Book), "b"); ParameterExpression
    Expression.Constant 创建常量节点(值、字面量) var five = Expression.Constant(5m, typeof(decimal)); ConstantExpression
    Expression.Default 创建类型默认值 var defaultBook = Expression.Default(typeof(Book)); DefaultExpression
  2. 成员访问(属性 / 字段)

    工厂方法 功能描述 示例代码 节点类型
    Expression.Property 访问属性 var priceProp = Expression.Property(b, nameof(Book.Price)); MemberExpression
    Expression.PropertyOrField 统一属性或字段访问 var titleMember = Expression.PropertyOrField(b, "Title"); MemberExpression
    Expression.Field 访问字段 var authorField = Expression.Field(b, "_author"); MemberExpression
  3. 二元运算(算术 / 比较 / 逻辑)

    工厂方法 功能 示例 ExpressionType
    Expression.Add 加法 Expression.Add(priceProp, five) Add
    Expression.Subtract 减法 Expression.Subtract(priceProp, five) Subtract
    Expression.GreaterThan 大于 Expression.GreaterThan(priceProp, five) GreaterThan
    Expression.Equal 等于 Expression.Equal(Expression.Property(b,"Title"), Expression.Constant("C#入门")) Equal
    Expression.AndAlso 逻辑与(短路) Expression.AndAlso(cond1, cond2) AndAlso
    Expression.OrElse 逻辑或(短路) Expression.OrElse(cond1, cond2) OrElse
    Expression.MakeBinary 通用入口 Expression.MakeBinary(ExpressionType.GreaterThanOrEqual, priceProp, five) 任意二元

    组合示例:

    1
    2
    3
    4
    5
    6
    var b = Expression.Parameter(typeof(Book), "b");
    var priceProp = Expression.Property(b, nameof(Book.Price));
    var five = Expression.Constant(5m);
    var cond1 = Expression.GreaterThan(priceProp, five);
    var cond2 = Expression.Equal(Expression.Property(b, "Title"), Expression.Constant("C#"));
    var andExpr = Expression.AndAlso(cond1, cond2);
  4. Lambda 表达式包装

    工厂方法 功能描述 示例 返回类型
    Expression.Lambda<TDelegate> 推荐用法 Expression<Func<Book,bool>> e = Expression.Lambda<Func<Book,bool>>(cond1, b); Expression<Func<Book,bool>>
    Expression.Lambda 非泛型 LambdaExpression raw = Expression.Lambda(cond1, b); LambdaExpression
  5. 方法调用(实例 / 静态)

    工厂方法 功能 示例(实例 / 静态) 节点类型
    Expression.Call 调用方法 见示例 MethodCallExpression
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var b = Expression.Parameter(typeof(Book), "b");
    var titleProp = Expression.Property(b, nameof(Book.Title));

    // 实例方法 b.Title.Contains("C#")
    var containsMethod = typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) });
    var containsCall = Expression.Call(titleProp, containsMethod, Expression.Constant("C#"));

    // 静态方法 string.IsNullOrEmpty(b.Title)
    var isNullOrEmptyMethod = typeof(string).GetMethod(nameof(string.IsNullOrEmpty), new[] { typeof(string) });
    var nullCheckCall = Expression.Call(isNullOrEmptyMethod, titleProp);
  6. 一元运算与类型转换

    工厂方法 功能 示例 节点类型
    Expression.Convert 强制转换 Expression.Convert(Expression.Constant(5), typeof(decimal)); UnaryExpression
    Expression.Not 逻辑非 Expression.Not(cond1) UnaryExpression
    Expression.TypeAs 安全转换 Expression.TypeAs(Expression.Parameter(typeof(object),"obj"), typeof(Book)); UnaryExpression
  7. 对象与集合创建

    工厂方法 功能 示例 节点类型
    Expression.New 构造对象 有 / 无参 NewExpression
    Expression.NewArrayInit 创建数组并初始化 Expression.NewArrayInit(typeof(string), Expression.Constant("A"), Expression.Constant("B")); NewArrayExpression
    Expression.ArrayIndex 数组索引访问 Expression.ArrayIndex(arrParam, Expression.Constant(0)); BinaryExpression

    有参构造示例:

    1
    2
    3
    4
    var titleConst = Expression.Constant("C#进阶");
    var priceConst = Expression.Constant(79.9m);
    var ctor = typeof(Book).GetConstructor(new[] { typeof(string), typeof(decimal) });
    var newBookExpr = Expression.New(ctor, titleConst, priceConst);

    数组访问示例:

    1
    2
    var arrParam = Expression.Parameter(typeof(string[]), "arr");
    var first = Expression.ArrayIndex(arrParam, Expression.Constant(0));

用ExpressionTreeToString简化动态构建表达式树

ExpressionTreeToString提供了ToString(“Factory methods”,”C#”),执行会输出类似工厂方法生成表达式树的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Expression<Func<Book, bool>> e = b => b.Price > 5;
Console.WriteLine(e.ToString("Factory methods", "C#"));
/* 输出
// using static System.Linq.Expressions.Expression

var b = Parameter(
typeof(Book),
"b"
);

Lambda(
MakeBinary(ExpressionType.GreaterThan,
MakeMemberAccess(b,
typeof(Book).GetProperty("Price")
),
Constant(5), false,
typeof(decimal).GetMethod("op_GreaterThan")
),
b
)
*/

通过ToString("Factory methods","C#")输出动态构建表达式树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using ExpressionTreeToString;
using System.Linq.Expressions;
using static System.Linq.Expressions.Expression;

var b = Parameter(typeof(Book),"b");

var e = Lambda(
MakeBinary(ExpressionType.GreaterThan,
MakeMemberAccess(b,
typeof(Book).GetProperty("Price")
),
Constant(5m), false,
typeof(decimal).GetMethod("op_GreaterThan")
),
b
);

Console.WriteLine(e);//b => (b.Price > 5)

让构建”动态”起来

上面代码动态构建出了表达式树,但逻辑是固定的。既然逻辑是固定的,为什么不直接硬编码Lambda表达式?动态构建表达式树的价值就在于,运行时根据条件的不同生成不同的表达式树。

场景示例:动态查询条件

假设有一个图书检索系统,用户可以选择多个筛选条件:

1
2
3
4
5
6
7
class BookSearchCriteria
{
public decimal? MinPrice { get; set; }
public decimal? MaxPrice { get; set; }
public string TitleKeyword { get; set; }
public bool? OnlyInStock { get; set; }
}

实现动态表达式构建器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
static Expression<Func<Book, bool>> BuildDynamicPredicate(BookSearchCriteria criteria)
{
var param = Expression.Parameter(typeof(Book), "b");
Expression body = Expression.Constant(true); // 初始条件:恒真

// 动态添加最低价格条件
if (criteria.MinPrice.HasValue)
{
var priceProp = Expression.Property(param, nameof(Book.Price));
var minPriceConst = Expression.Constant(criteria.MinPrice.Value);
var condition = Expression.GreaterThanOrEqual(priceProp, minPriceConst);
body = Expression.AndAlso(body, condition);
}

// 动态添加最高价格条件
if (criteria.MaxPrice.HasValue)
{
var priceProp = Expression.Property(param, nameof(Book.Price));
var maxPriceConst = Expression.Constant(criteria.MaxPrice.Value);
var condition = Expression.LessThanOrEqual(priceProp, maxPriceConst);
body = Expression.AndAlso(body, condition);
}

// 动态添加标题关键词条件
if (!string.IsNullOrEmpty(criteria.TitleKeyword))
{
var titleProp = Expression.Property(param, nameof(Book.Title));
var keywordConst = Expression.Constant(criteria.TitleKeyword);
var containsMethod = typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) });
var condition = Expression.Call(titleProp, containsMethod, keywordConst);
body = Expression.AndAlso(body, condition);
}

// 动态添加库存条件(假设 Book 有 InStock 属性)
if (criteria.OnlyInStock.HasValue && criteria.OnlyInStock.Value)
{
var inStockProp = Expression.Property(param, "InStock");
var condition = Expression.Equal(inStockProp, Expression.Constant(true));
body = Expression.AndAlso(body, condition);
}

return Expression.Lambda<Func<Book, bool>>(body, param);
}

使用动态构建的表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
static void DynamicQueryExample()
{
var books = new List<Book>
{
new Book { Title = "C# 入门", Price = 45m, InStock = true },
new Book { Title = "LINQ 实战", Price = 55m, InStock = false },
new Book { Title = "C# 高级编程", Price = 89m, InStock = true },
new Book { Title = "数据结构与算法", Price = 65m, InStock = true }
};

// 场景1:只查询价格在 50-100 之间的书
var criteria1 = new BookSearchCriteria
{
MinPrice = 50m,
MaxPrice = 100m
};
var predicate1 = BuildDynamicPredicate(criteria1);
var result1 = books.Where(predicate1.Compile()).ToList();
Console.WriteLine($"场景1 生成的表达式:{predicate1}");
Console.WriteLine($"结果数量:{result1.Count}");
// 输出:
// 场景1 生成的表达式:b => ((True AndAlso (b.Price >= 50)) AndAlso (b.Price <= 100))
// 结果数量:3

// 场景2:查询标题包含"C#"且有库存的书
var criteria2 = new BookSearchCriteria
{
TitleKeyword = "C#",
OnlyInStock = true
};
var predicate2 = BuildDynamicPredicate(criteria2);
var result2 = books.Where(predicate2.Compile()).ToList();
Console.WriteLine($"\n场景2 生成的表达式:{predicate2}");
Console.WriteLine($"结果数量:{result2.Count}");
// 输出:
// 场景2 生成的表达式:b => ((True AndAlso b.Title.Contains("C#")) AndAlso (b.InStock == True))
// 结果数量:2

// 场景3:组合多个条件
var criteria3 = new BookSearchCriteria
{
MinPrice = 40m,
TitleKeyword = "C#",
OnlyInStock = true
};
var predicate3 = BuildDynamicPredicate(criteria3);
var result3 = books.Where(predicate3.Compile()).ToList();
Console.WriteLine($"\n场景3 生成的表达式:{predicate3}");
Console.WriteLine($"结果数量:{result3.Count}");
// 输出:
// 场景3 生成的表达式:b => (((True AndAlso (b.Price >= 40)) AndAlso b.Title.Contains("C#")) AndAlso (b.InStock == True))
// 结果数量:2
}

移除冗余的 True 条件

上面的实现会产生 (True AndAlso ...) 这样的冗余表达式。可以优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
static Expression<Func<Book, bool>> BuildDynamicPredicateOptimized(BookSearchCriteria criteria)
{
var param = Expression.Parameter(typeof(Book), "b");
Expression body = null; // 初始为 null

// 辅助方法:安全添加条件
void AddCondition(Expression condition)
{
body = body == null ? condition : Expression.AndAlso(body, condition);
}

// 动态添加条件
if (criteria.MinPrice.HasValue)
{
var priceProp = Expression.Property(param, nameof(Book.Price));
AddCondition(Expression.GreaterThanOrEqual(priceProp, Expression.Constant(criteria.MinPrice.Value)));
}

if (criteria.MaxPrice.HasValue)
{
var priceProp = Expression.Property(param, nameof(Book.Price));
AddCondition(Expression.LessThanOrEqual(priceProp, Expression.Constant(criteria.MaxPrice.Value)));
}

if (!string.IsNullOrEmpty(criteria.TitleKeyword))
{
var titleProp = Expression.Property(param, nameof(Book.Title));
var containsMethod = typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) });
AddCondition(Expression.Call(titleProp, containsMethod, Expression.Constant(criteria.TitleKeyword)));
}

if (criteria.OnlyInStock == true)
{
var inStockProp = Expression.Property(param, "InStock");
AddCondition(Expression.Equal(inStockProp, Expression.Constant(true)));
}

// 如果没有任何条件,返回恒真表达式
if (body == null)
body = Expression.Constant(true);

return Expression.Lambda<Func<Book, bool>>(body, param);
}

汇总代码(在program.cs里面粘贴下面代码可直接运行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

var books = new List<Book>
{
new Book { Title = "C# 入门", Price = 45m, InStock = true },
new Book { Title = "LINQ 实战", Price = 55m, InStock = false },
new Book { Title = "C# 高级编程", Price = 89m, InStock = true },
new Book { Title = "数据结构与算法", Price = 65m, InStock = true }
};

// 场景1:只查询价格在 50-100 之间的书
var criteria1 = new BookSearchCriteria
{
MinPrice = 50m,
MaxPrice = 100m
};
var predicate1 = BuildDynamicPredicate(criteria1);
var result1 = books.Where(predicate1.Compile()).ToList();
Console.WriteLine($"场景1 生成的表达式:{predicate1}");
Console.WriteLine($"结果数量:{result1.Count}");

// 场景2:查询标题包含"C#"且有库存的书
var criteria2 = new BookSearchCriteria
{
TitleKeyword = "C#",
OnlyInStock = true
};
var predicate2 = BuildDynamicPredicate(criteria2);
var result2 = books.Where(predicate2.Compile()).ToList();
Console.WriteLine($"\n场景2 生成的表达式:{predicate2}");
Console.WriteLine($"结果数量:{result2.Count}");

// 场景3:组合多个条件
var criteria3 = new BookSearchCriteria
{
MinPrice = 40m,
TitleKeyword = "C#",
OnlyInStock = true
};
var predicate3 = BuildDynamicPredicate(criteria3);
var result3 = books.Where(predicate3.Compile()).ToList();
Console.WriteLine($"\n场景3 生成的表达式:{predicate3}");
Console.WriteLine($"结果数量:{result3.Count}");

var predicate4 = BuildDynamicPredicateOptimized(criteria3);
var result4 = books.Where(predicate4.Compile()).ToList();
Console.WriteLine($"\n场景4 生成的表达式:{predicate4}");
Console.WriteLine($"结果数量:{result4.Count}");

static Expression<Func<Book, bool>> BuildDynamicPredicate(BookSearchCriteria criteria)
{
var param = Expression.Parameter(typeof(Book), "b");
Expression body = Expression.Constant(true); // 初始条件:恒真

// 动态添加最低价格条件
if (criteria.MinPrice.HasValue)
{
var priceProp = Expression.Property(param, nameof(Book.Price));
var minPriceConst = Expression.Constant(criteria.MinPrice.Value);
var condition = Expression.GreaterThanOrEqual(priceProp, minPriceConst);
body = Expression.AndAlso(body, condition);
}

// 动态添加最高价格条件
if (criteria.MaxPrice.HasValue)
{
var priceProp = Expression.Property(param, nameof(Book.Price));
var maxPriceConst = Expression.Constant(criteria.MaxPrice.Value);
var condition = Expression.LessThanOrEqual(priceProp, maxPriceConst);
body = Expression.AndAlso(body, condition);
}

// 动态添加标题关键词条件
if (!string.IsNullOrEmpty(criteria.TitleKeyword))
{
var titleProp = Expression.Property(param, nameof(Book.Title));
var keywordConst = Expression.Constant(criteria.TitleKeyword);
var containsMethod = typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) });
var condition = Expression.Call(titleProp, containsMethod, keywordConst);
body = Expression.AndAlso(body, condition);
}

// 动态添加库存条件(假设 Book 有 InStock 属性)
if (criteria.OnlyInStock.HasValue && criteria.OnlyInStock.Value)
{
var inStockProp = Expression.Property(param, "InStock");
var condition = Expression.Equal(inStockProp, Expression.Constant(true));
body = Expression.AndAlso(body, condition);
}

return Expression.Lambda<Func<Book, bool>>(body, param);
}

static Expression<Func<Book, bool>> BuildDynamicPredicateOptimized(BookSearchCriteria criteria)
{
var param = Expression.Parameter(typeof(Book), "b");
Expression body = null; // 初始为 null

// 辅助方法:安全添加条件
void AddCondition(Expression condition)
{
body = body == null ? condition : Expression.AndAlso(body, condition);
}

// 动态添加条件
if (criteria.MinPrice.HasValue)
{
var priceProp = Expression.Property(param, nameof(Book.Price));
AddCondition(Expression.GreaterThanOrEqual(priceProp, Expression.Constant(criteria.MinPrice.Value)));
}

if (criteria.MaxPrice.HasValue)
{
var priceProp = Expression.Property(param, nameof(Book.Price));
AddCondition(Expression.LessThanOrEqual(priceProp, Expression.Constant(criteria.MaxPrice.Value)));
}

if (!string.IsNullOrEmpty(criteria.TitleKeyword))
{
var titleProp = Expression.Property(param, nameof(Book.Title));
var containsMethod = typeof(string).GetMethod(nameof(string.Contains), new[] { typeof(string) });
AddCondition(Expression.Call(titleProp, containsMethod, Expression.Constant(criteria.TitleKeyword)));
}

if (criteria.OnlyInStock == true)
{
var inStockProp = Expression.Property(param, "InStock");
AddCondition(Expression.Equal(inStockProp, Expression.Constant(true)));
}

// 如果没有任何条件,返回恒真表达式
if (body == null)
body = Expression.Constant(true);

return Expression.Lambda<Func<Book, bool>>(body, param);
}

class BookSearchCriteria
{
public decimal? MinPrice { get; set; }
public decimal? MaxPrice { get; set; }
public string TitleKeyword { get; set; }
public bool? OnlyInStock { get; set; }
}

class Book
{
public string Title { get; set; }
public decimal Price { get; set; }
public bool InStock { get; set; }
}

执行dotnet run 输出以下结果

1
2
3
4
5
6
7
8
9
10
11
12
PS D:\source\repo\ConsoleApp1> dotnet run --project "ConsoleApp1"
场景1 生成的表达式:b => ((True AndAlso (b.Price >= 50)) AndAlso (b.Price <= 100))
结果数量:3

场景2 生成的表达式:b => ((True AndAlso b.Title.Contains("C#")) AndAlso (b.InStock == True))
结果数量:2

场景3 生成的表达式:b => (((True AndAlso (b.Price >= 40)) AndAlso b.Title.Contains("C#")) AndAlso (b.InStock == True))
结果数量:2

场景4 生成的表达式:b => (((b.Price >= 40) AndAlso b.Title.Contains("C#")) AndAlso (b.InStock == True))
结果数量:2

动态构建的优势总结

场景 传统方式 动态表达式树
固定条件查询 books.Where(b => b.Price > 50) 无需使用,直接 Lambda 即可
用户可选条件 需要写大量 if-else 拼接 SQL/LINQ 根据条件动态组合表达式
规则引擎 硬编码规则 配置驱动,动态生成规则
权限过滤 在业务层手动过滤 统一在查询层注入权限表达式
低代码平台 无法实现 可视化拖拽转表达式树

性能注意事项

  1. 缓存编译结果:表达式编译 (Compile()) 成本较高

    1
    2
    3
    4
    5
    6
    private static readonly Dictionary<string, Func<Book, bool>> _compiledCache = new();

    var key = predicate.ToString();
    if (!_compiledCache.ContainsKey(key))
    _compiledCache[key] = predicate.Compile();
    var func = _compiledCache[key];
  2. 避免过度使用:简单固定查询直接用 Lambda

  3. EF Core 翻译限制:确保表达式可以被翻译成 SQL(避免调用本地方法)