ASP.NET Core C# 反射 & 表达式树 (第三篇)

前言

前一篇讲完了反射, 这一篇来讲一下和反射息息相关的表达式树.

首先搞清楚 Delegate, Action, Func, Anonymous Method, Lambda, Expression tree

看大神的文章: C#中的Lambda表达式和表达式树

简单说, Delegate 委托是上古年代的东西, 

后来有了泛型, 就演化成 Action 和 Func

然后开始玩匿名函数就有了 Lambda 表达式

然后又出来了一个表达式树.

Lambda 其实就是 Delegate, Action, Func 一样的东西. 

而表达式树, 则是一些方法调用的表示手法.

表达式树由很多表达式组成, 经过 compile 可以变成 Lambda, 然后拿去执行. 

也可以 parse 这个表达式树, 翻译成其它的东西. 

比如 EF Core,

var person = await DbContext.People.Where(p => p.Name == "Derrick").ToListAsync();

上面这一句它可以翻译成 SQL

SELECT * FROM People WHERE Name = 'Derrick';

这篇不会讲到如何做翻译, 只会讲到如何动态创建表达式树.

 

Dynamic Expression How It Work

模拟一下动态创建 Expression for call EF Core's SingleAsync 方法.

public class Person {
    public string Name { get; set; } = "";
}
public class Program
{
    public static void Main()
    {
        var people = new List<Person> {
            new Person { Name = "n1" },
            new Person { Name = "n2" },
            new Person { Name = "n3" },
            new Person { Name = "n4" },
        };
        var person = people.AsQueryable().Single(p => p.Name == "n1");
        // p => p.Name == "n1"
        var pExp = Expression.Parameter(typeof(Person), "p"); // p
        var pDotNameExp = Expression.Property(pExp, "Name"); // p.Name
        var valueExp = Expression.Constant("n1"); // "n1"
        var pDotNameEqualValueExp = Expression.Equal(pDotName, value); // p.Name == "n1"
        var lambda = (Expression<Func<Person, bool>>)Expression.Lambda(pDotNameEqualValueExp, new ParameterExpression[] { pExp }); // p => p.Name == "n1"
        var lambdaDelegate = lambda.Compile(); // 把表达式树 compile 成可执行的委托
        var person2 = people.AsQueryable().Single(lambda); // queryable 情况下参数是表达式, 因为要翻译成 SQL
        var person3 = people.Single(lambdaDelegate); // LINQ 的情况下参数是委托, 因为它是直接执行的
    }
}

需要动态创建的表达式是这一句 p => p.Name == "n1"

从上面可以看到, 它是逐个逐个通过 Expression 创建和拼接出来的. 看注释一句一句理解. 

你会发现它的语法有一种 left right 的感觉, 然后拼着拼着就有点像棵树了, 二叉树

通常, 最后会把 Expression 变成 lambda 作为方法的参数, 需不需要 compile 就看最终拿来干什么. 如果是 Queryable 就直接给表达式树它翻译, 如果是 LINQ 就 compile 成委托执行.

多一个例子

var people = new List<Person> {
    new Person { Age = 1 },
    new Person { Age = 2 },
    new Person { Age = 3 },
    new Person { Age = 4 },
};
var person = people.AsQueryable().Where(p => p.Age >= 1 && p.Age <= 3);
// p => p.Age >= 1 && p.Age <= 3
var pExp = Expression.Parameter(typeof(Person), "p"); // p
var pDotAgeExp = Expression.Property(pExp, "Age"); // p.Age
var value1Exp = Expression.Constant(1); // 1
var pDotAgeGreatThanOrEqualvalue1Exp = Expression.GreaterThanOrEqual(pDotAgeExp, value1Exp); // p.Age >= 1
var value3Exp = Expression.Constant(3); // 3
var pDotAgeLessThanOrEqualvalue3Exp = Expression.LessThanOrEqual(pDotAgeExp, value3Exp); // p.Age <= 1
var and = Expression.And(pDotAgeGreatThanOrEqualvalue1Exp, pDotAgeLessThanOrEqualvalue3Exp); // p.Age >= 1 && p.Age <= 3
var lambda = (Expression<Func<Person, bool>>)Expression.Lambda(and, pExp); // p => p.Age >= 1 && p.Age <= 3
var lambdaDelegate = lambda.Compile();
var result = people.Where(lambdaDelegate).ToList(); // [1,2,3]

