在现代 ORM 框架(如 EF Core、Dapper)中,LINQ 表达式树是连接业务代码和数据库查询的桥梁。本文将通过一个手写的表达式解析器项目,深入剖析 ExpressionVisitor 的工作原理,实现从 Lambda 表达式到数据库查询条件的完整转换链路。

背景与痛点

在日常开发中,我们经常需要将前端传入的动态查询条件转换为数据库查询。传统做法是拼接 SQL 字符串,但这会带来:

  • 大量手动映射代码,维护困难
  • 类型安全缺失,容易出错
  • 逻辑组合(AND/OR)实现复杂

我们的目标是:让开发者用熟悉的 LINQ 语法编写查询条件,自动转换为 SQL WHERE 子句

整体架构

graph TD
    A[Lambda 表达式] --> B[ExpressionVisitor 遍历]
    B --> C{遍历类型判断}
    C -->|二元表达式| D[解析比较运算和逻辑运算]
    C -->|成员访问| E[提取列名和值]
    C -->|方法调用| F[识别字符串方法 Contains/StartsWith]
    D --> G[生成 QueryCondition]
    E --> G
    F --> G
    G --> H[QueryCondition 列表]
    H --> I[构建 SQL WHERE 子句]

核心数据结构

我们先定义查询条件的结构:

public class QueryCondition
{
    // 数据库列名
    public string Column { get; set; }
    
    // 操作符:Equals, NotEquals, GreaterThan, Contains...
    public OperatorType Operator { get; set; }
    
    // 条件值
    public object Value { get; set; }
    
    // 与前一个条件的逻辑关系:AND / OR / None
    public RelationType Relation { get; set; }
    
    // 分组ID,用于处理括号语义
    public int GroupId { get; set; }
}

public enum OperatorType
{
    Equals, NotEquals, GreaterThan, LessThan,
    GreaterThanOrEqual, LessThanOrEqual,
    Contains, StartsWith, EndsWith,
    IsNull, IsNotNull
}

public enum RelationType
{
    Empty, AND, OR
}

一、排序表达式解析

1.1 需求分析

开发者这样写排序:

var result = queryable.OrderBy(x => x.Name).ThenByDescending(x => x.CreateTime);

我们需要解析出:Name ASC, CreateTime DESC

1.2 实现代码

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.3 关键技巧

空集合模拟:我们不需要真实数据,只需要表达式树结构。用 Enumerable.Empty<T>().AsQueryable() 创建一个零元素的 IQueryable,传入排序委托后,获取其 Expression 树即可。

方法调用链剥离:OrderBy/ThenBy 是链式嵌套的,每一层 MethodCallExpression.Arguments[0] 指向内层的查询表达式。我们逐层剥离,直到遇到非排序方法。

二、条件表达式解析

这是核心部分,我们实现一个自定义的 ExpressionVisitor

2.1 ExpressionVisitor 基础

ExpressionVisitor 是 .NET 内置的表达式树遍历基类,采用访问者模式。我们重写特定方法来处理不同类型的表达式节点:

  • VisitBinary:处理二元表达式(比较运算、逻辑运算)
  • VisitMember:处理成员访问(属性)
  • VisitMethodCall:处理方法调用(如字符串 Contains)

2.2 条件访问器实现

private class ConditionVisitor : ExpressionVisitor
{
    public List<QueryCondition> Conditions { get; } = new();
    
    private readonly TableMetadata _metadata;
    private RelationType? _pendingRelation;
    private int _currentGroupId;
    private int _nextGroupId = 1;
    private RelationType? _groupStartRelation;

    public ConditionVisitor(TableMetadata metadata) => _metadata = metadata;

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

        return base.VisitBinary(node);
    }
}

2.3 处理逻辑分组(括号语义)

这是解析器中最复杂的部分。考虑如下表达式:

x => x.Age > 18 && (x.Name.Contains("张") || x.City == "北京")

需要正确生成 Age > 18 AND (Name LIKE '%张%' OR City = '北京')

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

分组规则: - 父级是 &&,子级也是 && → 无需分组 - 父级是 &&,子级是 || → 需要分组(加括号) - 父级是 ||,子级是 && → 需要分组

2.4 闭包变量提取

Lambda 表达式中的外部变量会被编译器包装成闭包类的字段。例如:

string keyword = "张三";
x => x.Name.Contains(keyword)

编译器会生成类似:x => x.Name.Contains(<闭包>.keyword)

我们需要从闭包对象中提取实际值:

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

2.5 字符串方法识别

ContainsStartsWithEndsWith 方法调用需要转换为 LIKE 语句:

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

三、列名提取与元数据映射

属性名到数据库列名的映射通过特性标注实现:

[DbTable("sys_user")]
public class UserEntity
{
    [DbColumn("user_id")]
    public int UserId { get; set; }
    
    [DbColumn("user_name")]
    public string Name { get; set; }
    
