Как сгруппировать по нескольким общим выражениям 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" },
};

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

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
}

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

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

Как мне построить выражение group-by?

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

Изменить

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

Редактировать №2

Тип сущности в doLinq стал универсальным.


person McGarnagle    schedule 13.04.2012    source источник
comment
Похоже, вы должны быть в состоянии написать функцию, которая объединяет несколько Expression<Func<Row,object>> в один Expression<Func<Row,object>>, который возвращает кортеж или массив значений или что-то еще. Похоже, вы используете linq to sql, и я недостаточно знаю о переводе в sql, чтобы знать, можете ли вы сделать это переводимым способом.   -  person phoog    schedule 14.04.2012
comment
@phoog похоже, что вы должны объединить их в одно выражение - я до сих пор не знаю, как это сделать. Вместо этого я объединил результаты выражений для каждой строки.   -  person McGarnagle    schedule 22.04.2012
comment
Вы должны опубликовать свое решение как новый ответ, а не редактировать вопрос.   -  person svick    schedule 22.04.2012


Ответы (4)


Вам следует взглянуть на Dynamic Linq: http://blogs.msdn.com/b/mitsu/archive/2008/02/07/linq-groupbymany-dynamically.aspx

person Vladimir Perevalov    schedule 13.04.2012
comment
Очень интересная и полезная ссылка, но это не совсем то, что я хочу. Mitsu создает подгруппы в иерархическом порядке, но каждая группировка выполняется по одному ключу, т. е. группировка по странам, затем для каждой группы группировка по городам. Я хочу иметь один уровень группировки, но несколько свойств в ключе, например, группировать по стране и возрастной группе (Col1 и Col2 в моем примере). - person McGarnagle; 13.04.2012
comment
Извините, я пропустил цель :) Вот ссылка на аналогичный вопрос с тем, что я имел в виду в качестве решения: stackoverflow.com/questions/3929041/ С помощью Dynamic Linq я предложил использовать интерфейсы linq на основе строк. - person Vladimir Perevalov; 13.04.2012

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

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

  • динамическая связь

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

  • построение всего дерева выражений groupby вручную

см. 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");
     }
 }
person HugoRune    schedule 15.04.2012
comment
Я понял, что поначалу слишком упростил вопрос — это скрыло то, что я пытался сделать. Тип Row на самом деле является универсальным (см. обновление выше), что, казалось бы, исключает #1 и #3. #2 выглядит очень интересно, я проверяю... - person McGarnagle; 15.04.2012

У меня крайне ограниченные знания о 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");
            }
        }
    }
}

База данных LOL имеет одну таблицу «Тест» с тремя строками. Вывод таков:

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

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

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

person user1096188    schedule 15.04.2012
comment
Гениально, но у меня не получилось... Не знаю почему. В моем тестовом случае метод Props.Equals всегда возвращает true; казалось, что он сравнивает выражения Linq, а не оценки. Разве вам не нужно использовать Expression.Compile() где-то там? - person McGarnagle; 22.04.2012
comment
Итак, GroupBy организует Props в хеш-таблицу, поэтому Props.Equals будет вызываться только для объектов, для которых Props.GetHashCode возвращает равные значения, что означает, что они, скорее всего, уже равны. Так что само по себе то, что Props.Equals всегда возвращает true, вряд ли является проблемой. Вам также не нужно Expressions нигде в коде, так как вся группировка происходит на клиенте, а сгенерированный sql представляет собой простой select из таблицы. Я также пробовал это с SQL Server Express с идентичными результатами. - person user1096188; 22.04.2012

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

  • создать группирующий объект (который я неуклюже реализовал как object[]) с заданным значением строки и набором селекторов. Это включает лямбда-выражение, которое компилирует и вызывает каждый селектор в элементе строки.
  • реализовать 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()
)
{ ... }

Вторая часть

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();
    }
}

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

person McGarnagle    schedule 21.04.2012
comment
Я думаю, что это запросит базу данных для всех строк, а затем выполнит группировку локально. Так что это не будет работать для linq-to-sql с сервером ms-sql, где запрос переводится в t-sql, для этого вам понадобится дерево выражений. Тем не менее, даже в linq-to-sql вы всегда можете добавить .AsEnumerable() перед вашим GroupBy(), чтобы принудительно выполнить группировку локально - person HugoRune; 22.04.2012