前言

在构建通用数据访问层时,我们经常遇到这样的需求:将用户传入的 LINQ 表达式(Where/OrderBy)翻译为原生 SQL 或结构化查询条件。EF Core 虽然强大,但在需要精细控制 SQL 生成、接入自定义 ORM 框架、或实现动态查询 DSL 时,手写表达式解析器仍是不可替代的技能。

SwitchData 项目中的 ExpressionParserPredicateBuilder 提供了一套生产级的实现方案,覆盖了以下核心场景:

  • 排序表达式解析:从 OrderBy(x => x.Name).ThenByDescending(x => x.Age) 中提取出列名和排序方向
  • 条件表达式解析:将 Where(x => x.Age > 18 && (x.Name.Contains("张") || x.City == "北京")) 翻译为带分组的结构化条件
  • PredicateBuilder:动态拼接多个条件表达式,解决运行时组合查询的难题

本文将逐行剖析其实现原理,并给出可直接复用的代码。


一、整体架构

表达式解析的核心流程可以用下面的流程图表示:

flowchart TD
    A[传入 LINQ 表达式] --> B{类型判断}
    B -->|排序委托 Func<IQ,IOQ>| C[ParseSorting]
    B -->|条件表达式 Expression<Func<T,bool>>| D[ParseConditions]
    
    C --> C1[构造空 IQueryable 追踪表达式树]
    C1 --> C2[遍历 MethodCall 链: OrderBy/ThenBy...]
    C2 --> C3[提取 Lambda → 列名 + 方向]
    C3 --> C4[返回 OrderedDictionary 列名→ASC/DESC]
    
    D --> D1[获取表元数据 MetadataCache]
    D1 --> D2[ConditionVisitor 遍历表达式树]
    D2 --> D3[VisitBinary 处理 && / || / 比较]
    D2 --> D4[VisitMember 处理布尔属性简写]
    D2 --> D5[VisitMethodCall 处理 Contains 等]
    D3 & D4 & D5 --> D6[生成 List QueryCondition 带 GroupId]

二、排序表达式解析:ParseSorting

2.1 设计思路

排序表达式的特点是链式调用OrderBy().ThenBy().ThenByDescending(),每一层都返回 IOrderedQueryable<T>。我们无法直接获取内部信息,但可以利用一个巧妙的技巧:

传入一个空的 IQueryable<T> 作为”诱饵”,执行委托后追踪生成的表达式树,再反向解析出排序规则。

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

2.3 关键点说明

技巧 作用
Enumerable.Empty<T>().AsQueryable() 生成一个零分配的空查询对象,执行委托不会真正查询数据,仅构造表达式树
Arguments[1] 强转 UnaryExpression.Operand C# 编译器会对值类型属性做装箱转换(Expression.Quote 的包装层),必须剥离才能拿到真实的 Lambda
result.Reverse() 调用链是从外到内解析的(最外层 ThenBy 先被访问),需要反转才能得到 ORDER BY 子句中列的正确顺序
OrderedDictionary<string, string> 普通 Dictionary 不保证插入顺序,使用有序字典保证排序优先级正确

2.4 使用示例

var sortDict = ExpressionParser.ParseSorting<Product>(
    q => q.OrderBy(p => p.Category).ThenByDescending(p => p.Price));

// 输出:
// {
//   { "Category", "ASC" },
//   { "Price", "DESC" }
// }

三、条件表达式解析:ParseConditions

条件表达式的复杂程度远高于排序,需要处理: - 比较运算符== != > < >= <=(含 null 语义) - 逻辑运算符&& ||(含括号分组语义) - 布尔属性简写x.IsActive 等价于 x.IsActive == true - 字符串方法Contains / StartsWith / EndsWith - 多层属性路径x.Customer.Name → 对应数据库列名 - 闭包常量var age = 18; Where(x => x.Age > age) 编译器生成类的字段访问

3.1 数据结构

public class QueryCondition
{
    public string Column { get; set; }       // 数据库列名
    public OperatorType Operator { get; set; } // 操作符枚举
    public object Value { get; set; }        // 条件值
    public RelationType Relation { get; set; } // 与前一条件的关系:AND / OR
    public int GroupId { get; set; }         // 分组ID(相同表示同一括号组)
}

3.2 自定义 ExpressionVisitor

解析条件表达式的核心是继承 ExpressionVisitor,重写三种节点访问方法:

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

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

        _pendingRelation = relation;

        // 右子树同理
        var rightNeedGroup = NeedsGroup(node.Right, node.NodeType);
        // ...(对称逻辑,右子树额外需要设置 _groupStartRelation)
        
        return node;
    }
    // 处理比较运算...
}

/// 判断是否需要新分组:子表达式运算符与父级不同
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;
}

括号语义示例: - A && B && C:不需要分组,同一组内关系都是 AND - A || B && C:B 和 C 分到同一组(GroupId=1),A 在 GroupId=0 - (A || B) && C:A 和 B 分到同一组,C 在默认组

比较运算符处理== != > < >= <=):

if (IsComparison(node.NodeType))
{
    var left = StripConvert(node.Left);   // 剥离 Convert 节点
    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);
    }
}

特别注意 null 语义操作符反转

// 值为 null 时,== 翻译为 IS NULL,!= 翻译为 IS NOT NULL
private static OperatorType GetOperator(ExpressionType type, object value) =>
    value == null
        ? (type == ExpressionType.Equal ? OperatorType.IsNull : OperatorType.IsNotNull)
        : type switch
        {
            ExpressionType.Equal => OperatorType.Equals,
            ExpressionType.GreaterThan => OperatorType.GreaterThan,
            // ...
            _ => throw new NotSupportedException()
        };

