文章目录
前言
在 Entity Framework Core 中,表达式树(Expression Tree) 是 LINQ 查询的核心机制,它允许将 C# 代码中的查询逻辑转换为 SQL 语句,从而在数据库服务器端高效执行。
一、表达式树与委托的区别
委托(如 Func<T, bool>)
直接编译为可执行的代码,运行时在内存中过滤数据(客户端评估)。Func<House, bool> func = p => p.Owner.Contains("tom"); var res=dbContext.Houses.Where(exp1).ToList(); // 在客户端过滤!
表达式树(如 Expression<Func<T, bool>>)
保持查询逻辑的抽象语法树结构,EF Core 可将其转换为 SQL(服务器端评估)。Expression<Func<House, bool>> exp1 = b => b.Owner.Contains("tom"); var res=dbContext.Houses.Where(exp1).ToList();//生成 SQL:WHERE Owner like '%tom%'
二、动态构建表达式树
- 当需要根据运行时条件动态生成查询时,手动构建表达式树非常有用。
- ParameterExpression、BinaryExpression、MethodCallExpression、ConstantExpression等类几乎都没有提供构造方法,而且所有属性也几乎都是只读,因此我们一般不会直接创建这些类的实例,而是调用Expression类的Parameter、MakeBinary、Call、Constant等静态方法来生成,这些静态方法我们一般称作创建表达式树的工厂方法,而属性则是通过方法参数类设置。
- 工厂方法:
加法:Add
短路与运算:AndAlso
数组元素访问:ArraryAccess
方法访问:Call
三元条件运算符:Condition
常量表达式:Constant
类型转换:Convert
大于运算符:GreaterThan
小于运算:LessThan
大于或等于运算符:GreaterThanOrEqual
创建二元运算:MakeBinary
不等于运算:NotEqual
短路或运算:OrElse
表达式的参数:Parameter
示例1
动态过滤Owner包含关键字
using System.Linq.Expressions; using (MyDBContext dbContext=new MyDBContext()) { string name = Console.ReadLine(); // 参数表达式:代表实体对象(如 p => ... 中的 p) ParameterExpression param = Expression.Parameter(typeof(House), "p"); // 属性访问:p.Owner MemberExpression nameProperty = Expression.Property(param, "Owner"); MethodInfo containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) }); // 参数值 ConstantExpression keywordConstant = Expression.Constant(name); MethodCallExpression nameCondition = Expression.Call(nameProperty, containsMethod, keywordConstant); // 组合为 Lambda 表达式 Expression<Func<House, bool>> expr = Expression.Lambda<Func<House, bool>>(nameCondition, param); // 应用查询 var query = dbContext.Houses.Where(expr); foreach (var house in query) { Console.WriteLine(house.Owner); } }
生成的 SQL:
SELECT * FROM Houses WHERE Owner like '%tom%'
示例2
动态过滤价格
using System.Linq.Expressions; // 参数表达式:代表实体对象(如 p => ... 中的 p) ParameterExpression param = Expression.Parameter(typeof(House), "p"); // 属性访问:p.Price MemberExpression priceProperty = Expression.Property(param, "Price"); // 常量值:100 ConstantExpression constant = Expression.Constant(100.0); // 比较表达式:p.Price > 100 BinaryExpression priceComparison = Expression.GreaterThan(priceProperty, constant); // 组合为 Lambda 表达式 Expression<Func<House, bool>> expr = Expression.Lambda<Func<House, bool>>(priceComparison, param); // 应用查询 var query = dbContext.Houses.Where(expr);
生成的SQL
SELECT * FROM T_Houses WHERE Price > 100
示例3
组合多个表达式(动态查询中,常需要组合多个条件(如 AND/OR))
public static Expression<Func<House, bool>> BuildDynamicFilter(string paramOwnerStr,string paramPriceStr, string nameKeyword, double? minPrice) { ParameterExpression param = Expression.Parameter(typeof(House), "p"); Expression finalExpr = Expression.Constant(true); // 初始条件:true // 条件1:名称包含关键字 if (!string.IsNullOrEmpty(nameKeyword)) { MemberExpression nameProperty = Expression.Property(param, paramOwnerStr); MethodInfo containsMethod = typeof(string).GetMethod("Contains", new[] { typeof(string) }); ConstantExpression keywordConstant = Expression.Constant(nameKeyword); MethodCallExpression nameCondition = Expression.Call(nameProperty, containsMethod, keywordConstant); finalExpr = Expression.AndAlso(finalExpr, nameCondition); } // 条件2:价格 >= minPrice if (minPrice.HasValue) { MemberExpression priceProperty = Expression.Property(param, paramPriceStr); ConstantExpression minPriceConstant = Expression.Constant(minPrice.Value); BinaryExpression priceCondition = Expression.GreaterThanOrEqual(priceProperty, minPriceConstant); finalExpr = Expression.AndAlso(finalExpr, priceCondition); } return Expression.Lambda<Func<House, bool>>(finalExpr, param); }
// 使用: using (MyDBContext dbContext =new MyDBContext()) { var filter = BuildDynamicFilter("Owner","Price","Tom", 2000.0); var query = dbContext.Houses.Where(filter); foreach (var house in query) { Console.WriteLine(house.Owner); } }
生成的SQL
SELECT [t].[Id], [t].[Name], [t].[Owner], [t].[Price], [t].[RowVersion] FROM [T_Houses] AS [t] WHERE [t].[Owner] LIKE N'%Tom%' AND [t].[Price] >= 2000.0E0
高级技巧:表达式合并
如果需要组合两个已有的表达式(如 expr1 && expr2),需统一参数。
示例:合并两个表达式
public static Expression<Func<T, bool>> CombineAnd<T>( Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2) { var param = Expression.Parameter(typeof(T)); var body1 = ReplaceParameter(expr1.Body, expr1.Parameters[0], param); var body2 = ReplaceParameter(expr2.Body, expr2.Parameters[0], param); return Expression.Lambda<Func<T, bool>>( Expression.AndAlso(body1, body2), param); } private static Expression ReplaceParameter( Expression expression, ParameterExpression oldParam, ParameterExpression newParam) { return new ParameterReplacer(oldParam, newParam).Visit(expression); } class ParameterReplacer : ExpressionVisitor { private readonly ParameterExpression _oldParam; private readonly ParameterExpression _newParam; public ParameterReplacer(ParameterExpression oldParam, ParameterExpression newParam) { _oldParam = oldParam; _newParam = newParam; } protected override Expression VisitParameter(ParameterExpression node) { return node == _oldParam ? _newParam : node; } }
使用: Expression<Func<House, bool>> expr1 = p => p.Owner.Contains("Tom"); Expression<Func<House, bool>> expr2 = p => p.Price > 2000; var combinedExpr = CombineAnd(expr1, expr2); var query2 = dbContext.Houses.Where(combinedExpr); foreach (var house in query2) { Console.WriteLine(house.Owner); }
生成的SQL
SELECT [t].[Id], [t].[Name], [t].[Owner], [t].[Price], [t].[RowVersion] FROM [T_Houses] AS [t] WHERE [t].[Owner] LIKE N'%Tom%' AND [t].[Price] > 2000.0E0
三、ExpressionTreeToString
ExpressionTreeToString 是一个第三方库,用于将 LINQ 表达式树(Expression)转换为可读的字符串形式,帮助开发者调试和分析表达式树的结构。
输出的所有代码都是对于工厂方法的调用,且调用工厂方法的时候都省略了Expression类,手动添加Expression或者using static System.Linq.Expressions.Expression;
安装方法
通过 NuGet 包管理器安装
Install-Package ExpressionTreeToString
基本用法
示例
Expression<Func<House, bool>> exp1 = b => b.Owner.Contains("tom"); Expression<Func<House, bool>> exp2 = b => b.Price > 2000; //转换为字符串(支持多种格式化选项) //string exprString = expr.ToString("C#", "Dynamic LINQ"); //Console.WriteLine(exp1.ToString("Factory methods", "C#")); Console.WriteLine(exp2.ToString("Factory methods", "C#"));
//输出结果展示 // using static System.Linq.Expressions.Expression var b = Parameter( typeof(House), "b" ); Lambda( GreaterThan( MakeMemberAccess(b, typeof(House).GetProperty("Price") ), Constant(2000) ), b )
支持的格式化风格
ExpressionTreeToString 提供多种输出格式,方便不同场景使用:
- C# 语法风格:ToString(“C#”)
接近 C# 代码的直观表示。 - Visual Basic 语法风格:ToString(“VB”)
类似 VB 语法。 - 表达式树结构:ToString(“Object notation”)
显示表达式树的节点结构(如 BinaryExpression、ParameterExpression)。 - 调试视图:ToString(“DebugView”)
与 Visual Studio 调试器中表达式树的显示一致。
四、注意事项
- 不支持所有 C# 方法:某些方法(如 ToString())无法转换为 SQL,会导致运行时错误。
- 调试技巧:通过 query.ToQueryString() 查看生成的 SQL。
- 性能:表达式树构建在内存中完成,复杂逻辑可能影响启动性能。
总结
通过灵活使用表达式树,可以极大增强 EF Core 查询的灵活性,同时保持高效的服务器端执行。