不管表达式如何复杂, 它就是逐个逐个去拼接, 所以不要害怕. Expression 里面有非常非常都多方法, 几乎你能写出来的 code 都能找到对应的 Expression

 

执行 lambdaDelegate

上面例子中, 我们都是把 Compile() 后的 lambdaDelegate pass 给 Single 和 Where 方法执行.

这里给一个自己执行的例子. 

在第一篇 Anonymous method / Delegate, 我给过三种调用 delegate 的方式.

lambdaDelegate 也是委托, 按理说应该完全适用

第一种方式是类型清楚的, 这个没有问题

// width => width > 100;
var widthParameterExp = Expression.Parameter(typeof(int), "width");
var greaterThan100Exp = Expression.GreaterThan(widthParameterExp, Expression.Constant(100));
var lambdaDelegate = Expression.Lambda<Func<int, bool>>(greaterThan100Exp, widthParameterExp).Compile();
var isGreaterThan100 = lambdaDelegate(150); // True

第二种是类型不清楚的

var lambdaDelegate = Expression.Lambda(greaterThan100Exp, widthParameterExp).Compile();
var isGreaterThan100 = (bool)lambdaDelegate.Method.Invoke(lambdaDelegate.Target, new object[] { 150 })!;

结果报错了 : MethodInfo must be a runtime MethodInfo

第三种 DynamicInvoke 则没有问题

var isGreaterThan100 = (bool)lambdaDelegate.DynamicInvoke(new object[] { 150 })!;

注意: DynamicInvoke 是很慢的, 虽然是搞反射, 但如果能知道部分类型, 比如第一种方式, 那就尽量用第一种吧.

 

Expression.Call

表达式中也可以表达方法调用哦. 当然这个对翻译 SQL 的话可能会有点问题啦. 但是 for 执行的话就很好用哦. 

public class Person {
    public int Age { get; set; }
    public bool IsDeleted(int value) 
    {
        return true;
    }
}
public class Program
{
    public static void Main()
    {
        var people = new List<Person> {
            new Person { Age = 1 },
            new Person { Age = 2 },
            new Person { Age = 3 },
            new Person { Age = 4 },
        };
        // p => p.IsDeleted(5)
        var pExp = Expression.Parameter(typeof(Person), "p"); // p
        var method = typeof(Person).GetMethod("IsDeleted")!; // 通过反射获取 MethodInfo
        var callMethodExp = Expression.Call(pExp, method, new Expression[] { Expression.Constant(5) }); // p.IsDeleted(5)
        var lambda = (Expression<Func<Person, bool>>)Expression.Lambda(callMethodExp, pExp); // p => p.IsDeleted(5)
        var lambdaDelegate = lambda.Compile();
        var result = people.Where(lambdaDelegate).ToList(); // [1,2,3,4]
    }
}

 

总结

动态创建表达式树, 可以帮助我们更好的管理 EF.Core 的代码. OData 也是通过这个方式去写入 EF Core 的 OData -> EF Core -> SQL.

以前我不喜欢写反射, 表达式树, 但后来我发现, 只要把小方法写好, 一点一点拼接上来, 它只是代码多, 但是并不会乱. 所以不要怕.

随便提一嘴,表达式树一直没有更新,许多 C# 新语法都不支持,相关 Github Issue – Extend expression trees to cover more/all language constructs

 

posted @ 2021-11-03 00:06  兴杰  阅读(678)  评论(0编辑  收藏  举报