C# 多列按表达式树分组

C# 多列按表达式树分组,c#,linq,group-by,C#,Linq,Group By,据邮报报道 感谢Daniel Hilgarth的帮助,我已经实现了分组扩展,我需要帮助为GroupByMany扩展此功能,如下所示 _unitOfWork.MenuSetRepository.Get().GroupBy(“Role.Name”、“MenuText”) 扩展方法 public static IEnumerable<IGrouping<string, TElement>> GroupBy<TElement>(this IEnumerable<

据邮报报道 感谢Daniel Hilgarth的帮助,我已经实现了分组扩展,我需要帮助为GroupByMany扩展此功能,如下所示

_unitOfWork.MenuSetRepository.Get().GroupBy(“Role.Name”、“MenuText”)

扩展方法

public static IEnumerable<IGrouping<string, TElement>> GroupBy<TElement>(this IEnumerable<TElement> elements,string property)
    {
        var parameter = Expression.Parameter(typeof(TElement), "groupCol");
        Expression<Func<TElement, string>> lambda;
        if (property.Split('.').Count() > 1)
        {
            Expression body = null;
            foreach (var propertyName in property.Split('.'))
            {
                Expression instance = body;
                if (body == null)
                    instance = parameter;
                body = Expression.Property(instance, propertyName);
            }
            lambda = Expression.Lambda<Func<TElement, string>>(body, parameter);
        }
        else
        {
            var menuProperty = Expression.PropertyOrField(parameter, property);
            lambda = Expression.Lambda<Func<TElement, string>>(menuProperty, parameter);    
        }

        var selector= lambda.Compile();
       return elements.GroupBy(selector);
    }
公共静态IEnumerable GroupBy(此IEnumerable元素,字符串属性)
{
var参数=表达式参数(typeof(TElement),“groupCol”);
表达lambda;
if(property.Split('.').Count()>1)
{
表达式体=null;
foreach(property.Split('.')中的var propertyName)
{
表达式实例=主体;
if(body==null)
实例=参数;
body=Expression.Property(实例,propertyName);
}
lambda=表达式.lambda(主体,参数);
}
其他的
{
var menuperty=Expression.PropertyOrField(参数,属性);
lambda=Expression.lambda(menuperty,参数);
}
var selector=lambda.Compile();
返回元素.GroupBy(选择器);
}

