IEnumerable Group 按用户指定的动态键列表

IEnumerable Group By user specified dynamic list of keys

我有一个class喜欢

public class Empolyee
{
    public string Designation {get ;set;}
    public string Discipline {get ;set;}
    public int Scale {get ;set;}
    public DateTime DOB {get ;set;}
    public int Sales {get ;set;}
}

并在可枚举的 say

中记录所有员工
List<Employee> Employees;

和字符串键列表,例如

var Keys = new List<string>()
{
    "Designation",
    "Scale",
    "DOB"
};

假设列表 "Keys" 的元素是用户指定的,用户可以不指定或指定多个关键元素。

现在我想使用列表 "Keys" 中指定的键对所有 "Employees" 进行分组,并且 select 仅 "Keys" 中指定的属性加上每个组的销售额总和.

我尝试使用的 3 个解决方案中,以下看起来适用但无法使用,因为不知道列表 "Keys" 将如何转换为匿名类型

Employees.GroupBy(e => new { e.Key1, e.Key2, ... })
    .Select(group => new {
        Key1 = group.Key.Key1,
        Key2 = group.Key.Key2,
        ...
        TotalSales = group.Select(employee => employee.Sales).Sum()
    });

您可能需要 Dynamic LINQ 之类的东西,以便您可以将键和投影值指定为字符串。

查看一些分组和投影示例:

  • How to use GroupBy using Dynamic LINQ
  • Dynamic LINQ GroupBy Multiple Columns

如果您事先不知道关键属性的数量,静态编译的匿名类型不会让您走得太远。相反,您需要为每个组的键创建一个数组,因为键属性的数量是动态的。

首先,您需要将字符串映射到 属性 个值:

public object[] MapProperty(string key, Employee e)
{
    switch (k) {
       case "Designation" : return e.Designation;
       case "DOB" : return e.Dob;
       // etc
    }
}

然后您必须对数组进行分组和比较,确保使用自定义 IEqualityComparer 实现来比较每个数组的元素。您可以使用 this answer 中的 ArrayEqualityComparer<T>

var comparer = new ArrayEqualityComparer<object>();
Employees.GroupBy(e => Keys.Select(k => MapProperty(k, e)).ToArray(), e => e, comparer)
   .Select(group => new {
        Keys = group.Key,
        TotalSales = group.Select(employee => employee.Sales).Sum()
    })

不确定这是否是您想要的,但您可以 select 所有可用的键作为一个新列表,然后加入它们。

void Main()
{
    var employees = new List<Employee>()
    {
        new Employee{
            Name = "Bob",
            Sales = 1,
            Keys = { "A", "B" }
        },
        new Employee{
            Name = "Jane",
            Sales = 2,
            Keys = { "A", "C" }
        }
    };

    var grouping = (from e in employees
            from k in employees.SelectMany(s => s.Keys).Distinct()
            where e.Keys.Contains(k)                        
            select new          
            {
                e.Name,
                e.Sales,
                Key = k         
            })
            .GroupBy(a => a.Key)
            .Select(g => new { Key = g.Key, TotalSales = g.Select(a => a.Sales).Sum() });           
}


public class Employee
{
    public int Sales { get; set; }
    public string Name { get; set; }
    public List<string> Keys { get; set;}

    public Employee()
    {
        Keys = new List<string>();
    }
}

https://dotnetfiddle.net/jAg22Z

它不是特别干净,但可以整理一下 - 我刚刚使用一个字符串作为键,因为它为您提供了 GroupBy 需要的所有 hashcode/equality,但您可以创建一个 class 来以对对象更友好的方式执行此操作。

如果你真的想用字符串来做。

void Main()
{
        var vs = Enumerable.Range(0, 50).Select(i => Create(i));

        var groups = vs.GroupByKeys(new [] { "Scale" });

        Console.WriteLine("{0} groups", groups.Count());

        Console.WriteLine(string.Join(", ", groups.Select(g => g.Key)));

}
Employee Create(int i) {
    return new Employee { Scale = (((int)(i / 10)) * 10), DOB = new DateTime(2011, 11, 11), Sales = 50000 };

}
public class Employee
{
    public string Designation {get ;set;}
    public string Discipline {get ;set;}
    public int Scale {get ;set;}
    public DateTime DOB {get ;set;}
    public int Sales {get ;set;}
}

