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

如何通过表达式树实现动态指定GroupBy属性的IQueryable查询

动态构建分组查询的IQueryable实现方案

需求背景

现有一个可构建并返回IQueryable<ClassDTO>的静态分组方法,需要改造为支持动态指定分组属性的版本,允许通过字符串参数传入嵌套属性路径(如"OpportunityStage.Name"),动态替换原方法中固定的分组键和ItemGroup取值逻辑。

原静态方法代码:

public IQueryable<ClassDTO> ReportByNestedProperty()
{
    IQueryable<Class> query = this.dbSet;
    
    IQueryable<ClassDTO> groupedQuery =
        from opportunity in query
        group new
        {
            ItemGroup = opportunity.OpportunityStage.Name,
            EstimatedRevenue = opportunity.EstimatedRevenue,
            CostOfLead = opportunity.CostOfLead
        }
        by new
        {
            opportunity.OpportunityStage.Name,
            opportunity.OpportunityStage.Id
        }
        into item
        select new ClassDTO()
        {
            ItemGroup = string.IsNullOrEmpty(item.Key.Name) ? "[Not Assigned]" : item.Key.Name,
            Count = item.Select(z => z.ItemGroup.Name).Count(),
            Commission = item.Sum(z => z.EstimatedRevenue),
            Cost = item.Sum(z => z.CostOfLead),
        };

    return groupedQuery;
}

需要实现的动态方法签名:

public IQueryable<ClassDTO> ReportByNestedProperty(string firstNestedGroupByProperty, string secondNestedGroupByProperty)
{
    // TODO: 用表达式树实现动态逻辑
}

调用示例:

ReportByNestedProperty("OpportunityStage.Name","OpportunityStage.Id")
ReportByNestedProperty("OtherNestedProperty.Name","OtherNestedProperty.Id")
ReportByNestedProperty("OpportunityStage.Name","OpportunityStage.Price")

相关实体及DTO定义:

public class ClassDTO
{
    public ClassDTO() { }

    [Key]
    public string ItemGroup { get; set; }

    public decimal Commission { get; set; }

    public decimal? Cost { get; set; }

    public int Count { get; set; }
}

public partial class Class
{
    public Class()  {   }

    [Key]
    public Guid Id { get; set; }
    public Guid? OpportunityStageId { get; set; }

    [ForeignKey(nameof(OpportunityStageId))]
    [InverseProperty(nameof(OpportunityStage.Class))]
    public virtual OpportunityStage OpportunityStage { get; set; }
}

public partial class OpportunityStage
{
    public OpportunityStage()
    {
        this.Classes = new HashSet<Class>();
    }

    [Key]
    public Guid Id { get; set; }

    public string Name { get; set; }

    [InverseProperty(nameof(Class.OpportunityStage))]
    public virtual ICollection<Class> Classes{ get; set; }
}

实现方案

核心是通过表达式树解析嵌套属性路径,动态构建分组键、分组前的选择项,以及最终的DTO投影逻辑。以下是完整实现:

using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;

public IQueryable<ClassDTO> ReportByNestedProperty(string firstNestedGroupByProperty, string secondNestedGroupByProperty)
{
    IQueryable<Class> query = this.dbSet;

    // 1. 解析嵌套属性路径为表达式
    var parameter = Expression.Parameter(typeof(Class), "opportunity");
    var firstPropertyExpr = GetPropertyExpression(parameter, firstNestedGroupByProperty);
    var secondPropertyExpr = GetPropertyExpression(parameter, secondNestedGroupByProperty);

    // 2. 动态创建分组键的匿名类型,并构建分组键表达式
    var groupKeyType = CreateAnonymousType(
        new[] { firstPropertyExpr.Type, secondPropertyExpr.Type },
        new[] { "Key1", "Key2" }
    );
    var groupKeyCtor = groupKeyType.GetConstructor(new[] { firstPropertyExpr.Type, secondPropertyExpr.Type });
    var groupKeyExpr = Expression.New(groupKeyCtor, firstPropertyExpr, secondPropertyExpr, groupKeyType.GetProperties());

    // 3. 动态创建分组前选择项的匿名类型,并构建选择表达式
    var selectItemType = CreateAnonymousType(
        new[] { firstPropertyExpr.Type, typeof(decimal), typeof(decimal?) },
        new[] { "ItemGroup", "EstimatedRevenue", "CostOfLead" }
    );
    var selectItemCtor = selectItemType.GetConstructor(new[] { firstPropertyExpr.Type, typeof(decimal), typeof(decimal?) });
    var selectItemExpr = Expression.New(
        selectItemCtor,
        new[] {
            firstPropertyExpr,
            Expression.Property(parameter, nameof(Class.EstimatedRevenue)),
            Expression.Property(parameter, nameof(Class.CostOfLead))
        },
        selectItemType.GetProperties()
    );

    // 4. 调用GroupBy方法构建分组查询
    var groupByMethod = typeof(Queryable).GetMethods()
        .Where(m => m.Name == "GroupBy" && m.GetParameters().Length == 3)
        .First()
        .MakeGenericMethod(typeof(Class), selectItemType, groupKeyType);
    var groupedQuery = (IQueryable)groupByMethod.Invoke(
        null,
        new object[] { query, Expression.Lambda(selectItemExpr, parameter), Expression.Lambda(groupKeyExpr, parameter) }
    );

    // 5. 构建分组后的DTO投影表达式
    var groupParameter = Expression.Parameter(groupedQuery.ElementType, "item");
    var key1Prop = Expression.Property(groupParameter, "Key", "Key1");
    var key2Prop = Expression.Property(groupParameter, "Key", "Key2");

    // 处理ItemGroup空值替换逻辑
    var itemGroupExpr = Expression.Condition(
        Expression.Call(typeof(string), "IsNullOrEmpty", Type.EmptyTypes, key1Prop),
        Expression.Constant("[Not Assigned]"),
        key1Prop
    );

    // 构建聚合表达式:Count、Sum(Commission)、Sum(Cost)
    var countExpr = Expression.Call(
        typeof(Queryable), "Count", new[] { selectItemType },
        Expression.Property(groupParameter, "Items")
    );

    var commissionSumExpr = Expression.Call(
        typeof(Queryable), "Sum", new[] { selectItemType },
        Expression.Property(groupParameter, "Items"),
        Expression.Lambda(Expression.Property(Expression.Parameter(selectItemType, "z"), "EstimatedRevenue"), Expression.Parameter(selectItemType, "z"))
    );

    var costSumExpr = Expression.Call(
        typeof(Queryable), "Sum", new[] { selectItemType },
        Expression.Property(groupParameter, "Items"),
        Expression.Lambda(Expression.Property(Expression.Parameter(selectItemType, "z"), "CostOfLead"), Expression.Parameter(selectItemType, "z"))
    );

    // 构建ClassDTO实例化表达式
    var dtoInitExpr = Expression.MemberInit(
        Expression.New(typeof(ClassDTO)),
        Expression.Bind(typeof(ClassDTO).GetProperty(nameof(ClassDTO.ItemGroup)), itemGroupExpr),
        Expression.Bind(typeof(ClassDTO).GetProperty(nameof(ClassDTO.Count)), countExpr),
        Expression.Bind(typeof(ClassDTO).GetProperty(nameof(ClassDTO.Commission)), Expression.Convert(commissionSumExpr, typeof(decimal))),
        Expression.Bind(typeof(ClassDTO).GetProperty(nameof(ClassDTO.Cost)), costSumExpr)
    );

    // 6. 调用Select方法完成投影,返回最终查询
    var selectMethod = typeof(Queryable).GetMethods()
        .Where(m => m.Name == "Select" && m.GetParameters().Length == 2)
        .First()
        .MakeGenericMethod(groupedQuery.ElementType, typeof(ClassDTO));
    var resultQuery = (IQueryable<ClassDTO>)selectMethod.Invoke(
        null,
        new object[] { groupedQuery, Expression.Lambda(dtoInitExpr, groupParameter) }
    );

    return resultQuery;
}