// 常量在左时反转:如 18 < x.Age → x.Age > 18 → GreaterThan
private static OperatorType GetReversedOperator(ExpressionType type, object value) =>
    // ... GreaterThan ↔ LessThan 互换

3.2.2 VisitMember:布尔属性简写

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.2.3 VisitMethodCall:字符串匹配方法

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.3 常量值提取:兼容闭包与静态成员

这是一个极其容易踩坑的地方。C# 编译器在你引用外部变量时,会生成一个隐藏的闭包类:

var minAge = 18;
Expression<Func<User, bool>> expr = u => u.Age >= minAge;
// 编译后等价于:
// var __c = new <>c__DisplayClass0(); __c.minAge = 18;
// u => u.Age >= __c.minAge   (MemberExpression + ConstantExpression)

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

性能提醒:兜底的 Compile().DynamicInvoke() 在循环调用中会产生大量编译开销,生产环境建议对前两种情况做完整覆盖后再使用。


四、列名映射:属性路径 → 数据库列名

GetColumnName 支持多层属性路径元数据查表

private static string GetColumnName(MemberExpression member)
{
    var path = GetPropertyPath(member);  // 递归构造 "Customer.Name"
    var declaringType = member.Expression?.Type ?? member.Member.ReflectedType;

    // 1) 优先查完整路径映射(自定义特性)
    var meta = MetadataCache.GetTableMetadata(declaringType);
    if (meta.PropertyMap.TryGetValue(path, out var col))
        return col.ColumnName;

    // 2) 若为导航属性,逐层跳转到导航类型再查
    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;
    }

    // 3) 降级:直接用属性名作为列名(约定优于配置)
    return parts.Last();
}

五、PredicateBuilder:动态表达式组合器

当查询条件在运行时才确定(如用户勾选多个筛选框),需要将多个表达式用 AND / OR 串联。直接拼接 Lambda 会遇到参数不一致的问题(每个 Lambda 的参数表达式是不同实例),需要用 ParameterReplacerVisitor 统一参数。

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];
        // 将 right 的参数替换为 left 的参数实例,否则编译时报 "参数未绑定"
        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 class ParameterReplacerVisitor : ExpressionVisitor
    {
        private readonly ParameterExpression _oldParam, _newParam;
        public ParameterReplacerVisitor(ParameterExpression oldParam, ParameterExpression newParam)
        {
            _oldParam = oldParam; _newParam = newParam;
        }
        protected override Expression VisitParameter(ParameterExpression node)
            => node == _oldParam ? _newParam : base.VisitParameter(node);
    }
}

使用示例:动态拼接多条件

var predicate = PredicateBuilder.True<Product>(); // 用 True 作为 AND 的起点

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

if (minPrice.HasValue)
    predicate = predicate.And(p => p.Price >= minPrice.Value);

if (categories?.Count > 0)
{
    var sub = PredicateBuilder.False<Product>(); // 用 False 作为 OR 的起点
    foreach (var c in categories)
        sub = sub.Or(p => p.Category == c);
    predicate = predicate.And(sub);
}

var list = db.Query<Product>().Where(predicate).ToList();

起点选择技巧: - AND 组合的起点选 Truetrue && A = A,空操作) - OR 组合的起点选 Falsefalse || A = A,空操作)


六、实战:完整查询示例

// 1. 定义排序
var orderBy = ExpressionParser.ParseSorting<User>(
    q => q.OrderBy(u => u.Department).ThenByDescending(u => u.CreateTime));

// 2. 定义条件(含括号分组)
var keyword = "王";
Expression<Func<User, bool>> predicate = u =>
    u.State == UserState.Active &&
    (u.Name.Contains(keyword) || u.Email.EndsWith("@example.com"));

// 3. 翻译为结构化条件
var conditions = ExpressionParser.ParseConditions(predicate);

/*  conditions 输出:
 *  +-----+----------+-----------+----------+----------+---------+
 *  | #   | Column   | Operator  | Value    | Relation | GroupId |
 *  +-----+----------+-----------+----------+----------+---------+
 *  | 0   | State    | Equals    | Active   | Empty    | 0       |
 *  | 1   | Name     | Contains  | 王       | AND      | 1       |
 *  | 2   | Email    | EndsWith  | @...com  | OR       | 1       |
 *  +-----+----------+-----------+----------+----------+---------+
 *  
 *  对应 SQL:WHERE State = @p0 AND (Name LIKE '%'+@p1+'%' OR Email LIKE '%'+@p2)
 */

七、总结与最佳实践

技术点 最佳实践
排序解析 使用 OrderedDictionary 保证列顺序;记得 Reverse 还原优先级
括号分组 NeedsGroup 只在运算符变化时切分组,GroupStartRelation 标记分组首条件的前缀逻辑符
null 比较 任何比较前先检查 value 是否为 null,== null 翻译成 IS NULL 而非 = NULL
成员/常量位置 TryExtractMemberAndConstant 左右都要试,常量在左需反转操作符
闭包值提取 优先处理 MemberExpression + ConstantExpression 组合,避免兜底 Compile 的高开销
PredicateBuilder 切记替换参数表达式!AND 用 True 起步,OR 用 False 起步
导航属性列名 先查完整路径映射,再降级到逐层跳转后的最后一级属性,最后 fallback 属性名

这套表达式解析器的设计体现了分层解耦的思想:ExpressionVisitor 负责遍历表达式树,MetadataCache 负责属性→列名映射,QueryCondition 作为中间数据结构与具体数据库无关。下游接 ADO.NET、SqlSugar 或自研 ORM 都十分方便,是构建通用数据访问层的瑞士军刀。