很多人每天写
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);
}
ReplaceParameter 用 ExpressionVisitor 把
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# 编译器在 int 与 int? 比较时会插入
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 有更深的理解。