C# 表达式树实战:从零构建动态 LINQ Where 解析器
在日常开发中,我们经常会遇到这样的需求:前端表格需要根据多列条件动态筛选数据,后端则要根据这些条件拼接出对应的
SQL WHERE 子句。最直觉的做法是拼接字符串,但随之而来的 SQL
注入、类型转换、括号优先级问题都让人头疼。
本文结合 SwitchData 项目中的
ExpressionParser 实现,介绍如何用 C#
表达式树(Expression Trees) 把强类型的 LINQ 表达式
x => x.Name.Contains("A") && x.Age > 18
解析成结构化查询条件,从而安全、可维护地生成 SQL。
一、为什么需要表达式树解析
表达式树并不是新东西,但它能把 “代码即数据”
的理念发挥到极致。普通委托 Func<T, bool> 只能执行,而
Expression<Func<T, bool>>
可以被遍历、分析和转换。
在数据查询层,我们希望做到:
- 强类型:编译期就能发现字段名拼写错误。
- 可解析:把
Where表达式拆成列名、运算符、值、逻辑关系等结构化信息。 - 可组合:多个条件能自由
AND/OR组合,并正确处理括号优先级。 - 防注入:使用参数化查询,而不是字符串拼接。
下面先看一个完整的解析示例,再拆解实现细节。
二、整体流程
flowchart LR
A[前端查询条件] --> B[构建 Expression<Func<T, bool>>]
B --> C[ExpressionParser 遍历表达式树]
C --> D[生成 QueryCondition 列表]
D --> E[转换为 SQL WHERE 子句]
E --> F[参数化查询执行]
核心类职责如下:
PredicateBuilder:把多个独立条件组合成一个表达式。ConditionVisitor:继承ExpressionVisitor,遍历表达式树并收集条件。MetadataCache:缓存实体与数据库字段的映射关系,用于把属性名转成列名。FilterOperator:把解析后的条件格式化成具体 SQL 片段。
三、用 PredicateBuilder 组合条件
当条件来自前端多个筛选框时,我们通常会得到多个零散的条件表达式。PredicateBuilder
提供 And 和 Or 扩展方法,把多个
Expression<Func<T, bool>> 合并成一个。
public static class PredicateBuilder
{
public static Expression<Func<T, bool>> True<T>() => f => true;
public static Expression<Func<T, bool>> False<T>() => f => false;
public static Expression<Func<T, bool>> And<T>(
this Expression<Func<T, bool>> left,
Expression<Func<T, bool>> right)
{
var parameter = left.Parameters[0];
var rightBody = ReplaceParameter(right.Body, right.Parameters[0], parameter);
return Expression.Lambda<Func<T, bool>>(
Expression.AndAlso(left.Body, rightBody), parameter);
}
public static Expression<Func<T, bool>> Or<T>(
this Expression<Func<T, bool>> left,
Expression<Func<T, bool>> right)
{
var parameter = left.Parameters[0];
var rightBody = ReplaceParameter(right.Body, right.Parameters[0], parameter);
return Expression.Lambda<Func<T, bool>>(
Expression.OrElse(left.Body, rightBody), parameter);
}
}
关键点在于 参数统一。每个表达式都有自己的参数
f,合并时必须把右侧表达式的参数替换成左侧的参数,否则
Expression.AndAlso 会抛出异常。
四、遍历表达式树:ConditionVisitor
ExpressionVisitor 是 .NET
提供的表达式树遍历基类。我们只需重写几个关键方法,就能把表达式转换成自定义的
QueryCondition。
4.1 二元表达式:比较与逻辑运算
protected override Expression VisitBinary(BinaryExpression node)
{
// 处理 && 和 ||
if (node.NodeType == ExpressionType.AndAlso ||
node.NodeType == ExpressionType.OrElse)
{
var relation = node.NodeType == ExpressionType.AndAlso
? RelationType.AND : RelationType.OR;
Visit(node.Left);
_pendingRelation = relation;
Visit(node.Right);
return node;
}
// 处理 ==、!=、>、<、>=、<=
if (IsComparison(node.NodeType))
{
var left = StripConvert(node.Left);
var right = StripConvert(node.Right);
if (TryExtractMemberAndConstant(left, right, out var member, out var value, out var memberOnLeft))
{
Conditions.Add(new QueryCondition
{
Column = GetColumnName(member),
Value = value,
Operator = memberOnLeft
? GetOperator(node.NodeType, value)
: GetReversedOperator(node.NodeType, value),
Relation = GetEffectiveRelation()
});
}
return node;
}
return base.VisitBinary(node);
}
这里有两个细节:
- 剥除 Convert 节点:C#
编译器在值类型与可空类型比较时,会自动生成
Convert(x.Age)节点,需要递归剥离。 - 支持常量左右互换:比如
18 < x.Age与x.Age > 18应该解析出同样的条件。
4.2 方法调用:字符串模糊查询
x.Name.Contains("A")、StartsWith、EndsWith
属于 MethodCallExpression,需要单独处理:
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method.DeclaringType == typeof(string) &&
node.Object is MemberExpression member &&
node.Arguments.Count == 1)
{
var op = node.Method.Name switch
{
"Contains" => OperatorType.Contains,
"StartsWith" => OperatorType.StartsWith,
"EndsWith" => OperatorType.EndsWith,
_ => (OperatorType?)null
};
if (op.HasValue)
{
Conditions.Add(new QueryCondition
{
Column = GetColumnName(member),
Operator = op.Value,
Value = GetConstantValue(node.Arguments[0]),
Relation = GetEffectiveRelation()
});
return node;
}
}
return base.VisitMethodCall(node);
}
4.3 成员访问:布尔属性简写
x.IsActive 等价于
x.IsActive == true,ExpressionVisitor
可以直接识别:
protected override Expression VisitMember(MemberExpression node)
{
if ((node.Type == typeof(bool) || node.Type == typeof(bool?)) &&
node.Expression is ParameterExpression)
{
Conditions.Add(new QueryCondition
{
Column = GetColumnName(node),
Operator = OperatorType.Equals,
Value = true,
Relation = GetEffectiveRelation()
});
return node;
}
return base.VisitMember(node);
}
五、提取常量值与闭包变量
表达式中的值可能是字面常量,也可能是闭包捕获的外部变量。GetConstantValue
方法要同时处理这两种情况:
private static object GetConstantValue(Expression expr)
{
expr = StripConvert(expr);
if (expr is ConstantExpression c) return c.Value;
if (expr is MemberExpression m)
{
// 闭包变量被编译器生成到匿名类字段中
if (m.Expression is ConstantExpression container)
{
var obj = container.Value;
if (m.Member is FieldInfo f) return f.GetValue(obj);
if (m.Member is PropertyInfo p) return p.GetValue(obj);
}
// 静态字段/属性
if (m.Expression == null)
{
if (m.Member is FieldInfo sf) return sf.GetValue(null);
if (m.Member is PropertyInfo sp) return sp.GetValue(null);
}
}
// 兜底:编译执行
return Expression.Lambda(expr).Compile().DynamicInvoke();
}
六、处理括号与分组
&& 与 ||
混合时,必须正确处理括号。实现思路是:当子表达式中的逻辑运算符与父级不一致时,为其分配一个新的
GroupId。
private static bool NeedsGroup(Expression expr, ExpressionType parentType)
{
if (expr is BinaryExpression binary &&
(binary.NodeType == ExpressionType.AndAlso ||
binary.NodeType == ExpressionType.OrElse))
{
return binary.NodeType != parentType;
}
return false;
}
例如 (A && B) || C
中,A && B 会被标记为新分组,生成的 SQL
才能正确写成 (A AND B) OR C。
七、从属性名到数据库列名
表达式里只有属性名 x.Name,但 SQL 里需要列名
[UserName]。MetadataCache 通过
DbColumnAttribute 建立映射,并在
ExpressionParser.GetColumnName 中解析:
private static string GetColumnName(MemberExpression member)
{
var path = GetPropertyPath(member); // 例如 "Customer.Name"
var meta = MetadataCache.GetTableMetadata(declaringType);
if (meta.PropertyMap.TryGetValue(path, out var col))
return col.ColumnName;
// 降级:属性名与列名一致
return member.Member.Name;
}
八、使用示例
Expression<Func<User, bool>> predicate = u =>
u.Name.Contains("张") && (u.Age > 18 || u.IsActive);
var conditions = ExpressionParser.ParseConditions(predicate);
// 生成类似:Name LIKE '%张%' AND (Age > 18 OR IsActive = 1)
九、总结
表达式树让 “把代码当数据解析” 成为可能。通过
PredicateBuilder 组合条件、ExpressionVisitor
遍历表达式、元数据缓存映射字段,我们可以:
- 用强类型 LINQ 表达式替代字符串拼接。
- 自动处理常量、闭包、可空类型、布尔简写等边界情况。
- 生成带括号的结构化条件,最终输出安全的参数化 SQL。
在 SwitchData
的查询层,这套机制支撑了前端复杂筛选与后端动态 SQL
的无缝衔接。如果你的项目也在为动态查询苦恼,不妨试试表达式树这套方案。