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;
}
)
);
我有一个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;
}
)
);