什么是表达式树

在之前的章节中,我们用委托的形式生成了SQL语句,但是这个生成SQL语句的过程是怎么进行的呢?
在编译原理这门课中会讲到表达式树:
将一个表达式比如b.Price>5,转为一个树,这棵树的根节点是大于号,左节点是成员访问符,右节点是5,根节点的左节点的左节点是b,根节点的左节点的右节点是Price,EFCore可以通过遍历这样的表达式树生成对应的SQL语句。

表达式树(Expression)和委托的不同

我们用Expression<TDelegate>类型表示表达式树,具体代码如下:

1
2
3
4
5
6
7
8
9
static void Main(string[] args) {
Expression<Func<Article, bool>> e1 = a => a.Id > 5;
//Expression<Func<Article,bool>> f2= a => { a.Id < 3};注意,这样的写法不被允许,因为在语句体写法中可能夹杂其他语句导致无法翻译成表达式树
Func<Article,bool> f1=a=>a.Id > 5;
using(var ctx = new MyDbContext()) {
ctx.Articles.Where(e1).ToArray();
ctx.Articles.Where(f1).ToArray();
}
}

通过表达式树生成:

1
2
3
SELECT `t`.`Id`, `t`.`IsDeleted`, `t`.`Message`, `t`.`Title`
FROM `T_Articles` AS `t`
WHERE `t`.`Id` > 5

通过委托生成:

1
2
SELECT `t`.`Id`, `t`.`IsDeleted`, `t`.`Message`, `t`.`Title`
FROM `T_Articles` AS `t`

Expression对象储存了运算逻辑,它把对象逻辑保存成抽象语法树,可以在运行时动态获取运算逻辑,而普通的委托则没有。

通过代码查看表达式树结构

安装ExpressionTreeToString包,它给Expression对象扩展了ToString方法,可以看到所构建的表达式树。

代码展示

源代码:

1
2
3
4
5
6
7
static void Main(string[] args) {
Expression<Func<Article, bool>> e1 = a => a.Id > 5;
Console.WriteLine(e1.ToString("Object notation","C#"));
using(var ctx = new MyDbContext()) {
ctx.Articles.Where(e1).ToArray();
}
}

控制台输出:

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
var a = new ParameterExpression {
Type = typeof(Article),
IsByRef = false,
Name = "a"
};

new Expression<Func<Article, bool>> {
NodeType = ExpressionType.Lambda,
Type = typeof(Func<Article, bool>),
Parameters = new ReadOnlyCollection<ParameterExpression> {
a
},
Body = new BinaryExpression {
NodeType = ExpressionType.GreaterThan,
Type = typeof(bool),
Left = new MemberExpression {
Type = typeof(int),
Expression = a,
Member = typeof(Article).GetProperty("Id")
},
Right = new ConstantExpression {
Type = typeof(int),
Value = 5
}
},
ReturnType = typeof(bool)
}

通过代码动态构造表达式树

创建出每个节点的类型,然后再给它赋值。
ParameterExpression、BinaryExpression、MethodCallExpression、ConstantExpression等类几乎没有提供构造函数,而且所有属性也几乎都是只读的,因此我们一般不会直接创建这些类的实例,而是调用Expression类的Parameter、MakeBinary、Call、Constant等静态方法来生成,这些静态方法我们一般称作创建表达式树的工厂方法,而属性则通过方法参数类设置。

代码展示

1
2
3
4
5
6
7
8
9
10
static void Main(string[] args) {
ParameterExpression paramExprA = Expression.Parameter(typeof(Article), "a");
ConstantExpression constExpr5 = Expression.Constant(5);//如果是布尔类型则需要用函数重载,显示指定它的类型
MemberExpression memExprId = Expression.MakeMemberAccess(paramExprA, typeof(Article).GetProperty("Id"));
BinaryExpression binExpGreaterThan = Expression.GreaterThan(memExprId, constExpr5);
Expression<Func<Article,bool>> exprRoot = Expression.Lambda<Func<Article,bool>>(binExpGreaterThan, paramExprA);
using(var ctx = new MyDbContext()) {
ctx.Articles.Where(exprRoot).ToArray();
}
}

常见工厂方法请自行查找。

让构建表达式树更简单

我们可以使用ExpressionTreeToString包提供的ToString方法,只要将ToString方法的参数写成以下形式就行。

1
2
3
4
static void Main(string[] args) {
Expression<Func<Article, bool>> e1 = a => a.Id > 5;
Console.WriteLine(e1.ToString("Factory methods","C#"));
}

以上代码生成如下内容:

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

var a = Parameter(
typeof(Article),
"a"
);