public static class GroupByExtensions 
{
    public static IEnumerable<IGrouping<string, TValue>> GroupByKeys<TValue>(this IEnumerable<TValue> values, IEnumerable<string> keys) 
    {
        var getters = typeof(TValue).GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty)
            .Where(pi => keys.Contains(pi.Name))
            .Select(pi => pi.GetMethod)
            .Where(mi => mi != null)
            .ToArray();

        if (keys.Count() != getters.Length) 
        {
            throw new InvalidOperationException("Couldn't find all keys for grouping");
        }

        return values.GroupBy(v => getters.Aggregate("", (acc, getter) => string.Format("{0}¬{1}", acc, getter.Invoke(v, null).ToString())));

    }

}

我鼓励您使用函数来加强输入...

void Main()
{
        var vs = Enumerable.Range(0, 50).Select(i => Create(i));

        var groups = vs.GroupByKeys(new Func<Employee, object>[] { x=> x.Scale });

        Console.WriteLine("{0} groups", groups.Count());

        Console.WriteLine(string.Join(", ", groups.Select(g => g.Key)));

}
Employee Create(int i) {
    return new Employee { Scale = (((int)(i / 10)) * 10), DOB = new DateTime(2011, 11, 11), Sales = 50000 };

}
public class Employee
{
    public string Designation {get ;set;}
    public string Discipline {get ;set;}
    public int Scale {get ;set;}
    public DateTime DOB {get ;set;}
    public int Sales {get ;set;}
}

public static class GroupByExtensions 
{
    public static IEnumerable<IGrouping<string, TValue>> GroupByKeys<TValue>(this IEnumerable<TValue> values, IEnumerable<Func<TValue, object>> getters) 
    {

        return values.GroupBy(v => getters.Aggregate("", (acc, getter) => string.Format("{0}¬{1}", acc, getter(v).ToString())));

    }

}

对于这个问题的最终解决方案,我使用了@jamespconnor 的答案中的编码方法,但字符串作为分组键在我的实际场景中对我没有太大帮助。所以我使用@tim-rogers 的基本数组思想作为分组键并使用 ArrayEqualityComparer 比较数组。

为了获取字符串集合指定的关键属性,我构建了一个静态 class like

public static class MembersProvider
{
    public static IEnumerable<PropertyInfo> GetProperties(Type type, params string[] names)
    {
        var properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty)
            .Where(pi => names.Contains(pi.Name))
            .Where(pi => pi != null)
            .AsEnumerable();
        if (names.Count() != properties.Count())
        {
            throw new InvalidOperationException("Couldn't find all properties on type " + type.Name);
        }

        return properties;
    }
}

并更改了@jamespconnor 的 GroupByKeys 扩展,有点像

public static class GroupByExtensions
{
    public static IEnumerable<IGrouping<object[], TValue>> GroupByKeys<TValue>(this IEnumerable<TValue> values, IEnumerable<string> keys)
    {
        var properties = MembersProvider.GetProperties(typeof(TValue), keys.ToArray());
        var comparer = new ArrayEqualityComparer<object>();


        // jamespconnor's string as key approch - off course it will need to return IEnumerable<IGrouping<string, TValue>> 
        /*return values.GroupBy(v => getters.Aggregate(
            "",
            (acc, getter) => string.Format(
                "{0}-{1}",
                acc,
                getter.Invoke(v, null).ToString()
                )
            )
        );*/

        //objects array as key approch 
        return values.GroupBy(v => properties.Select(property => property.GetValue(v, null)).ToArray(), comparer);
    }

}

因为我还需要 select 匿名类型的结果,每个 "Key" 作为它的 属性 和一个额外的 "Total" 属性,但是没有成功,我最终喜欢

// get properties specified by "Keys" collection
    var properties = MembersProvider.GetProperties(typeof(Employee), Keys.ToArray());

    // Group and Select 
    var SalesSummary = Employees
        .GroupByKeys(Keys.ToArray())
        .Select(g =>
            properties.Aggregate(
                new Dictionary<string, object>() { { "TotalSales", g.Select(employee => employee.Sales).Sum() } },
                (dictionary, property) => {
                    dictionary.Add(property.Name, property.GetValue(g.FirstOrDefault(), null));
                    return dictionary;
                }   
            )
        );