很多人每天写 LINQ,却不知道它背后是一棵可以被”拆解翻译”的表达式树。Entity Framework 能把 Where(x => x.Name == "张三") 转成 SQL,靠的就是表达式树解析。但 EF Core 的源码太复杂了,有没有更轻量、更清晰的实现?

本文基于生产项目中的 ExpressionParser,从零实现了一个表达式树解析器,能把 LINQ 的 Where 条件和 OrderBy 排序翻译成数据库查询条件。配合反射构建的元数据缓存,构成了一个自实现 ORM 的核心查询层。

一、整体架构概览

先看一眼完整的查询链路:

graph LR
    A[业务代码<br/>q => q.Where(x => x.Name.Contains("test"))<br/>  .OrderBy(x => x.Age)] --> B[ExpressionParser<br/>ParseConditions / ParseSorting]
    B --> C[QueryCondition 列表<br/>Column, Operator, Value, Relation, GroupId]
    C --> D[SQL 生成器<br/>拼装 WHERE + ORDER BY]
    D --> E[ADO.NET<br/>SqlCommand.ExecuteReader]
    F[MetadataCache<br/>反射构建表/字段元数据] --> B

三个核心组件:

  • MetadataCache:启动时通过反射扫描实体类特性([DbTable][DbColumn]),构建”属性名 → 数据库列名”的映射字典,并缓存 getter/setter 委托。
  • ExpressionParser.ParseConditions:把 Expression<Func<T, bool>> 条件表达式树解析成 List<QueryCondition>
  • ExpressionParser.ParseSorting:把 Func<IQueryable<T>, IOrderedQueryable<T>> 排序表达式链解析成 OrderedDictionary<列名, 排序方向>

二、元数据缓存:反射 + 特性 + ConcurrentDictionary

ExpressionParser 的前提是知道每个 C# 属性对应哪个数据库列。这就是 MetadataCache 干的事:

public class User
{
    [DbTable("SYS_USER")]
    public class UserTable { }

    [DbColumn("USER_ID")]
    [DbPrimaryKey]
    public int UserId { get; set; }

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

    [DbColumn("AGE")]
    public int Age { get; set; }
}

MetadataCache 用 ConcurrentDictionary<Type, TableMetadata> 做线程安全缓存,GetOrAdd 保证只反射一次。核心逻辑:

public static TableMetadata GetTableMetadata(Type type)
{
    return Cache.GetOrAdd(type, CreateMetadata);
}

private static TableMetadata CreateMetadata(Type type)
{
    // 扫描 [DbTable] 特性获取表名
    var tableAttr = type.GetCustomAttribute<DbTableAttribute>();
    
    // 遍历所有属性,扫描 [DbColumn] 特性
    var columns = new List<ColumnMetadata>();
    foreach (var prop in type.GetProperties())
    {
        var colAttr = prop.GetCustomAttribute<DbColumnAttribute>();
        if (colAttr == null) continue;

        columns.Add(new ColumnMetadata
        {
            PropertyName = prop.Name,
            ColumnName = colAttr.Name,          // 数据库列名
            Getter = CreateGetter(prop),        // Expression.Compile 生成的委托
            Setter = CreateSetter(prop),
            // ... 其他字段
        });
    }

    return new TableMetadata
    {
        PropertyMap = columns.ToDictionary(x => x.PropertyName), // 属性名 → ColumnMetadata
        ColumnMap = columns.ToDictionary(x => x.ColumnName),     // 列名 → ColumnMetadata
        // ...
    };
}

注意 CreateGetter / CreateSetter 的巧妙之处:不是每次用反射取值,而是用 Expression.Lambda 动态编译成强类型委托,后续调用几乎零开销。

三、条件表达式解析:ExpressionVisitor 遍历

ParseConditions 接收一个 Expression<Func<T, bool>>,返回 List<QueryCondition>。内部用 ExpressionVisitor 递归遍历表达式树。

3.1 入口与访问器

public static List<QueryCondition> ParseConditions<T>(
    Expression<Func<T, bool>> predicate)
{
    var metadata = MetadataCache.GetTableMetadata<T>();
    var visitor = new ConditionVisitor(metadata);
    visitor.Visit(predicate.Body);   // 从 lambda 体开始遍历
    return visitor.Conditions;
}

ConditionVisitor 继承自 ExpressionVisitor,重写特定节点的 Visit 方法:

private class ConditionVisitor : ExpressionVisitor
{
    public List<QueryCondition> Conditions { get; } = new();
    private readonly TableMetadata _metadata;
    private RelationType? _pendingRelation;  // 待使用的逻辑关系
    private int _currentGroupId;              // 当前括号分组
    private int _nextGroupId = 1;
}

