在现代 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 字符串方法识别
Contains、StartsWith、EndsWith
方法调用需要转换为 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 条件的解析器,覆盖了以下核心技术点:
- ExpressionVisitor 模式:通过重写 VisitBinary、VisitMember、VisitMethodCall 实现对表达式树的遍历
- 逻辑分组处理:通过分组 ID 机制正确处理 AND/OR 的括号语义
- 闭包变量提取:从编译器生成的闭包类字段中提取实际值
- 字符串方法识别:将 Contains/StartsWith/EndsWith 转换为 LIKE 语句
- 动态表达式组合:PredicateBuilder 支持运行时动态拼接查询条件
- 元数据映射:通过特性标注实现属性名到列名的自动映射
这种模式在不依赖 EF Core 的轻量级 ORM 场景中特别有价值,为自定义数据访问层提供了类型安全的动态查询能力。