【EFCore】笔记20 表达式树

Expression 的结构

在 EFCore 中,几乎所有的 Linq 方法都增加了重载方法,这些重载方法是专门给 IQueryable 使用的,比如 Where 方法。之前说到,EFCore 的原理是将我们传入的查询条件翻译为 SQL 语句,而查询条件一般是通过 lambda 表达式编写并作为参数传入的,在类型上是一个委托。但这个委托不会被 EFCore 当成函数去调用执行,而是由 EFCore 核心转换为抽象语法树,然后由对应数据库的 Provider 去翻译为 SQL 语句。

这里传入的委托和我们平时对内存集合(IEnumerable)时传入的委托显然有不同的作用,这就是为什么 EFCore 要重写绝大部分的 Linq 方法。

IEnumerable 的 Where 方法声明:

public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);

IQueryable 的 Where 方法声明:

public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate);

其中最大的区别就是 IEnumerable 接受一个 Func<TSource, bool> 类型的委托,而 IQueryable 接受的是一个 Expression<Func<TSource, bool>> 类型的委托。这个 Expression 就是 EFCore 中的表达式树类型。

Expression 类型储存了这个委托(即查询条件)的条件逻辑(通过抽象语法树的结构储存),从而能够让 .NET 在运行时生成用于数据过滤的运行逻辑,实现动态条件查询。

我们如何查看一个 Expression 的结构呢?需要借助由 zspitz 开发的 ExpressionTreeVisualizer  插件或者 ExpressionTreeToString 包。具体的使用方法在项目主页里都有,这里不重复。

使用 ExpressionTreeToString 查看一个 Expression 的结构:

Expression<Func<Book, bool>> exp = e => e.Price > 50;
string str = exp.ToString(BuiltinRenderer.ObjectNotation, Language.CSharp);
Console.WriteLine(str);

这里使用的是 ObjectNotation 形式,输出:

var e = new ParameterExpression {
    Type = typeof(Book),
    IsByRef = false,
    Name = "e"
};

new Expression<Func<Book, bool>> {
    NodeType = ExpressionType.Lambda,
    Type = typeof(Func<Book, bool>),
    Parameters = new ReadOnlyCollection<ParameterExpression> {
        e
    },
    Body = new BinaryExpression {
        NodeType = ExpressionType.GreaterThan,
        Type = typeof(bool),
        Left = new MemberExpression {
            Type = typeof(double),
            Expression = e,
            Member = typeof(Book).GetProperty("Price")
        },
        Right = new ConstantExpression {
            Type = typeof(double),
            Value = 50
        }
    },
    ReturnType = typeof(bool)
}

这就是上面那个表达式树的结构了。每个结点都有自己的类型,根节点是 Lambda 类型的。它有一个名字为 e 的参数,e 是一个 Book 类型的 ParameterExpression,即参数表达式。根结点的 Body 是一个类型为 GreaterThan 的 BinaryExpression,即一个表示大于关系的二元表达式结点,其中左子结点是一个类型为 double 的 MemberExpression,用于获取 Book 的 Price 成员,而右子节点是一个类型为 double 的 ConstantExpression,即常量表达式,值为 50,当左节点大于右节点时,BinaryExpression 返回 true,否则返回 false。整个树的结构表示的逻辑就是价格大于 50 的 Book 返回真。

Expression 的创建

在 EFCore 中,结点的类型非常多,用于表示各种逻辑关系。如果我们需要手动创建表达式树,就需要使用 Expression 的工厂方法创建各种结点,例如上面这棵树的创建和使用代码如下:

using (MyDbContext ctx = new MyDbContext())
{
    var param_type = typeof(Book);
    var price_property = typeof(Book).GetProperty("Price");
    var price_const = Convert.ChangeType(50, price_property.PropertyType);
    var param_exp = Expression.Parameter(param_type);
    var memb_exp = Expression.MakeMemberAccess(param_exp, price_property);
    var const_exp = Expression.Constant(price_const, price_property.PropertyType);
    var bin_exp = Expression.MakeBinary(ExpressionType.GreaterThan, memb_exp, const_exp);
    var exp = Expression.Lambda<Func<Book, bool>>(bin_exp, param_exp);
    string str = exp.ToString(BuiltinRenderer.ObjectNotation, Language.CSharp);
    Console.WriteLine(str);
    Console.WriteLine(ctx.Books.Where(exp).Count());
}

