在日常的 .NET 开发中,我们经常使用 LINQ 来查询数据。Where、OrderBy、Select 这些扩展方法让我们用一种声明式的方式操作集合。但你有没有想过,当我们把 LINQ 表达式传给自定义的数据访问层时,如何从中提取出数据库查询所需的字段名、操作符和值?

本文将基于 SwitchData 项目中的 ExpressionParser 实现,深入剖析 C# 表达式树(Expression Tree)的解析机制,手把手教你构建一个将 LINQ 表达式转换为数据库查询条件的解析器。

一、表达式树基础

什么是表达式树

表达式树是将 lambda 表达式表示为数据结构(树状结构)而非编译后的委托的一种机制。每个节点都是一个 Expression 对象,代表一个操作(常量、成员访问、方法调用、二元运算等)。

// 普通 lambda:编译为委托,直接执行
Func<int, bool> isAdult = age => age >= 18;

// 表达式树 lambda:保存为 Expression 对象,可以在运行时分析
Expression<Func<int, bool>> isAdultExpr = age => age >= 18;

当我们将 Expression> 作为参数传递时,IDE 会识别它为表达式树而非普通委托。这正是 ORM 框架(如 EF Core、Dapper 扩展)能够将 LINQ 转换为 SQL 的基础。

核心表达式类型

在 System.Linq.Expressions 命名空间下,常见的表达式节点包括:

类型 说明 示例
ConstantExpression 常量值 18, "admin"
MemberExpression 成员访问 x.Name, x.IsActive
BinaryExpression 二元运算 x >= 18, x && y
MethodCallExpression 方法调用 x.Name.Contains("a")
UnaryExpression 一元运算 Convert(类型转换)
LambdaExpression Lambda 表达式 x => x.Name

二、项目背景:从 LINQ 到数据库查询

需求场景

SwitchData 项目是一个 C#/.NET 的数据采集与分析平台,内置了一套轻量级 ORM。在查询数据时,上层业务代码希望能够用 LINQ 风格的表达式来声明查询条件:

// 业务代码:用 LINQ 表达式声明查询条件
var conditions = ExpressionParser.ParseConditions<User>(
    u => u.Age >= 18 && u.Name.Contains("admin") || u.IsActive);

var sorting = ExpressionParser.ParseSorting<User>(
    q => q.OrderBy(u => u.Name).ThenByDescending(u => u.Age));

解析结果需要转化为数据库查询所需的结构化数据:

// 解析后的条件列表
// [{ Column: "age", Operator: GreaterThanOrEqual, Value: 18, Relation: AND, GroupId: 1 },
//  { Column: "name", Operator: Contains, Value: "admin", Relation: AND, GroupId: 1 },
//  { Column: "is_active", Operator: Equals, Value: true, Relation: OR, GroupId: 0 }]

// 解析后的排序
// { "name": "ASC", "age": "DESC" }

整体架构

ExpressionParser 采用了经典的 访问者模式(Visitor Pattern),内部类 ConditionVisitor 继承自 ExpressionVisitor,负责遍历表达式树并生成 QueryCondition 列表。

graph TD A[LINQ 表达式] --> B{ExpressionParser} B -->|ParseSorting| C[MethodCallExpression 链遍历] B -->|ParseConditions| D[ConditionVisitor 访问者] C --> E[OrderedDictionary 列名→排序方向] D --> F[QueryCondition 列表] F --> G[数据库查询构建器] E --> G

三、核心实现:排序解析

解析 OrderBy/ThenBy 表达式

排序表达式的解析相对直接。ParseSorting 方法接收一个 Func, IOrderedQueryable\> 委托,我们用空集合模拟 IQueryable 来获取表达式树:

