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

.NET中合并Func/Expression生成EF Core动态GroupBy表达式问询

动态构建EF Core的GroupBy与Select表达式

要实现基于用户选择动态生成EF Core查询的GroupBy和Select参数,核心是将多个独立的属性选择表达式合并为一个返回包含所有选中属性的对象的表达式。你的现有代码尝试用Expression.Add合并,这并不适用于对象组合,需要改为动态创建对象初始化表达式。

核心解决方案:属性表达式合并工具

以下是专门用于合并多属性选择表达式的工具类,它能动态生成包含所有选中属性的匿名类型表达式,EF Core可直接解析并生成对应的SQL:

using System.Linq.Expressions;
using System.Reflection;
using System.Threading;

public static class ExpressionCombiner
{
    public static Expression<Func<T, object>> CombineProperties<T>(params Expression<Func<T, object>>[] propertyExpressions)
    {
        if (propertyExpressions == null || propertyExpressions.Length == 0)
            throw new ArgumentException("至少需要传入一个属性表达式");

        var parameter = propertyExpressions[0].Parameters[0];
        var propertyInfos = new List<MemberInfo>();
        var propertyValues = new List<Expression>();

        foreach (var expr in propertyExpressions)
        {
            // 去除因返回object自动添加的Convert包装
            var body = expr.Body is UnaryExpression unary ? unary.Operand : expr.Body;
            
            if (body is MemberExpression memberExpr)
            {
                propertyInfos.Add(memberExpr.Member);
                propertyValues.Add(Expression.MakeMemberAccess(parameter, memberExpr.Member));
            }
            else
            {
                throw new ArgumentException("仅支持属性访问表达式");
            }
        }

        // 动态创建匿名类型(EF Core可自动识别映射)
        var anonymousType = AnonymousTypeCreator.CreateType(
            propertyInfos.Select(p => p.Name), 
            propertyInfos.Select(p => p.GetMemberType())
        );
        var constructor = anonymousType.GetConstructors().First();
        var bindings = constructor.GetParameters()
            .Select((param, index) => Expression.Bind(anonymousType.GetProperty(param.Name), propertyValues[index]))
            .ToList();

        var initExpression = Expression.New(constructor, propertyValues, bindings);
        return Expression.Lambda<Func<T, object>>(Expression.Convert(initExpression, typeof(object)), parameter);
    }

    private static class AnonymousTypeCreator
    {
        private static readonly AssemblyName _assemblyName = new AssemblyName("DynamicAnonymousTypes");
        private static readonly AssemblyBuilder _assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(_assemblyName, AssemblyBuilderAccess.Run);
        private static readonly ModuleBuilder _moduleBuilder = _assemblyBuilder.DefineDynamicModule(_assemblyName.Name);
        private static int _typeCounter = 0;

        public static Type CreateType(IEnumerable<string> propertyNames, IEnumerable<Type> propertyTypes)
        {
            var typeName = $"AnonymousType_{Interlocked.Increment(ref _typeCounter)}";
            var typeBuilder = _moduleBuilder.DefineType(typeName, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Serializable);

            var properties = propertyNames.Zip(propertyTypes, (name, type) => (name, type)).ToList();
            
            // 创建属性和对应字段
            foreach (var (name, type) in properties)
            {
                var fieldBuilder = typeBuilder.DefineField($"_{name}", type, FieldAttributes.Private);
                var propertyBuilder = typeBuilder.DefineProperty(name, PropertyAttributes.None, type, null);
                
                // Getter方法
                var getterBuilder = typeBuilder.DefineMethod($"get_{name}", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, type, Type.EmptyTypes);
                var getterIl = getterBuilder.GetILGenerator();
                getterIl.Emit(OpCodes.Ldarg_0);
                getterIl.Emit(OpCodes.Ldfld, fieldBuilder);
                getterIl.Emit(OpCodes.Ret);
                
                // Setter方法
                var setterBuilder = typeBuilder.DefineMethod($"set_{name}", MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, null, new[] { type });
                var setterIl = setterBuilder.GetILGenerator();
                setterIl.Emit(OpCodes.Ldarg_0);
                setterIl.Emit(OpCodes.Ldarg_1);
                setterIl.Emit(OpCodes.Stfld, fieldBuilder);
                setterIl.Emit(OpCodes.Ret);
                
                propertyBuilder.SetGetMethod(getterBuilder);
                propertyBuilder.SetSetMethod(setterBuilder);
            }

            // 创建构造函数
            var ctorParams = properties.Select(p => p.type).ToArray();
            var ctorBuilder = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, ctorParams);
            var ctorIl = ctorBuilder.GetILGenerator();
            
