如何将指定LINQ分组查询语句转换为Expression Tree表达式树
实现思路
- 采用泛型扩展方法实现通用逻辑,不绑定具体业务实体类型,可直接适配员工位置、员工部门、员工薪资等所有结构相似的业务表。
- 全程通过Expression Tree手动拼接查询逻辑,既支持内存集合查询,也支持EF/EF Core等需要表达式树转SQL的ORM场景。
- 核心逻辑拆分4个部分构建:
- 构建分组键访问表达式,对应原LINQ中
p => p.EmpId的逻辑 - 构建GroupBy方法调用表达式,实现按指定键分组
- 构建分组内聚合逻辑:取每组内生效日期的最大值,对应原LINQ中
q.Max(r => r.EffectiveDt)的逻辑 - 构建投影初始化表达式,将分组结果映射为
temptable类型,拼接Select方法调用生成最终查询
- 构建分组键访问表达式,对应原LINQ中
- 提供属性选择器作为入参,不硬编码实体属性名,就算后续业务表字段名调整、或者分组键不是EmpId,也可以直接复用方法。
完整实现代码
using System.Linq.Expressions; using System.Reflection; public static class QueryExtensions { // 缓存反射获取的泛型方法定义,避免每次调用重复反射 private static readonly MethodInfo GroupByMethod = typeof(Queryable).GetMethods() .First(m => m.Name == nameof(Queryable.GroupBy) && m.GetParameters().Length == 2); private static readonly MethodInfo SelectMethod = typeof(Queryable).GetMethods() .First(m => m.Name == nameof(Queryable.Select) && m.GetParameters().Length == 2); private static readonly MethodInfo MaxMethod = typeof(Enumerable).GetMethods() .First(m => m.Name == nameof(Enumerable.Max) && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType && m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>)); /// <summary> /// 通用查询:按指定键分组,取每组最大生效日期,投影为temptable结果 /// </summary> /// <typeparam name="TSource">业务实体类型</typeparam> /// <param name="source">数据源</param> /// <param name="groupKeySelector">分组键选择器,例如p => p.EmpId</param> /// <param name="effectiveDateSelector">生效日期属性选择器,例如p => p.EffectiveDt</param> public static IQueryable<temptable> GetLatestRecordByKey<TSource>( this IQueryable<TSource> source, Expression<Func<TSource, uint?>> groupKeySelector, Expression<Func<TSource, DateTime>> effectiveDateSelector) { // 1. 构建GroupBy(p => p.分组键) ParameterExpression sourceParam = Expression.Parameter(typeof(TSource), "p"); MemberExpression keyMember = GetMemberFromExpression(groupKeySelector.Body); LambdaExpression keyLambda = Expression.Lambda( Expression.Property(sourceParam, keyMember.Member.Name), sourceParam); MethodCallExpression groupCall = Expression.Call( instance: null, method: GroupByMethod.MakeGenericMethod(typeof(TSource), typeof(uint?)), arguments: source.Expression, Expression.Quote(keyLambda)); // 2. 构建分组内Max(r => r.生效日期) ParameterExpression groupParam = Expression.Parameter(typeof(IGrouping<uint?, TSource>), "q"); ParameterExpression groupItemParam = Expression.Parameter(typeof(TSource), "r"); MemberExpression dateMember = GetMemberFromExpression(effectiveDateSelector.Body); LambdaExpression maxLambda = Expression.Lambda( Expression.Property(groupItemParam, dateMember.Member.Name), groupItemParam); MethodCallExpression maxCall = Expression.Call( instance: null, method: MaxMethod.MakeGenericMethod(typeof(DateTime)), arguments: groupParam, maxLambda); // 3. 构建Select投影:q => new temptable { Id = q.Key.Value, EffectiveDt = max结果 } MemberInitExpression initTemptable = Expression.MemberInit( newExpression: Expression.New(typeof(temptable)), bindings: [ Expression.Bind( member: typeof(temptable).GetProperty(nameof(temptable.Id))!, expression: Expression.Property(groupParam, nameof(IGrouping<uint?, TSource>.Key), "Value")), Expression.Bind( member: typeof(temptable).GetProperty(nameof(temptable.EffectiveDt))!, expression: maxCall) ]); LambdaExpression selectLambda = Expression.Lambda(initTemptable, groupParam); // 4. 拼接最终Select调用 MethodCallExpression finalCall = Expression.Call( instance: null, method: SelectMethod.MakeGenericMethod(typeof(IGrouping<uint?, TSource>), typeof(temptable)), arguments: groupCall, Expression.Quote(selectLambda)); return source.Provider.CreateQuery<temptable>(finalCall); } // 辅助方法:解析属性选择表达式,兼容隐式类型转换生成的UnaryExpression节点 private static MemberExpression GetMemberFromExpression(Expression expr) { return expr switch { MemberExpression me => me, UnaryExpression ue when ue.NodeType is ExpressionType.Convert or ExpressionType.ConvertChecked => (MemberExpression)ue.Operand, _ => throw new ArgumentException("请传入直接指向实体属性的选择表达式") }; } }
使用示例
// 1. 员工位置表调用 List<tblEmpLocation> empLocationList = new List<tblEmpLocation>(); List<temptable> empLocationLatest = empLocationList .AsQueryable() .GetLatestRecordByKey( groupKeySelector: p => p.EmpId, effectiveDateSelector: p => p.EffectiveDt) .ToList(); // 2. 其他同结构表(如员工薪资表tblEmpSalary、员工部门表tblEmpDept)调用,无需修改扩展方法 // List<tblEmpSalary> empSalaryList = new List<tblEmpSalary>(); // List<temptable> empSalaryLatest = empSalaryList // .AsQueryable() // .GetLatestRecordByKey(p => p.EmpId, p => p.EffectiveDt) // .ToList();
补充说明:如果数据源中存在分组键(如EmpId)为null的脏数据,可以在GroupBy调用后拼接Where方法过滤
q => q.Key != null的逻辑,避免访问q.Key.Value时抛出空引用异常,拼接方式和GroupBy、Select的构建逻辑一致。
内容的提问来源于stack exchange,提问作者Prabhakar
相关产品推荐
相关产品推荐