3.2 二元表达式:比较运算符 + 逻辑运算符

VisitBinary 处理两种 BinaryExpression:比较运算(==、!=、>、<)和逻辑运算(&&、||)。

比较运算:提取成员 + 常量

// x.Name == "张三"
//  左: MemberExpression (x.Name)
//  右: ConstantExpression ("张三")
protected override Expression VisitBinary(BinaryExpression node)
{
    if (IsComparison(node.NodeType))  // Equal, NotEqual, GreaterThan...
    {
        var left = StripConvert(node.Left);   // 剥离 Convert 节点
        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 = GetOperator(node.NodeType, value),
                Relation = GetEffectiveRelation(),
                GroupId = _currentGroupId
            });
        }
    }
    // ...
}

关键点:StripConvert 递归剥离所有 Convert / ConvertChecked 节点。因为 C# 编译器常插入类型转换,比如 int?int 比较时会有装箱/拆箱节点,不剥开会匹配不到 MemberExpression。

逻辑运算:&& 与 || + 分组

处理 && / || 时要考虑运算符优先级——|| 包裹 && 时,内部的 && 必须加括号。

// 处理 x.Age > 18 && (x.Name == "张三" || x.Name == "李四")
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 rightNeedGroup = NeedsGroup(node.Right, node.NodeType);
        
        if (rightNeedGroup)
        {
            // 分配新分组 ID,递归访问
            var saved = _currentGroupId;
            _currentGroupId = _nextGroupId++;
            var savedStart = _groupStartRelation;
            _groupStartRelation = relation;  // 分组内第一个条件用 AND/OR
            Visit(node.Right);
            _groupStartRelation = savedStart;
            _currentGroupId = saved;
        }
        else
        {
            _pendingRelation = relation;
            Visit(node.Right);
        }
    }
    return base.VisitBinary(node);
}

NeedsGroup 的逻辑很简洁:如果子表达式的逻辑运算符和当前不同,就需要括号:

// x.Name == "张三" || x.Age > 18  (两个比较,不需要括号)
// x.Name == "张三" || (x.Age > 18 && x.Enabled) (&& 被 || 包裹,需要括号)
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;
}

3.3 方法调用:字符串 Contains/StartsWith/EndsWith

字符串方法调用(x.Name.Contains("test"))是 MethodCallExpression,不是 BinaryExpression。

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,      // LIKE '%test%'
            "StartsWith" => OperatorType.StartsWith,  // LIKE 'test%'
            "EndsWith" => OperatorType.EndsWith,      // LIKE '%test'
            _ => (OperatorType?)null
        };
        if (op.HasValue)
        {
            Conditions.Add(new QueryCondition
            {
                Column = GetColumnName(member),
                Operator = op.Value,
                Value = GetConstantValue(node.Arguments[0]),
                // ...
            });
        }
    }
    return base.VisitMethodCall(node);
}

3.4 常量提取:闭包变量的陷阱

GetConstantValue 是个容易踩坑的地方。Lambda 里引用的外部变量(闭包捕获)不是 ConstantExpression,而是编译器生成的闭包类实例的成员访问

var name = "张三";
Expression<Func<User, bool>> expr = x => x.Name == name;
// 这里的 name 会变成:
// MemberExpression (字段 access)
//   └─ ConstantExpression (闭包类实例)
//   └─ FieldInfo 指向编译器生成的 <>c__DisplayClass 类

所以不能只处理 ConstantExpression,还要处理 MemberExpression + ConstantExpression 组合:

private static object GetConstantValue(Expression expr)
{
    expr = StripConvert(expr);

    // 直接常量:x.IsActive (布尔简写)
    if (expr is ConstantExpression c) return c.Value;

    // 闭包变量:name 被编译器包装成 <>c__DisplayClass.field
    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);
        }
        // 静态成员:DateTime.Now
        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 兜底是关键——万一遇到复杂表达式(比如方法调用返回常量),编译执行总能拿到值。

四、排序表达式解析:OrderBy/ThenBy 链

排序的输入是一个委托 Func<IQueryable<T>, IOrderedQueryable<T>>,调用后得到的 IOrderedQueryable<T>.Expression 是一个嵌套的方法调用链

q => q.OrderBy(x => x.Name).ThenByDescending(x => x.Age)
// 表达式树:
// MethodCallExpression (ThenByDescending)
//   ├─ MethodCallExpression (OrderBy)
//   │    ├─ 参数 q (ParameterExpression)
//   │    └─ Lambda (x => x.Name)
//   └─ Lambda (x => x.Age)