// 辅助方法:解析嵌套属性路径为表达式树
private Expression GetPropertyExpression(Expression parameter, string propertyPath)
{
    var propertySegments = propertyPath.Split('.');
    Expression currentExpr = parameter;
    foreach (var segment in propertySegments)
    {
        currentExpr = Expression.Property(currentExpr, segment);
    }
    return currentExpr;
}

// 辅助方法:动态生成匿名类型用于分组键和选择项
private Type CreateAnonymousType(Type[] propertyTypes, string[] propertyNames)
{
    var assemblyName = new AssemblyName("DynamicAnonymousTypes");
    var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
    var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");

    // 生成唯一类型名避免冲突
    var typeName = $"Anonymous_{Guid.NewGuid():N}";
    var typeBuilder = moduleBuilder.DefineType(typeName, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Serializable);

    // 定义字段和对应的属性
    var fields = new FieldBuilder[propertyTypes.Length];
    for (int i = 0; i < propertyTypes.Length; i++)
    {
        fields[i] = typeBuilder.DefineField($"_{propertyNames[i]}", propertyTypes[i], FieldAttributes.Private);
        var propBuilder = typeBuilder.DefineProperty(propertyNames[i], PropertyAttributes.None, propertyTypes[i], null);

        // Get方法实现
        var getMethod = typeBuilder.DefineMethod($"get_{propertyNames[i]}", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyTypes[i], Type.EmptyTypes);
        var getIl = getMethod.GetILGenerator();
        getIl.Emit(OpCodes.Ldarg_0);
        getIl.Emit(OpCodes.Ldfld, fields[i]);
        getIl.Emit(OpCodes.Ret);

        // Set方法实现
        var setMethod = typeBuilder.DefineMethod($"set_{propertyNames[i]}", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, null, new[] { propertyTypes[i] });
        var setIl = setMethod.GetILGenerator();
        setIl.Emit(OpCodes.Ldarg_0);
        setIl.Emit(OpCodes.Ldarg_1);
        setIl.Emit(OpCodes.Stfld, fields[i]);
        setIl.Emit(OpCodes.Ret);

        propBuilder.SetGetMethod(getMethod);
        propBuilder.SetSetMethod(setMethod);
    }

    // 定义构造函数
    var ctorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, propertyTypes);
    var ctorIl = ctorBuilder.GetILGenerator();
    for (int i = 0; i < propertyTypes.Length; i++)
    {
        ctorIl.Emit(OpCodes.Ldarg_0);
        ctorIl.Emit(OpCodes.Ldarg, i + 1);
        ctorIl.Emit(OpCodes.Stfld, fields[i]);
    }
    ctorIl.Emit(OpCodes.Ret);

    return typeBuilder.CreateType();
}

关键逻辑说明

  1. 属性路径解析:GetPropertyExpression方法将字符串形式的嵌套属性(如"OpportunityStage.Name")逐层解析为表达式树,支持任意多层嵌套。
  2. 动态匿名类型生成:CreateAnonymousType通过反射Emit动态生成匿名类型,用于构建分组键和分组前的选择项,确保EF Core能正确解析并转换为SQL查询。
  3. 分组与投影构建:通过反射调用Queryable.GroupBy和Queryable.Select方法,传入动态构建的表达式树,完整还原原静态方法的分组、聚合、空值替换逻辑。

内容的提问来源于stack exchange,提问作者Jackie

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 01:20:20