# 手写 LINQ 表达式树解析器:将 Lambda 条件翻译为数据库查询
在 ORM 框架中,最核心的能力之一就是把开发者写的 Lambda 表达式(如
x => x.Name == "foo" && x.Age > 18)翻译成
SQL。Entity Framework Core
靠的是内部庞大的表达式树翻译管线,而在自研的轻量级数据访问层里,我们完全可以自己实现一个小型的表达式树解析器。
本文从 SwitchData 项目的 ExpressionParser
出发,手把手拆解如何把一棵 LINQ
表达式树拆解成结构化的查询条件,再交给底层拼接 SQL。读完你会理解
ExpressionVisitor
的遍历机制、闭包常量的提取、括号分组的语义还原,以及为什么”手写一个 LINQ
Provider”并没有想象中那么难。
一、整体设计
ExpressionParser 对外暴露两个核心方法:
ParseSorting<T>(orderBy):把OrderBy/ThenBy链式调用解析成「列名 → 排序方向」的有序字典。ParseConditions<T>(predicate):把Expression<Func<T, bool>>解析成List<QueryCondition>,每个条件包含列名、操作符、值、逻辑关系(AND/OR)和分组 ID。
整体流程可以用一张流程图概括:
flowchart LR
A[Lambda 表达式] --> B[ExpressionParser]
B --> C[ParseSorting 解析排序]
B --> D[ParseConditions 解析条件]
C --> E[列名+方向字典]
D --> F[ConditionVisitor 遍历表达式树]
F --> G[List QueryCondition]
E --> H[拼接 ORDER BY]
G --> I[拼接 WHERE 子句]
二、排序解析:从 OrderBy 链提取列名
IOrderedQueryable<T> 本质上是一棵由
MethodCallExpression 组成的调用链。例如:
q.OrderBy(x => x.Name).ThenByDescending(x => x.Age)
对应的表达式树结构是(从外到内):
ThenByDescending( OrderBy( source, x=>x.Name ), x=>x.Age )
解析思路是:用一个空的 IQueryable 触发 Lambda,得到
IOrderedQueryable,然后不断向内剥
MethodCallExpression:
public static OrderedDictionary<string, string> ParseSorting<T>(
Func<IQueryable<T>, IOrderedQueryable<T>> orderBy)
{
var dummy = Enumerable.Empty<T>().AsQueryable();
var ordered = orderBy(dummy);
var result = new OrderedDictionary<string, string>();
var expr = ordered.Expression;
while (expr is MethodCallExpression call)
{
var methodName = call.Method.Name;
if (!new[] { "OrderBy", "OrderByDescending", "ThenBy", "ThenByDescending" }.Contains(methodName))
break;
// 第二个参数是 Lambda,外层包了 UnaryExpression
var lambda = (LambdaExpression)((UnaryExpression)call.Arguments[1]).Operand;
var column = GetColumnName(lambda.Body);
var direction = methodName.EndsWith("Descending") ? "DESC" : "ASC";
if (!result.ContainsKey(column))
result[column] = direction;
expr = call.Arguments[0]; // 继续向内
}
return new OrderedDictionary<string, string>(result.Reverse());
}
两个细节值得注意:
- 从外到内解析,最后反转:
ThenBy在外层,OrderBy在内层,收集到的顺序是反的,所以最后要Reverse()。 UnaryExpression包装:方法参数里的 Lambda 在表达式树中会被包一层Convert/Quote,需要先剥掉UnaryExpression才能拿到真正的LambdaExpression。
三、条件解析:用 ExpressionVisitor 遍历表达式树
条件解析是整个解析器的核心。框架提供了抽象基类
ExpressionVisitor,我们只需重写关心的节点类型,框架会自动递归遍历。
3.1 比较运算:== != > < >= <=
比较运算对应
BinaryExpression。核心难点是区分左右两边哪个是成员访问、哪个是常量,因为用户既可能写
x.Age > 18,也可能写 18 < x.Age。
protected override Expression VisitBinary(BinaryExpression 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))
{
var condition = new QueryCondition
{
Column = GetColumnName(member),
Value = value,
Operator = memberOnLeft
? GetOperator(node.NodeType, value)
: GetReversedOperator(node.NodeType, value),
Relation = GetEffectiveRelation(),
GroupId = _currentGroupId
};
Conditions.Add(condition);
}
return node;
}
return base.VisitBinary(node);
}
GetReversedOperator
的作用是:当常量在左、成员在右时,把运算符反转。例如
18 < x.Age 等价于 x.Age > 18,所以
LessThan 要翻成 GreaterThan。
3.2 闭包常量的提取
用户写 var name = "foo"; x => x.Name == name
时,编译器会把 name
包装成一个编译器生成类的字段,表达式树里表现为
MemberExpression 挂在 ConstantExpression
上。提取时需要反射取字段值:
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);
}
}
// 兜底:编译并动态执行(性能较低,但保证兼容)
return Expression.Lambda(expr).Compile().DynamicInvoke();
}
这里的兜底策略很关键——对于无法直接提取的复杂表达式(如方法调用结果),直接编译执行 Lambda 拿到值,牺牲一点性能换取兼容性。
3.3 布尔属性简写
x => x.IsActive 实际上是
x => x.IsActive == true 的简写。在
VisitMember 中判断成员类型是否为
bool,自动补成 Equals true:
protected override Expression VisitMember(MemberExpression node)
{
var type = node.Type;
if ((type == typeof(bool) || type == typeof(bool?)) && node.Expression is ParameterExpression)
{
Conditions.Add(new QueryCondition
{
Column = GetColumnName(node),
Operator = OperatorType.Equals,
Value = true,
Relation = GetEffectiveRelation(),
GroupId = _currentGroupId
});
return node;
}
return base.VisitMember(node);
}
3.4 字符串方法:Contains / StartsWith / EndsWith
x.Name.Contains("abc") 对应
MethodCallExpression,node.Object
是成员访问,node.Arguments[0] 是参数:
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(),
GroupId = _currentGroupId
});
return node;
}
}
return base.VisitMethodCall(node);
}
3.5 逻辑运算符与括号分组
这是最复杂的部分。&& 和 || 也是
BinaryExpression,但它们的左右子树可能需要括号。例如:
x => (x.Age > 18 || x.Age < 10) && x.Name == "foo"
这里 || 子树需要用括号括起来,而 &&
不需要。判断规则是:子表达式的逻辑运算符与父级不同时,需要分组。
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;
}
实现时用 GroupId
标记同一括号内的条件。当需要分组时,分配一个新的
_currentGroupId,遍历完子树后恢复:
var leftNeedGroup = NeedsGroup(node.Left, node.NodeType);
if (leftNeedGroup)
{
var saved = _currentGroupId;
_currentGroupId = _nextGroupId++; // 新分组
Visit(node.Left);
_currentGroupId = saved; // 恢复
}
同时用 _groupStartRelation
记录分组内第一个条件应使用的逻辑关系,确保生成的 SQL
中括号内的连接符正确。
四、列名解析与元数据缓存
实体属性名和数据库列名往往不同(如 UserName →
user_name)。MetadataCache 通过反射扫描
DbTableAttribute、DbColumnAttribute
等特性,构建类型到 TableMetadata 的映射,并用
ConcurrentDictionary 缓存。
GetColumnName 支持多层属性路径(如
x.Customer.Name),逐层查找最后一级属性的列名:
private static string GetColumnName(MemberExpression member)
{
var path = GetPropertyPath(member); // "Customer.Name"
var declaringType = member.Expression?.Type ?? member.Member.ReflectedType;
var meta = MetadataCache.GetTableMetadata(declaringType);
if (meta.PropertyMap.TryGetValue(path, out var col))
return col.ColumnName;
// 路径含点号时,逐层定位最后一级属性的类型
var parts = path.Split('.');
if (parts.Length > 1)
{
var currentType = declaringType;
for (int i = 0; i < parts.Length - 1; i++)
{
var prop = currentType.GetProperty(parts[i]);
if (prop == null) break;
currentType = prop.PropertyType;
}
meta = MetadataCache.GetTableMetadata(currentType);
if (meta.PropertyMap.TryGetValue(parts.Last(), out col))
return col.ColumnName;
}
return parts.Last(); // 降级:属性名即列名
}
值得一提的是,MetadataCache 中属性的 Getter/Setter
不是用原生反射,而是通过表达式树编译成强类型委托,避免了每次调用的反射开销:
private static Func<object, object> CreateGetter(PropertyInfo property)
{
ParameterExpression instance = Expression.Parameter(typeof(object), "obj");
UnaryExpression cast = Expression.Convert(instance, property.DeclaringType);
MemberExpression propertyAccess = Expression.Property(cast, property);
UnaryExpression box = Expression.Convert(propertyAccess, typeof(object));
return Expression.Lambda<Func<object, object>>(box, instance).Compile();
}
五、PredicateBuilder:动态组合表达式
除了解析已有表达式,我们还常常需要动态构建表达式。比如根据前端传来的筛选条件,按需用
AND/OR 拼接。PredicateBuilder 提供了 And 和
Or 扩展方法:
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);
}
这里的关键是 ReplaceParameter:两个表达式的参数
ParameterExpression 是不同的实例,必须用自定义的
ParameterReplacerVisitor
把右边的参数替换成左边的,否则合并后会报”参数未绑定”的错误。
使用示例:
var predicate = PredicateBuilder.True<User>();
if (!string.IsNullOrEmpty(keyword))
predicate = predicate.And(x => x.Name.Contains(keyword));
if (minAge.HasValue)
predicate = predicate.And(x => x.Age >= minAge.Value);
var conditions = ExpressionParser.ParseConditions(predicate);
六、总结
手写一个 LINQ 表达式树解析器,核心就三点:
- 理解表达式树结构:Lambda 编译成
Expression<TDelegate>后是一棵对象树,BinaryExpression、MemberExpression、MethodCallExpression、ConstantExpression是最常见的节点。 - 继承
ExpressionVisitor:重写你关心的节点类型,框架自动递归。把条件提取出来,存进结构化的
QueryCondition列表。 - 处理边界情况:Convert
节点剥离、闭包常量提取、运算符反转、括号分组、空值判断(
== null→IS NULL)。
SwitchData 的 ExpressionParser 没有追求支持所有 LINQ
操作(如 Select、Join),而是聚焦于
Where 条件和 OrderBy
排序——这恰好覆盖了绝大多数业务查询场景。这种”够用就好”的设计思路,在自研基础设施中非常值得借鉴。
如果你正在构建自己的轻量级数据访问层,不妨从这个解析器出发,逐步扩展支持的表达式类型。表达式树没有那么神秘,它只是把代码变成了可以遍历的数据结构而已。