前言
在构建通用数据访问层时,我们经常遇到这样的需求:将用户传入的 LINQ 表达式(Where/OrderBy)翻译为原生 SQL 或结构化查询条件。EF Core 虽然强大,但在需要精细控制 SQL 生成、接入自定义 ORM 框架、或实现动态查询 DSL 时,手写表达式解析器仍是不可替代的技能。
SwitchData 项目中的 ExpressionParser 与
PredicateBuilder
提供了一套生产级的实现方案,覆盖了以下核心场景:
- 排序表达式解析:从
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 组合的起点选
True(true && A=A,空操作) - OR 组合的起点选False(false || 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
都十分方便,是构建通用数据访问层的瑞士军刀。