你有没有好奇过,EF Core 这样的 ORM 框架是怎么把 ctx.Users.Where(x => x.Age > 18 && x.Name.Contains(“张”)) 翻译成 SELECT * FROM Users WHERE Age > @p1 AND Name LIKE ‘%张%’ 的?
答案就是 表达式树(Expression Tree)。本文从零带你走进 SwitchData 项目里那个只有 462 行的小巧但完整的表达式解析器——它实现了从 LINQ Lambda 到 QueryCondition 列表和排序字典的完整翻译流程,是理解 ORM 底层原理的绝佳练手素材。
一、为什么需要表达式解析器
在手写 ORM 的场景下,我们希望业务代码写 LINQ:
csharp var users = db.Query<User>() .Where(u => u.Age > 18 && u.Status == 1) .OrderBy(u => u.CreatedAt) .ToList();
但底层数据库只认 SQL。ORM 的核心职责就是把 LINQ 翻译成 SQL,而第一步就是把 Lambda 表达式翻译成中间表示(IR)——一个结构化的查询条件列表。
SwitchData 项目中的 ExpressionParser 类就是这个翻译器:
LINQ Lambda ──ExpressionParser──▶ QueryCondition 列表 + 排序字典 │ ▼ SQL 构建器 ──▶ WHERE / ORDER BY 子句
二、项目中的数据模型
在看解析逻辑之前,先了解解析产物长什么样。SwitchData.Common.Db 命名空间下定义了查询条件的核心模型:
`csharp public enum OperatorType { Equals, NotEquals, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Contains, StartsWith, EndsWith, IsNull, IsNotNull }
public enum RelationType { Empty, AND, OR }
public class QueryCondition { public string Column { get; set; } public object Value { get; set; } public OperatorType Operator { get; set; } public RelationType Relation { get; set; } public int GroupId { get; set; } } `
举个例子,u => (u.Age > 18 && u.Status == 1) || u.Name.Contains(“张”) 会被解析成:
| Column | Value | Operator | Relation | GroupId |
|---|---|---|---|---|
| Age | 18 | GreaterThan | Empty | 1 |
| Status | 1 | Equals | AND | 1 |
| Name | “张” | Contains | OR | 0 |
GroupId 相同的条件属于同一括号分组,Relation 表示当前条件和前一条条件用什么逻辑连接。有了这个结构,SQL 构建器就能精确还原括号语义。
三、第一部分:排序表达式解析
先从简单的开始——OrderBy / ThenBy 排序链的解析。LINQ 的排序语法本质上是一个方法调用链。
3.1 核心思路:沿表达式树向内剥离
`csharp public static OrderedDictionary<string, string>
ParseSorting
var expr = ordered.Expression;
while (expr is MethodCallExpression call)
{
var name = call.Method.Name;
if (!new[] { "OrderBy","OrderByDescending",
"ThenBy","ThenByDescending" }.Contains(name))
break;
var lambda = (LambdaExpression)((UnaryExpression)call.Arguments[1]).Operand;
var column = GetColumnName(lambda.Body);
var direction = name.EndsWith("Descending") ? "DESC" : "ASC";
if (!result.ContainsKey(column))
result[column] = direction;
expr = call.Arguments[0];
}
return new OrderedDictionary<string, string>(result.Reverse());
} `
关键点:传入的 orderBy 是一个 Func<IQueryable
3.2 从 Lambda 体提取数据库列名
GetColumnName 做了两件事:剥离类型转换 + 属性到列名映射。
`csharp private static string GetColumnName(Expression body) { while (body is UnaryExpression u && (u.NodeType == ExpressionType.Convert || u.NodeType == ExpressionType.ConvertChecked)) body = u.Operand;
if (body is MemberExpression m) return GetColumnName(m);
throw new NotSupportedException(body.ToString());
}
private static string GetColumnName(MemberExpression member) { var path = GetPropertyPath(member); var declaringType = member.Expression?.Type ?? member.Member.ReflectedType;
var meta = MetadataCache.GetTableMetadata(declaringType);
if (meta.PropertyMap.TryGetValue(path, out var col))
return col.ColumnName;
return path.Split('.').Last();
} `
这里用到了 MetadataCache——基于 ConcurrentDictionary<Type, TableMetadata> 的反射缓存。实体类通过特性标注数据库映射:
`csharp [DbTable(“sys_user”)] public class User { [DbPrimaryKey] [DbColumn(“user_id”)] public int UserId { get; set; }
[DbColumn("user_name")]
public string Name { get; set; }
} `
MetadataCache 首次访问某类型时通过反射构建属性到列名的映射字典,后续全部从缓存读取。
四、第二部分:条件表达式解析(核心难点)
条件解析需要处理:逻辑运算符连接、括号分组、闭包变量捕获、string.Contains 方法、布尔属性简写、null 值比较。
4.1 ExpressionVisitor 模式
.NET 自带的 ExpressionVisitor 是一个深度优先遍历表达式树的抽象基类。重写 VisitBinary、VisitMember、VisitMethodCall 等方法,就能在遍历到特定节点时执行自定义逻辑。
csharp private class ConditionVisitor : ExpressionVisitor { public List<QueryCondition> Conditions { get; } = new(); private RelationType? _pendingRelation; private int _currentGroupId; private int _nextGroupId = 1; private RelationType? _groupStartRelation; }
4.2 处理 && / || 逻辑运算符
逻辑运算符的难点在于括号语义。(A && B) || C 和 A && (B || C) 完全不同。在表达式树中,括号是通过子树嵌套隐式表达的。
NeedsGroup 判断子表达式是否需要独立分组——内部逻辑运算符与父级不同时,就需要括号:
csharp private static bool NeedsGroup(Expression expr, ExpressionType parent) { if (expr is BinaryExpression b && (b.NodeType == ExpressionType.AndAlso || b.NodeType == ExpressionType.OrElse)) return b.NodeType != parent; return false; }
VisitBinary 的处理核心是栈式保存/恢复状态:遇到需要分组的子树时,暂存当前 _currentGroupId,分配新分组 ID,递归处理完子树后恢复原状。
`csharp protected override Expression VisitBinary(BinaryExpression node) { if (node.NodeType == ExpressionType.AndAlso || node.NodeType == ExpressionType.OrElse) { var rel = node.NodeType == ExpressionType.AndAlso ? RelationType.AND : RelationType.OR;
// 左子树
if (NeedsGroup(node.Left, node.NodeType))
{
var saved = _currentGroupId;
_currentGroupId = _nextGroupId++;
Visit(node.Left);
_currentGroupId = saved;
}
else Visit(node.Left);
// 设置关系供右子树使用
var prev = _pendingRelation;
_pendingRelation = rel;
// 右子树
if (NeedsGroup(node.Right, node.NodeType))
{
var saved = _currentGroupId;
_currentGroupId = _nextGroupId++;
var savedStart = _groupStartRelation;
_groupStartRelation = rel;
Visit(node.Right);
_groupStartRelation = savedStart;
_currentGroupId = saved;
}
else Visit(node.Right);
_pendingRelation = prev;
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(),
GroupId = _currentGroupId
});
}
}
return base.VisitBinary(node);
} `
4.3 闭包变量提取
当你写 u => u.Age > minAge 时,minAge 并非常量——编译器会把它捕获到闭包类的字段里,表达式树里看到的是 ConstantExpression(closureObj) 再包一层 MemberExpression(MinAge)。
`csharp 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();
} `
最后那个 DynamicInvoke 兜底虽然性能低,但保证了极端情况的兼容性。
4.4 string.Contains 转 LIKE
csharp 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); }
4.5 null 值比较特殊处理
Where(x => x.MiddleName == null) 不能翻译成 WHERE MiddleName = NULL(SQL 中 = NULL 永远是 UNKNOWN),必须用 IS NULL:
csharp private static OperatorType GetOperator(ExpressionType type, object value) => value == null ? (type == ExpressionType.Equal ? OperatorType.IsNull : OperatorType.IsNotNull) : type switch { ExpressionType.Equal => OperatorType.Equals, ExpressionType.NotEqual => OperatorType.NotEquals, // ... _ => throw new NotSupportedException() };
五、PredicateBuilder:动态组合表达式
有时候条件不是一次性写死的,需要根据运行时参数动态拼接:
csharp var expr = PredicateBuilder.True<User>(); if (!string.IsNullOrEmpty(name)) expr = expr.And(u => u.Name.Contains(name)); if (ageMin.HasValue) expr = expr.And(u => u.Age >= ageMin.Value);
原理是参数替换:把右侧 Lambda 的参数节点替换成左侧的,然后把表达式体用 AndAlso / OrElse 连接:
`csharp public static class PredicateBuilder { public static
Expression<Func<T, bool>> True
public static Expression<Func<T, bool>> And<T>(
this Expression<Func<T, bool>> left,
Expression<Func<T, bool>> right)
{
var param = left.Parameters[0];
var rightBody = ReplaceParameter(right.Body, right.Parameters[0], param);
return Expression.Lambda<Func<T, bool>>(
Expression.AndAlso(left.Body, rightBody), param);
}
private static Expression ReplaceParameter(
Expression body, ParameterExpression oldP, ParameterExpression newP)
=> new ParameterReplacerVisitor(oldP, newP).Visit(body);
private class ParameterReplacerVisitor : ExpressionVisitor
{
// 只重写 VisitParameter 做节点替换
protected override Expression VisitParameter(ParameterExpression node)
=> node == _old ? _new : base.VisitParameter(node);
}
} `
六、完整流程示例
业务代码:
csharp db.Query<User>() .Where(u => (u.Age > 18 && u.Status == 1) || u.Name.Contains("张")) .OrderBy(u => u.CreatedAt) .ToList();
解析结果:
sql SELECT * FROM sys_user WHERE (age > @p1 AND status = @p2) OR user_name LIKE CONCAT('%', @p3, '%') ORDER BY created_at ASC
七、实战价值与学习建议
这个 462 行的表达式解析器涵盖了 ORM 表达式解析的所有核心难点:
| 难点 | 本文方案 | EF Core 方案 |
|---|---|---|
| 逻辑运算符分组 | NeedsGroup + 分组ID + 栈式保存/恢复 | 树重写 + SQL 生成流水线 |
| 闭包变量提取 | 反射访问闭包类字段 + DynamicInvoke 兜底 | CompiledQueryCache + 结构化提取 |
| 字符串方法翻译 | 硬编码 switch 映射 | TranslationPlugin |
| 属性→列名映射 | MetadataCache 反射缓存 | Runtime Model + Convention |
| 参数替换 | ParameterReplacerVisitor | ExpressionVisitor 体系 |
学习建议:先用 ILSpy 查看编译后的表达式树结构,再写简化版 ExpressionVisitor 逐步加入功能。
八、总结
表达式树是 LINQ 的根基,也是手写 ORM 的核心技术。本文从 SwitchData.ExpressionParser 真实项目代码出发,展示了从 LINQ Lambda 到结构化查询条件的完整翻译流程:排序解析沿方法调用链向内剥离并通过元数据缓存映射列名;条件解析基于 ExpressionVisitor 深度优先遍历,用分组 ID 还原括号语义,用反射提取闭包变量;PredicateBuilder 通过参数替换实现表达式动态组合。这 462 行代码浓缩了 ORM 表达式解析的大部分核心思想。