Как группировать по нескольким обобщенным выражениям linq

Я пытаюсь использовать выражения Linq для построения запроса, и я пытаюсь группировать по нескольким столбцам. Скажем, у меня есть базовая коллекция:

IEnumerable<row> collection = new Row[]
{
 new Row() { Col1 = "a", Col2="x" },
 new Row() { Col1 = "a", Col2="x" },
 new Row() { Col1 = "a", Col2="y" },
};
</row>

Я знаю, что вы можете группировать их с помощью лямбда-выражений:

foreach (var grp in collection.GroupBy(item => new { item.Col1, item.Col2 }))
{
 Debug.Write("Grouping by " + grp.Key.Col1 + " and " + grp.Key.Col2 + ": ");
 Debug.WriteLine(grp.Count() + " rows");
}

Это правильно, как вы можете видеть:

Grouping by a and x: 2 rows
Grouping by a and y: 1 rows

Но теперь, скажем, я получаю набор селекторов для группировки против, который передается мне как параметр в моем методе и что тип объекта является общим:

void doLinq<t>(params Expression<func<t,object>>[] selectors)
{
 // linq stuff
}
</func<t,object></t>

Тот, кто вызывает метод, будет выглядеть так:

doLinq<row>(entity=>entity.Col1, entity=>entity.Col2);
</row>

Как бы я построил выражение group-by?

foreach (var grp in collection.GroupBy(
 item => new { 
 // selectors??
 }))
{
 // grp.Key. ??
}

Edit

Я обновил выше, чтобы, надеюсь, уточнить, зачем мне нужен набор селекторов.

Изменить # 2

Создал тип сущности в doLinq generic.

4 ответа

Решение работало для меня. Он включает две части:

  • создайте объект группировки (который я внедрил inelegantly как объект []), учитывая значение строки и набор селекторов. Это связано с выражением лямбда, которое компилирует и вызывает каждый селектор в элементе строки.
  • реализует IEquality для типа объекта группировки (в моем случае IEqualityComparer).

Первая часть

foreach (System.Linq.IGrouping<object[], t=""> g in collection.GroupBy(
 new Func<t, object[]="">(
 item => selectors.Select(sel => sel.Compile().Invoke(item)).ToArray()
 ),
 new ColumnComparer()
)
{ ... }
</t,></object[],>

Вторая часть

public class ColumnComparer : IEqualityComparer<object[]>
{
 public bool Equals(object[] x, object[] y)
 {
 return Enumerable.SequenceEqual(x, y);
 }
 public int GetHashCode(object[] obj)
 {
 return (string.Join("", obj.ToArray())).GetHashCode();
 }
}
</object[]>

Это работает для базовых Linq и Linq для соединителя MySql. Какие другие провайдеры Linq, и какие типы выражений это работает, - это еще один вопрос...


У меня очень ограниченное знание linq-to-sql, но действительно ли важно, что внутри GroupBy? Потому что, если это не так, вы можете развернуть свой собственный keySelector. Во всяком случае, я попробовал это как с Sql Server CE, так и с Sql Server Express, и это, похоже, работает:

using System;
using System.Linq;
using System.Collections.Generic;
using System.Data.Linq;
using System.Linq.Expressions;
namespace ConsoleApplication1 {
 class Props {
 public List<object> list = new List<object>();
 public override bool Equals(object obj) {
 return Enumerable.SequenceEqual(list, (obj as Props).list);
 }
 public override int GetHashCode() {
 return list.Select(o => o.GetHashCode()).Aggregate((i1, i2) => i1 ^ i2);
 }
 }
 class Program {
 static void Main(string[] args) {
 Lol db = new Lol(@"Data Source=.\SQLExpress;Initial Catalog=Lol;Integrated Security=true");
 db.Log = Console.Out;
 doLinq(db.Test, row => row.Col1, row => row.Col2);
 Console.ReadLine();
 }
 static void doLinq<t>(Table<t> table, params Func<t, object="">[] selectors) where T : class {
 Func<t, props=""> selector = item => {
 var props = new Props();
 foreach (var sel in selectors) props.list.Add(sel(item));
 return props;
 };
 foreach (var grp in table.GroupBy(selector)) {
 Console.Write("Grouping by " + string.Join(", ", grp.Key.list) + ": ");
 Console.WriteLine(grp.Count() + " rows");
 }
 }
 }
}
<p>База данных Lol имеет одну таблицу "Тест" с тремя строками. Вывод:</p>
<pre class="prettyprint linenums"><code>SELECT [t0].[Col1], [t0].[Col2]
FROM [dbo].[Test] AS [t0]
-- Context: SqlProvider(Sql2008) Model: AttributedMetaModel Build: 4.0.30319.1
Grouping by a, x: 2 rows
Grouping by a, y: 1 rows
</code>

Я проверил запросы, и кажется, что linq-to-sql достаточно умен, чтобы не генерировать sql для groupBy, когда он не может, поэтому он будет перебирать все строки таблицы и затем группировать их на клиенте.

Изменить: второстепенные дополнения для завершения, а строка подключения теперь предполагает Sql Server Express.


Хорошо, я предполагаю, что вы используете linq-to-sql или что-то подобное, поэтому вам нужны деревья выражений. Если бы не были другие возможности.

Возможные решения, которые я вижу:

  • динамический linq

см. ответ Владимира Перевалова.

  • создание всей группы с помощью дерева выражений вручную

см http://msdn.microsoft.com/en-us/library/bb882637.aspx

  • Уродливое обходное решение

Хорошо, что мой департамент:)

непроверенный код:

void doLinq(params string[] selectors) // checking two expressions for equality is messy, so I used strings
 foreach (var grp in collection.GroupBy(
 item => new { 
 Col1 = (selectors.Contains("Col1") ? item.Col1 : String.Empty),
 Col2 = (selectors.Contains("Col2") ? item.Col2 : String.Empty)
 // need to add a line for each column :(
 }))
 {
 string[] grouping = (new string[]{grp.Key.Col1, grp.Key.Col2 /*, ...*/ }).Where(s=>!s.IsNullOrEmpty()).ToArray();
 Debug.Write("Grouping by " + String.Join(" and ", grouping)+ ": ");
 Debug.WriteLine(grp.Count() + " rows");
 }
 }

licensed under cc by-sa 3.0 with attribution.