    [DbColumn("user_age")]
    public int Age { get; set; }
}

列名提取逻辑:

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

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

四、PredicateBuilder:动态表达式组合

当需要在运行时动态拼接查询条件时,PredicateBuilder 提供了优雅的 API:

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

    private static Expression ReplaceParameter(Expression body, 
        ParameterExpression oldParam, ParameterExpression newParam)
    {
        return new ParameterReplacerVisitor(oldParam, newParam).Visit(body);
    }

    private class ParameterReplacerVisitor : ExpressionVisitor
    {
        private readonly ParameterExpression _oldParam;
        private readonly ParameterExpression _newParam;

        public ParameterReplacerVisitor(ParameterExpression old, ParameterExpression newParam)
        {
            _oldParam = old;
            _newParam = newParam;
        }

        protected override Expression VisitParameter(ParameterExpression node)
            => node == _oldParam ? _newParam : base.VisitParameter(node);
    }
}

使用示例

var predicate = PredicateBuilder.True<UserEntity>();

if (!string.IsNullOrEmpty(keyword))
    predicate = predicate.And(x => x.Name.Contains(keyword));

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

if (!string.IsNullOrEmpty(city))
    predicate = predicate.Or(x => x.City == city);

var conditions = ExpressionParser.ParseConditions(predicate);
// 生成: [Name LIKE '%关键词%' AND Age >= 18 OR City = '北京']

五、从条件列表生成 SQL

最后一步,将 QueryCondition 列表转换为 SQL WHERE 子句:

public static string BuildWhereClause(List<QueryCondition> conditions)
{
    if (conditions.Count == 0) return "";

    var sql = new StringBuilder();
    var currentGroup = 0;
    var groupStart = true;

    foreach (var cond in conditions)
    {
        // 处理分组变化
        if (cond.GroupId != currentGroup)
        {
            if (!groupStart) sql.Append(")");
            sql.Append(" (");
            currentGroup = cond.GroupId;
            groupStart = true;
        }

        // 添加逻辑关系
        if (!groupStart)
        {
            sql.Append(cond.Relation == RelationType.AND ? " AND " : " OR ");
        }

        // 添加条件
        sql.Append(BuildCondition(cond));
        groupStart = false;
    }

    if (!groupStart) sql.Append(")");
    return sql.ToString();
}

private static string BuildCondition(QueryCondition cond)
{
    return cond.Operator switch
    {
        OperatorType.Equals => $"{cond.Column} = '{cond.Value}'",
        OperatorType.Contains => $"{cond.Column} LIKE '%{cond.Value}%'",
        OperatorType.StartsWith => $"{cond.Column} LIKE '{cond.Value}%'",
        OperatorType.EndsWith => $"{cond.Column} LIKE '%{cond.Value}'",
        OperatorType.GreaterThan => $"{cond.Column} > {cond.Value}",
        OperatorType.LessThan => $"{cond.Column} < {cond.Value}",
        // ... 其他操作符
        _ => $"{cond.Column} = '{cond.Value}'"
    };
}

六、完整使用示例

// 1. 解析排序
var sortDict = ExpressionParser.ParseSorting<UserEntity>(
    q => q.OrderBy(x => x.Name).ThenByDescending(x => x.CreateTime));
// 结果: {"user_name": "ASC", "create_time": "DESC"}

// 2. 解析条件
var conditions = ExpressionParser.ParseConditions<UserEntity>(
    x => x.Age > 18 && (x.Name.Contains("张") || x.City == "北京"));
// 结果:
// [
//   {Column: "user_age", Operator: GreaterThan, Value: 18, Relation: Empty, GroupId: 0},
//   {Column: "user_name", Operator: Contains, Value: "张", Relation: OR, GroupId: 1},
//   {Column: "city", Operator: Equals, Value: "北京", Relation: Empty, GroupId: 1}
// ]

// 3. 生成 SQL
string whereClause = SqlBuilder.BuildWhereClause(conditions);
// 结果: user_age > 18 AND (user_name LIKE '%张%' OR city = '北京')

总结

本文实现了一个完整的 LINQ 表达式到 SQL 条件的解析器,覆盖了以下核心技术点:

  1. ExpressionVisitor 模式:通过重写 VisitBinary、VisitMember、VisitMethodCall 实现对表达式树的遍历
  2. 逻辑分组处理:通过分组 ID 机制正确处理 AND/OR 的括号语义
  3. 闭包变量提取:从编译器生成的闭包类字段中提取实际值
  4. 字符串方法识别:将 Contains/StartsWith/EndsWith 转换为 LIKE 语句
  5. 动态表达式组合:PredicateBuilder 支持运行时动态拼接查询条件
  6. 元数据映射:通过特性标注实现属性名到列名的自动映射

这种模式在不依赖 EF Core 的轻量级 ORM 场景中特别有价值,为自定义数据访问层提供了类型安全的动态查询能力。