public static OrderedDictionary<string, string> ParseSorting<T>(
    Func<IQueryable<T>, IOrderedQueryable<T>> orderBy)
{
    if (orderBy == null) return new OrderedDictionary<string, string>();

    // 用空集合模拟 IQueryable,仅用于获取表达式树
    var dummy = Enumerable.Empty<T>().AsQueryable();
    var ordered = orderBy(dummy);
    var result = new OrderedDictionary<string, string>();

    // 从表达式中剥离方法调用链:OrderBy/ThenBy 等
    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. 空集合模拟:Enumerable.Empty().AsQueryable() 创建一个零元素的 IQueryable,仅用于获取表达式树结构
  2. 方法调用链遍历:OrderBy 和 ThenBy 形成链式结构,通过 MethodCallExpression 逐层向内解析
  3. Reverse 反转:由于是从外向内解析(OrderBy 在外,ThenBy 在内),最终需要反转列表以获得正确的排序优先级

列名提取的递归逻辑

GetColumnName 方法负责从表达式节点中提取数据库列名,它处理了多种情况:

private static string GetColumnName(Expression body)
{
    // 递归剥离类型转换(Convert/ConvertChecked)
    while (body is UnaryExpression unary &&
           (unary.NodeType == ExpressionType.Convert ||
            unary.NodeType == ExpressionType.ConvertChecked))
        body = unary.Operand;

    if (body is MemberExpression member)
        return GetColumnName(member);

    throw new NotSupportedException($"不支持的表达式: {body}");
}

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

// 递归获取属性路径,如 "Customer.Name"
private static string GetPropertyPath(MemberExpression expr)
{
    if (expr.Expression is MemberExpression inner)
        return GetPropertyPath(inner) + "." + expr.Member.Name;
    return expr.Member.Name;
}

四、核心实现:条件解析

条件解析是整个解析器最复杂的部分。ConditionVisitor 类继承 ExpressionVisitor,重写了多个访问方法来处理不同类型的表达式节点。

表达式遍历总览

graph TD A[Expression 树] --> B{ConditionVisitor} B --> C[VisitBinary 处理 && || 和比较运算] B --> D[VisitMember 处理布尔属性简写] B --> E[VisitMethodCall 处理字符串方法调用] C --> F{NodeType} F -->|AndAlso / OrElse| G[逻辑分组处理] F -->|比较运算| H[提取成员+常量] D --> I[bool属性 → Equals true] E --> J[Contains/StartsWith/EndsWith]

处理逻辑运算符(&& 和 ||)

VisitBinary 方法处理两种情况:逻辑运算符(&&、||)和比较运算符(==、>、< 等)。

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;

        // 左子树是否需要括号
        var leftNeedGroup = NeedsGroup(node.Left, node.NodeType);
        if (leftNeedGroup)
        {
            var saved = _currentGroupId;
            _currentGroupId = _nextGroupId++;
            Visit(node.Left);
            _currentGroupId = saved;
        }
        else
        {
            Visit(node.Left);
        }

        // 右子树处理
        var prevRelation = _pendingRelation;
        _pendingRelation = relation;

        var rightNeedGroup = NeedsGroup(node.Right, node.NodeType);
        if (rightNeedGroup)
        {
            var saved = _currentGroupId;
            _currentGroupId = _nextGroupId++;
            var savedStart = _groupStartRelation;
            _groupStartRelation = relation;
            Visit(node.Right);
            _groupStartRelation = savedStart;
            _currentGroupId = saved;
        }
        else
        {
            Visit(node.Right);
        }

        _pendingRelation = prevRelation;
        return node;
    }

    // 处理比较运算
    if (IsComparison(node.NodeType))
    {
        // ...
    }
    return base.VisitBinary(node);
}

逻辑分组的核心思想:

当表达式为 (A && B) || C 时,&& 和 || 交替出现导致语义变化。NeedsGroup 方法判断子表达式是否需要分组:

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;
}

分组 ID(GroupId)用于在 SQL 中添加括号。同一分组的条件会被括号包裹:

-- 输入: u.Age >= 18 && (u.Name.Contains("admin") || u.IsVIP)
-- 输出: WHERE age >= 18 AND (name LIKE '%admin%' OR is_vip = 1)

处理比较运算

比较运算处理的关键是从表达式两侧提取成员访问和常量值:

if (IsComparison(node.NodeType))
{
    // 剥离左右两边的 Convert 节点
    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);
    }
}

常量值提取的艺术

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

为什么需要这么复杂? 当 lambda 表达式引用外部变量时,C# 编译器会生成闭包类将变量包装为字段。例如:

int minAge = 18;
Expression<Func<User, bool>> expr = u => u.Age >= minAge;

编译器生成的表达式树中,minAge 不再是一个简单的常量,而是一个 MemberExpression(访问闭包对象的字段)。GetConstantValue 能够正确识别这种闭包结构并提取实际值。

处理特殊表达式类型

布尔属性简写: VisitMember 支持 u.IsActive 这种布尔属性简写,自动转换为 IsActive == true:

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(),
            GroupId = _currentGroupId
        });
        return node;
    }
    return base.VisitMember(node);
}

字符串方法调用: VisitMethodCall 处理 Contains、StartsWith、EndsWith 等字符串方法:

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);
}

五、元数据驱动的列名映射

ExpressionParser 并不是直接用属性名作为数据库列名,而是通过 MetadataCache 进行映射。这使得代码和数据库解耦:

// 实体类定义
[DbTable("sys_user")]
public class User
{
    [DbColumn("user_id")]
    [DbPrimaryKey]
    public int Id { get; set; }

    [DbColumn("user_name")]
    public string Name { get; set; }

    [DbColumn("is_active")]
    public bool IsActive { get; set; }
}

当解析 u.Name 时,GetColumnName 会通过 MetadataCache 查找 PropertyMap["Name"],返回 ColumnMetadata 对象中的 ColumnName(即 "user_name"),最终生成正确的 SQL 列名。

