# 手写 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());
}

两个细节值得注意:

  1. 从外到内解析,最后反转ThenBy 在外层,OrderBy 在内层,收集到的顺序是反的,所以最后要 Reverse()
  2. 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") 对应 MethodCallExpressionnode.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 中括号内的连接符正确。

四、列名解析与元数据缓存

实体属性名和数据库列名往往不同(如 UserNameuser_name)。MetadataCache 通过反射扫描 DbTableAttributeDbColumnAttribute 等特性,构建类型到 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 提供了 AndOr 扩展方法:

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 表达式树解析器,核心就三点:

  1. 理解表达式树结构:Lambda 编译成 Expression<TDelegate> 后是一棵对象树,BinaryExpressionMemberExpressionMethodCallExpressionConstantExpression 是最常见的节点。
  2. 继承 ExpressionVisitor:重写你关心的节点类型,框架自动递归。把条件提取出来,存进结构化的 QueryCondition 列表。
  3. 处理边界情况:Convert 节点剥离、闭包常量提取、运算符反转、括号分组、空值判断(== nullIS NULL)。

SwitchData 的 ExpressionParser 没有追求支持所有 LINQ 操作(如 SelectJoin),而是聚焦于 Where 条件和 OrderBy 排序——这恰好覆盖了绝大多数业务查询场景。这种”够用就好”的设计思路,在自研基础设施中非常值得借鉴。

如果你正在构建自己的轻量级数据访问层,不妨从这个解析器出发,逐步扩展支持的表达式类型。表达式树没有那么神秘,它只是把代码变成了可以遍历的数据结构而已。