如何通过表达式树实现动态指定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(); }
关键逻辑说明
- 属性路径解析:
GetPropertyExpression方法将字符串形式的嵌套属性(如"OpportunityStage.Name")逐层解析为表达式树,支持任意多层嵌套。 - 动态匿名类型生成:
CreateAnonymousType通过反射Emit动态生成匿名类型,用于构建分组键和分组前的选择项,确保EF Core能正确解析并转换为SQL查询。 - 分组与投影构建:通过反射调用
Queryable.GroupBy和Queryable.Select方法,传入动态构建的表达式树,完整还原原静态方法的分组、聚合、空值替换逻辑。
内容的提问来源于stack exchange,提问作者Jackie
相关产品推荐
相关产品推荐