Lambda(
GreaterThan(
MakeMemberAccess(a,
typeof(Article).GetProperty("Id")
),
Constant(5)
),
a
)

只需将它粘贴到代码中稍作修改即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void Main(string[] args) {
Console.WriteLine("请输入 1:小于,2:大于");
string s = Console.ReadLine();
var a = Parameter(typeof(Article),"a" );
BinaryExpression binaryCompare;
if (s == "1") {
binaryCompare = LessThan(MakeMemberAccess(a,typeof(Article).GetProperty("Id")), Constant(5));
}else {
binaryCompare = GreaterThan(MakeMemberAccess(a, typeof(Article).GetProperty("Id")), Constant(5));
}
var expr = Lambda<Func<Article,bool>>(binaryCompare, a);
using(var ctx = new MyDbContext()) {
ctx.Articles.Where(expr).ToArray();
}
}

动态构建表达式树代码举例

修改Article类,override ToString方法:

1
2
3
4
5
6
7
8
9
10
11
public class Article
{
public int Id { get; set; }
public string Title { get; set; }
public string Message { get; set; }
public List<Comment> Comments = new List<Comment>();//实体类中关系属性
public bool IsDeleted { get; set; }
public override string ToString() {
return $"ID:{Id},Title:{Title},Message:{Message}";
}
}

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
using ExpressionTreeToString;
using Interview.一对多;
using System.Linq.Expressions;
using static System.Linq.Expressions.Expression;
namespace Interview
{
internal class Program {
static void Main(string[] args) {
var aritcles1 = QueryAritcles("Id", 8.0);
foreach(var aritcle in aritcles1) {
Console.WriteLine("====================================");
Console.WriteLine(aritcle);
}
var aritcles2 = QueryAritcles("Title", "T1");
foreach (var aritcle in aritcles2) {
Console.WriteLine("====================================");
Console.WriteLine(aritcle);
}
}
static IEnumerable<Article> QueryAritcles(string property,object value) {
var a = Parameter(
typeof(Article),
"a"
);
var proType = typeof(Article).GetProperty(property);
var valType = proType.PropertyType;
Expression<Func<Article, bool>> expr;
if (value.GetType().IsPrimitive) {
//如果是原始数据类型
expr = Lambda<Func<Article, bool>>(
Equal(
MakeMemberAccess(a, proType),
Constant(System.Convert.ChangeType(value,valType))
),
a
);
} else {
//如果是复杂数据类型
expr = Lambda<Func<Article,bool>>(
Call(
MakeMemberAccess(a,proType),
value.GetType().GetMethod("Contains", new[] { value.GetType() }),
Constant(System.Convert.ChangeType(value, valType))
),
a
);
}
using (var ctx = new MyDbContext()) {
return ctx.Articles.Where(expr).ToList();
}
}
}
}

不用Emit生成IL代码实现Select的动态化

如果我们想生成只查询xxx字段的SQL语句,那么就需要使用Select()方法比如以下两种方式:

1
2
ctx.Article.Select(a=>new{a.Title,a.Id});
ctx.Article.Select(a=>new object[]{a.Title,a.Id});

运行时动态设定Select查询出来的属性,需要使用Emit技术来采用动态生成IL的技术来在运行时创建一个类。难度大!
我们可以使用向Select函数传递object数组的方式来实现动态创建表达式树。
方法:
把列对应的属性的访问表达式放到一个Expression数组中,然后使用Expression.NewArrayInit构建一个代表数组的NewArrayExpression表达式对象,然后就可以用这个NewArrayExpression对象供Select调用来执行了
具体代码如下:
说实话,我没太学明白怕误导大家,大家还是请看杨中科老师的视频吧。

尽量避免使用动态构建表达式树

1、动态构建表达式树易读性差,维护麻烦。
2、一般只有在编写不特定于某个实体类的通用框架的时候,由于无法在编译器确定要操作的类名、属性等,所以才需要编写动态构建表达式树的代码。否则为了提高代码可读性和维护性,要尽量避免动态构建表达式树。而是用IQueryable的延迟执行特性来动态构造。

System.Linq.Dynamic.Core

1、安装Nuget包

1
Install-Package System.Linq.Dynamic.Core

2、使用字符串格式的语法进行数据操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System.Linq.Dynamic.Core;
namespace Interview
{
internal class Program {
static void Main(string[] args) {
using(var ctx = new MyDbContext()) {
var res = ctx.Articles.Where("Id>=5").Select("new(Id,Title)").ToDynamicArray();
foreach(var item in res) {
Console.WriteLine(item.Id+","+item.Title);
}
}
}
}
}