You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何将指定LINQ分组查询语句转换为Expression Tree表达式树

实现思路
  • 采用泛型扩展方法实现通用逻辑,不绑定具体业务实体类型,可直接适配员工位置、员工部门、员工薪资等所有结构相似的业务表。
  • 全程通过Expression Tree手动拼接查询逻辑,既支持内存集合查询,也支持EF/EF Core等需要表达式树转SQL的ORM场景。
  • 核心逻辑拆分4个部分构建:
    1. 构建分组键访问表达式,对应原LINQ中p => p.EmpId的逻辑
    2. 构建GroupBy方法调用表达式,实现按指定键分组
    3. 构建分组内聚合逻辑:取每组内生效日期的最大值,对应原LINQ中q.Max(r => r.EffectiveDt)的逻辑
    4. 构建投影初始化表达式,将分组结果映射为temptable类型,拼接Select方法调用生成最终查询
  • 提供属性选择器作为入参,不硬编码实体属性名,就算后续业务表字段名调整、或者分组键不是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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 12:45:30