            // 构造函数赋值逻辑
            for (int i = 0; i < properties.Count; i++)
            {
                ctorIl.Emit(OpCodes.Ldarg_0);
                ctorIl.Emit(OpCodes.Ldarg, i + 1);
                ctorIl.Emit(OpCodes.Stfld, typeBuilder.GetField($"_{properties[i].name}", BindingFlags.NonPublic | BindingFlags.Instance));
            }
            ctorIl.Emit(OpCodes.Ret);

            return typeBuilder.CreateType()!;
        }
    }

    private static Type GetMemberType(this MemberInfo member)
    {
        return member switch
        {
            PropertyInfo p => p.PropertyType,
            FieldInfo f => f.FieldType,
            _ => throw new ArgumentException("不支持的成员类型")
        };
    }
}

实际使用示例

根据用户选择动态构建GroupBy和Select表达式:

// 收集用户选中的分组列表达式
var groupingExpressions = new List<Expression<Func<EntryLine, object>>>();

if (userSelectedOffice)
{
    groupingExpressions.Add(x => x.Office.Name);
}

if (userSelectedIndustry)
{
    groupingExpressions.Add(x => x.Industry.Name);
}

// 合并为单个GroupBy键表达式
var groupingKey = ExpressionCombiner.CombineProperties(groupingExpressions.ToArray());

// 执行GroupBy查询
var groupedQuery = _context.EntryLines.GroupBy(groupingKey);

// 动态构建Select表达式(以DTO为例)
var selectParam = Expression.Parameter(typeof(IGrouping<object, EntryLine>), "g");
var selectBody = Expression.New(
    typeof(GroupResultDto).GetConstructors().First(),
    new Expression[] {
        selectParam.Property("Key"),
        Expression.Call(typeof(Enumerable), "Count", new[] { typeof(EntryLine) }, selectParam),
        Expression.Call(typeof(Enumerable), "Sum", new[] { typeof(EntryLine) }, selectParam, Expression.Lambda<Func<EntryLine, decimal>>(Expression.Property(Expression.Parameter(typeof(EntryLine)), "Amount"), Expression.Parameter(typeof(EntryLine))))
    },
    typeof(GroupResultDto).GetProperties().Select(p => (MemberInfo)p).ToArray()
);
var selectExpression = Expression.Lambda<Func<IGrouping<object, EntryLine>, GroupResultDto>>(selectBody, selectParam);

// 获取最终结果
var result = groupedQuery.Select(selectExpression).ToList();

// 示例DTO类
public class GroupResultDto
{
    public object GroupKey { get; set; }
    public int TotalCount { get; set; }
    public decimal TotalAmount { get; set; }

    public GroupResultDto(object groupKey, int totalCount, decimal totalAmount)
    {
        GroupKey = groupKey;
        TotalCount = totalCount;
        TotalAmount = totalAmount;
    }
}

关键说明

  1. 动态生成的匿名类型可被EF Core正常解析,生成对应的SQL分组语句。
  2. 你的现有ExpressionMerger适用于函数链式组合(如x => f(g(x))),但不适用于多属性合并,需替换为上述工具类。
  3. Select表达式的构建逻辑与GroupBy类似,可复用属性合并逻辑,或提前定义DTO类简化对象初始化。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 11:30:54