这里有两个问题,第一就是这样手写创建代码非常麻烦,有什么快速的方法呢?我们可以使用 ExpressionTreeToString 的 Factory Method 形式输出树的结构:

Expression<Func<Book, bool>> exp = e => e.Price > 50;
string str = exp.ToString(BuiltinRenderer.FactoryMethods, Language.CSharp);
Console.WriteLine(str);

输出:

// using static System.Linq.Expressions.Expression

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

Lambda(
    GreaterThan(
        MakeMemberAccess(e,
            typeof(Book).GetProperty("Price")
        ),
        Constant(50)
    ),
    e
)

可以看出这样输出的文本就是通过工厂方法创建该树的代码,由于引用了命名空间,所以这里的方法都省略了 Expression 类名。我们只需要复制这个代码再完善一下即可。

第二个问题就是,我们一行代码就能写出来的委托,为什么要费这么大劲来写表达式树呢?在上面这个例子中,确实没有必要。使用表达式树的好处在于我们能动态创建适用于任意实体类型的查找逻辑,下面两个例子更好地展现了这一点。

Expression 的应用

例子1:动态查询

写一个方法 DynamicalQuery,这个方法接受任意实体的属性名和该属性类型的一个值,返回属性等于该值的所有实体对象。代码如下:

static IQueryable<T> DynamicalQuery<T>(string propertyName, object propertyValue,
    MyDbContext ctx) where T : class
{
    var param_type = typeof(T);
    var property = typeof(T).GetProperty(propertyName);
    var property_value = Convert.ChangeType(propertyValue, property.PropertyType);
    var param_exp = Expression.Parameter(param_type);
    var memb_exp = Expression.MakeMemberAccess(param_exp, property);
    var const_exp = Expression.Constant(property_value, property.PropertyType);
    var bin_exp = Expression.MakeBinary(ExpressionType.Equal, memb_exp, const_exp);
    var exp = Expression.Lambda<Func<T, bool>>(bin_exp, param_exp);
    return ctx.Set<T>().Where(exp);
}

当然,使用 SQL 语句拼接也能达到类似的效果,但是用拼接的办法健壮性不强,并且容易出现 SQL 语法错误。对于动态查询,使用 IQueryable 的复用也是可以实现类似效果,但前提是你确定了实体类型,否则使用 IQueryable 会特别困难。而使用表达式树的方法,即使实体类型在运行时确定,也相对简单。

例子2:动态投影

写一个方法 DynamicalSelect,这个方法接受一个数组,表示任意实体的一组属性名,返回该实体对这组属性的投影。在 Select 的时候,我们通常会使用匿名类来投影,但动态创建一个类类型的话需要使用 Emit,非常麻烦。这里不使用类,而是使用数组,然后用表达式树来赋值:

static IEnumerable<object[]> DynamicalSelect<T>(MyDbContext ctx, params string[] propertyNames) where T : class
{
    var param_type = typeof(T);
    var param_exp = Expression.Parameter(param_type);
    var memb_exps = new List<Expression>();
    foreach (var propertyName in propertyNames)
    {
        var property = typeof(T).GetProperty(propertyName);
        var memb_exp = Expression.MakeMemberAccess(param_exp, property);
        memb_exps.Add(Expression.Convert(memb_exp, typeof(object)));
    }
    var array_exp = Expression.NewArrayInit(typeof(object), memb_exps);
    var exp = Expression.Lambda<Func<T, object[]>>(array_exp, param_exp);
    return ctx.Set<T>().Select(exp);
}

上面两个例子的调用代码如下:

using (MyDbContext ctx = new MyDbContext())
{

    Console.WriteLine(DynamicalQuery<Book>("Title", "红楼梦", ctx).Count());
    var objs = DynamicalSelect<Book>(ctx, "Title", "Price").Take(10);
    foreach (var obj in objs)
    {
        Console.WriteLine($"{obj[0]} {obj[1]}");
    }

}

使用表达式树,可以解决泛型下的动态查询问题,但代码可读性较差,且难为维护。在实体类型确定的情况下,应该使用 IQueryable 复用来实现动态查询。如果真的要用到表达式树,则可以使用第三方开源库来简化操作,这里推荐 System.Linq.Dynamic.Core。它提供了一种非常简单的方式来进行泛型动态查询,其原理就是表达式树,但程序员可以使用字符串来表示属性,具体的使用教程见官网。