你有没有好奇过,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( Func<IQueryable, IOrderedQueryable> orderBy) { var dummy = Enumerable.Empty().AsQueryable(); var ordered = orderBy(dummy); var result = new OrderedDictionary<string, string>();

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, IOrderedQueryable>——它不立即执行,而是给你一个构建表达式树的机会。把空集合传进去,方法链不会查询数据库,只是返回一棵由 MethodCallExpression 组成的表达式树。排序方法链是嵌套结构(OrderBy 返回的 IOrderedQueryable 又作为 ThenBy 的源),所以需要从外向内逐层剥离,最后 Reverse 回来保证顺序一致。

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() => f => 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 表达式解析的大部分核心思想。