此答案由两部分组成:

  • 为您的问题提供解决方案
  • 向您介绍
    IEnumerable
    IQueryable
    以及两者之间的差异
  • 第1部分:解决眼前问题的方案 新要求不像其他要求那么容易满足。这样做的主要原因是,通过复合键分组的LINQ查询会导致在编译时创建匿名类型:

    source.GroupBy(x => new { x.MenuText, Name = x.Role.Name })
    
    这将生成一个新类,该类具有编译器生成的名称和两个属性
    MenuText
    name

    在运行时这样做是可能的,但实际上并不可行,因为这将涉及将IL发送到新的动态程序集中

    对于我的解决方案,我选择了不同的方法:
    因为所有涉及的属性似乎都是
    string
    类型,所以我们分组所依据的键只是由分号分隔的属性值的串联。
    因此,我们的代码生成的表达式等价于以下内容:

    source.GroupBy(x => x.MenuText + ";" + x.Role.Name)
    
    实现此目的的代码如下所示:

    private static Expression<Func<T, string>> GetGroupKey<T>(
        params string[] properties)
    {
        if(!properties.Any())
            throw new ArgumentException(
                "At least one property needs to be specified", "properties");
    
        var parameter = Expression.Parameter(typeof(T));
        var propertyExpressions = properties.Select(
            x => GetDeepPropertyExpression(parameter, x)).ToArray();
    
        Expression body = null;
        if(propertyExpressions.Length == 1)
            body = propertyExpressions[0];
        else
        {
            var concatMethod = typeof(string).GetMethod(
                "Concat",
                new[] { typeof(string), typeof(string), typeof(string) });
    
            var separator = Expression.Constant(";");
            body = propertyExpressions.Aggregate(
                (x , y) => Expression.Call(concatMethod, x, separator, y));
        }
    
        return Expression.Lambda<Func<T, string>>(body, parameter);
    }
    
    private static Expression GetDeepPropertyExpression(
        Expression initialInstance, string property)
    {
        Expression result = null;
        foreach(var propertyName in property.Split('.'))
        {
            Expression instance = result;
            if(instance == null)
                instance = initialInstance;
            result = Expression.Property(instance, propertyName);
        }
        return result;
    }
    
    x => string.Concat(
            string.Concat(x.MenuText, ";", x.Role.Name), ";", x.ActionName)
    
    public static IEnumerable<IGrouping<string, TElement>> GroupBy<TElement>(
        this IEnumerable<TElement> source, params string[] properties)
    {
        return source.GroupBy(GetGroupKey<TElement>(properties).Compile());
    }
    
    public static IQueryable<IGrouping<string, TElement>> GroupBy<TElement>(
        this IQueryable<TElement> source, params string[] properties)
    {
        return source.GroupBy(GetGroupKey<TElement>(properties));
    }
    
    这与C#编译器为使用加号连接字符串的表达式生成的表达式相同,因此等价于:

    x => x.MenuText + ";" + x.Role.Name + ";" + x.ActionName
    
    第二部分:教育你 您在问题中展示的扩展方法是一个非常糟糕的想法。
    为什么?嗯,因为它在
    IEnumerable
    上工作。这意味着此group by不是在数据库服务器上执行的,而是在应用程序的内存中本地执行的。此外,后面的所有LINQ子句,如
    Where
    也在内存中执行

    如果您想提供扩展方法,您需要为
    IEnumerable
    (内存中,即LINQ到对象)和
    IQueryable
    (用于在数据库上执行的查询,如LINQ到实体框架)。
    这也是微软选择的方法。对于大多数LINQ扩展方法,存在两种变体:一种在
    IEnumerable
    上工作,另一种在
    IQueryable
    上工作,它们位于两个不同的类和类中。比较这些类中方法的第一个参数

    所以,你想做的是这样的:

    private static Expression<Func<T, string>> GetGroupKey<T>(
        params string[] properties)
    {
        if(!properties.Any())
            throw new ArgumentException(
                "At least one property needs to be specified", "properties");
    
        var parameter = Expression.Parameter(typeof(T));
        var propertyExpressions = properties.Select(
            x => GetDeepPropertyExpression(parameter, x)).ToArray();
    
        Expression body = null;
        if(propertyExpressions.Length == 1)
            body = propertyExpressions[0];
        else
        {
            var concatMethod = typeof(string).GetMethod(
                "Concat",
                new[] { typeof(string), typeof(string), typeof(string) });
    
            var separator = Expression.Constant(";");
            body = propertyExpressions.Aggregate(
                (x , y) => Expression.Call(concatMethod, x, separator, y));
        }
    
        return Expression.Lambda<Func<T, string>>(body, parameter);
    }
    
    private static Expression GetDeepPropertyExpression(
        Expression initialInstance, string property)
    {
        Expression result = null;
        foreach(var propertyName in property.Split('.'))
        {
            Expression instance = result;
            if(instance == null)
                instance = initialInstance;
            result = Expression.Property(instance, propertyName);
        }
        return result;
    }
    
    x => string.Concat(
            string.Concat(x.MenuText, ";", x.Role.Name), ";", x.ActionName)
    
    public static IEnumerable<IGrouping<string, TElement>> GroupBy<TElement>(
        this IEnumerable<TElement> source, params string[] properties)
    {
        return source.GroupBy(GetGroupKey<TElement>(properties).Compile());
    }
    
    public static IQueryable<IGrouping<string, TElement>> GroupBy<TElement>(
        this IQueryable<TElement> source, params string[] properties)
    {
        return source.GroupBy(GetGroupKey<TElement>(properties));
    }
    
    公共静态IEnumerable GroupBy(
    此IEnumerable源,参数字符串[]属性)
    {
    返回source.GroupBy(GetGroupKey(properties.Compile());
    }
    公共静态IQueryable GroupBy(
    此IQueryable源,参数字符串[]属性)
    {
    返回source.GroupBy(GetGroupKey(properties));
    }
    
    我更喜欢使用
    Tuple.Create
    而不是
    String.Concat
    ,但是+1是一个非常好的完整答案。@p.s.w.g:我也考虑过,但我不确定是否所有LINQ提供程序都支持它。它可能不支持,但仍然比字符串串联更可靠。您还可以生成虚拟的“匿名”类型,如
    Tuple
    ,但它会公开属性设置器,因此
    表达式.Bind
    可以工作,而不必像真正的匿名类型那样动态生成程序集。当然,您只能使用最多数量的属性进行分组。@p.s.w.g:如何生成虚拟匿名类型,以便LINQ提供程序支持它?字符串连接是否不可靠完全取决于数据。是的,我会尽量使它们简单,但提供者仍然需要支持通用DTO。