解析逻辑是从外向内循环剥离

public static OrderedDictionary<string, string> ParseSorting<T>(
    Func<IQueryable<T>, IOrderedQueryable<T>> orderBy)
{
    var dummy = Enumerable.Empty<T>().AsQueryable(); // 模拟 IQueryable
    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());
}

巧妙之处:用空集合 Enumerable.Empty<T>().AsQueryable() 作为参数,纯粹是为了让 LINQ 方法能正常调用、返回完整的表达式树——不会有任何数据被访问。

五、PredicateBuilder:动态组合条件

除了解析,还需要能构造表达式。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];
    // 把 right 的参数替换成 left 的(两个 lambda 参数名不同)
    var rightBody = ReplaceParameter(right.Body, right.Parameters[0], parameter);
    return Expression.Lambda<Func<T, bool>>(
        Expression.AndAlso(left.Body, rightBody), parameter);
}

ReplaceParameterExpressionVisitor 把 right 表达式里的 x 全部换成 left 的 x,这样两个表达式才能合并到同一个 lambda 里。

六、实战:一次完整查询

把所有东西串起来看一个真实查询:

public List<User> QueryUsers(string name, int minAge)
{
    // 1. 动态组合 Where 条件
    var predicate = PredicateBuilder.True<User>();
    if (!string.IsNullOrEmpty(name))
        predicate = predicate.And(x => x.Name.Contains(name));
    if (minAge > 0)
        predicate = predicate.And(x => x.Age >= minAge);

    // 2. 解析条件表达式树
    var conditions = ExpressionParser.ParseConditions(predicate);
    // conditions:
    // [{ Column: "USER_NAME", Operator: Contains, Value: "test" },
    //  { Column: "AGE", Operator: GreaterThanOrEqual, Value: 18 }]

    // 3. 解析排序
    var sorting = ExpressionParser.ParseSorting<User>(
        q => q.OrderByDescending(x => x.CreateTime).ThenBy(x => x.Name));
    // sorting: { "CREATE_TIME" -> "DESC", "USER_NAME" -> "ASC" }

    // 4. 生成 SQL + 执行
    var sql = "SELECT * FROM SYS_USER WHERE " + 
              string.Join(" AND ", conditions.Select(BuildWhere)) +
              " ORDER BY " + string.Join(", ", sorting.Select(BuildOrder));
    // SELECT * FROM SYS_USER 
    // WHERE USER_NAME LIKE '%test%' AND AGE >= @p0
    // ORDER BY CREATE_TIME DESC, USER_NAME ASC
    
    // 5. ADO.NET 执行并映射回实体
    return _database.ExecuteDataTable(sql, parameters)
        .MapTo<User>();
}

七、踩过的坑

最后记录几个踩过的雷区:

7.1 Convert 节点陷阱

C# 编译器在 intint? 比较时会插入 Convert / ConvertChecked 节点。直接匹配 MemberExpression 会失败。解决方法就是上面的 StripConvert,递归剥到只剩底层节点。

7.2 闭包变量不是常量

Lambda 里引用的外部变量(闭包捕获)被编译器包装成自动生成的类实例。所以 x => x.Name == name 里的 name 不是 ConstantExpression,而是 MemberExpression + ConstantExpression(包装类实例)的组合。处理方法见 GetConstantValue

7.3 ThenBy 的参数顺序

ParseSorting 从外向内解析,得到的顺序是反的(ThenByDescending → ThenBy → OrderBy),最后必须 Reverse()

7.4 布尔属性简写

x => x.IsActive 这种没有 == true 的简写,Expression 里是一个裸的 MemberExpression。需要在 VisitMember 里特殊处理,自动转成 Equals true

总结

表达式树是 LINQ 的灵魂,EF Core、Dapper、Squirrel 等 ORM 都在做同样的事——把 C# lambda 翻译成 SQL。本文的 ExpressionParser 实现虽然不像 EF Core 那么全面,但覆盖了生产项目 95% 的查询场景,而且代码量小(400 行)、可读性强。

核心思路三步: 1. MetadataCache:反射 + 特性 + 委托编译,建立”属性 → 列”映射 2. ExpressionVisitor:重写 VisitBinary / VisitMethodCall / VisitMember,遍历解析节点 3. 剥离 Convert + 处理闭包变量:解决编译器插入节点导致的匹配失败问题

掌握了表达式树,你就掌握了 LINQ 到 SQL 转换的本质。下次用 EF Core 写复杂查询时,想想它背后的解析器在干什么,你会对 ORM 有更深的理解。