六、PredicateBuilder:动态组合表达式

PredicateBuilder 提供了在运行时动态组合条件表达式的能力:

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);
    }
}

使用示例:

// 动态构建查询条件
Expression<Func<User, bool>> predicate = PredicateBuilder.True<User>();

if (!string.IsNullOrEmpty(name))
    predicate = predicate.And(u => u.Name.Contains(name));

if (minAge.HasValue)
    predicate = predicate.And(u => u.Age >= minAge.Value);

if (onlyActive)
    predicate = predicate.And(u => u.IsActive);

var conditions = ExpressionParser.ParseConditions(predicate);

关键技术点: ReplaceParameter 使用了另一个 ExpressionVisitor(ParameterReplacerVisitor)来统一两个表达式的参数引用,确保组合后的表达式树结构正确。

七、实战应用:完整流程

下面是一个完整的使用示例,展示了从 LINQ 表达式到 SQL 查询的完整链路:

// 1. 定义查询条件
Expression<Func<User, bool>> condition =
    u => (u.Age >= 18 && u.Name.Contains("admin")) || u.IsActive;

// 2. 定义排序规则
Func<IQueryable<User>, IOrderedQueryable<User>> orderBy =
    q => q.OrderBy(u => u.Name).ThenByDescending(u => u.Age);

// 3. 解析表达式
var conditions = ExpressionParser.ParseConditions(condition);
var sorting = ExpressionParser.ParseSorting(orderBy);

// 4. 生成 SQL
var sqlBuilder = new SqlBuilder();
sqlBuilder.AddConditions(conditions);
sqlBuilder.AddSorting(sorting);

// 5. 执行查询
var results = db.Query<User>(sqlBuilder.Build());

解析过程中各阶段的数据转换:

// 输入表达式树
// (u.Age >= 18 && u.Name.Contains("admin")) || u.IsActive

// ConditionVisitor 生成的 QueryCondition 列表
// [{ Column: "age",   Operator: GreaterThanOrEqual, Value: 18,       Relation: AND,  GroupId: 1 },
//  { Column: "name",  Operator: Contains,          Value: "admin",    Relation: AND,  GroupId: 1 },
//  { Column: "is_active", Operator: Equals,        Value: true,       Relation: OR,   GroupId: 0 }]

// 最终生成的 SQL(带分组括号)
// SELECT * FROM sys_user
// WHERE (age >= @p1 AND name LIKE @p2) OR is_active = @p3
// ORDER BY user_name ASC, age DESC

八、设计亮点与注意事项

设计亮点

  1. 访问者模式:ConditionVisitor 继承 ExpressionVisitor,通过重写特定方法实现关注点分离,每种表达式类型的处理逻辑独立清晰
  2. 分组语义支持:通过 GroupId 追踪括号嵌套,正确处理 (A && B) || C 等混合逻辑表达式
  3. 元数据映射:属性名到列名的映射由 MetadataCache 统一管理,实体类通过特性标注即可完成映射
  4. 闭包兼容:GetConstantValue 正确处理闭包变量、静态成员等多种常量引用场景
  5. 降级策略:当常量提取失败时,使用 Expression.Lambda(expr).Compile().DynamicInvoke() 作为兜底方案

注意事项

  1. 性能考虑:Expression.Lambda(expr).Compile().DynamicInvoke() 涉及动态编译,仅作为兜底方案,实际应尽量通过反射提取常量
  2. 表达式白名单:ConditionVisitor 仅支持特定表达式类型,不支持的表达式会抛出 NotSupportedException,需要在上层做好异常处理
  3. 类型转换:CLR 生成的表达式树常包含 Convert 节点(值类型转换),解析时必须正确剥离
  4. 空值处理:== null 和 != null 需要特殊处理为 IS NULL / IS NOT NULL,而非普通等于/不等于

九、总结

表达式树是 C# 中一项强大但常被忽视的特性。本文基于 SwitchData 项目的 ExpressionParser 实现,详细剖析了如何构建一个将 LINQ 表达式转换为数据库查询条件的解析器。

核心要点回顾:

  • 表达式树解析:通过 ExpressionVisitor 遍历表达式树,识别并转换各种节点类型
  • 排序解析:遍历 MethodCallExpression 链提取排序列和方向
  • 条件解析:处理二元运算、成员访问、方法调用等多种表达式类型
  • 分组语义:用 GroupId 跟踪逻辑分组,正确处理括号嵌套
  • 元数据映射:通过反射和特性标注实现属性名到列名的映射
  • 动态组合:PredicateBuilder 提供运行时动态组合条件的能力

这套解析器已在生产环境中稳定运行,支撑着数据采集平台的查询引擎。如果你正在构建自己的 ORM 或数据访问层,不妨参考这种表达式解析的设计思路。

参考资料