Как я могу иерархически группировать данные с помощью LINQ?

У меня есть некоторые данные, которые имеют разные атрибуты, и я хочу иерархически группировать эти данные. Например:

public class Data
{
   public string A { get; set; }
   public string B { get; set; }
   public string C { get; set; }
}

Я бы хотел, чтобы это сгруппировалось как:

A1
 - B1
    - C1
    - C2
    - C3
    - ...
 - B2
    - ...
A2
 - B1
    - ...
...

В настоящее время мне удалось сгруппировать это с помощью LINQ таким образом, что верхняя группа делит данные на A, тогда каждая подгруппа делит на B, тогда каждая подгруппа B содержит подгруппы через C и т.д. LINQ выглядит так (предполагая последовательность IEnumerable<Data>, называемую data):

var hierarchicalGrouping =
            from x in data
            group x by x.A
                into byA
                let subgroupB = from x in byA
                                group x by x.B
                                    into byB
                                    let subgroupC = from x in byB
                                                    group x by x.C
                                    select new
                                    {
                                        B = byB.Key,
                                        SubgroupC = subgroupC
                                    }
                select new
                {
                    A = byA.Key,
                    SubgroupB = subgroupB
                };

Как вы можете видеть, это становится несколько грязным, чем больше требуется подгруппа. Есть ли лучший способ выполнить этот тип группировки? Кажется, должно быть, и я просто не вижу его.

Обновление
До сих пор я обнаружил, что выражение этой иерархической группировки с использованием беглого API-интерфейса LINQ, а не языка запросов, возможно, улучшает читаемость, но оно не чувствует себя очень СУХОЙ.

Было два способа сделать это: один с помощью GroupBy с селектором результатов, другой - с помощью GroupBy, за которым следует вызов Select. Оба могут быть отформатированы, чтобы быть более читабельными, чем использование языка запросов, но не все еще хорошо масштабируются.

var withResultSelector =
    data.GroupBy(a => a.A, (aKey, aData) =>
        new
        {
            A = aKey,
            SubgroupB = aData.GroupBy(b => b.B, (bKey, bData) =>
                new
                {
                    B = bKey,
                    SubgroupC = bData.GroupBy(c => c.C, (cKey, cData) =>
                    new
                    {
                        C = cKey,
                        SubgroupD = cData.GroupBy(d => d.D)
                    })
                })
        });

var withSelectCall =
    data.GroupBy(a => a.A)
        .Select(aG =>
        new
        {
            A = aG.Key,
            SubgroupB = aG
                .GroupBy(b => b.B)
                .Select(bG =>
            new
            {
                B = bG.Key,
                SubgroupC = bG
                    .GroupBy(c => c.C)
                    .Select(cG =>
                new
                {
                    C = cG.Key,
                    SubgroupD = cG.GroupBy(d => d.D)
                })
            })
        });

Что я хочу...
Я могу предусмотреть несколько способов, которыми это могло бы быть выражено (предполагая, что язык и рамки поддерживали его). Первым будет расширение GroupBy, которое берет ряд пар функций для выбора ключа и выбора результата, Func<TElement, TKey> и Func<TElement, TResult>. Каждая пара описывает следующую подгруппу. Эта опция падает, потому что каждая пара потенциально требует, чтобы TKey и TResult отличались от остальных, что означало бы, что GroupBy потребует конечных параметров и сложного объявления.

Второй вариант - это метод расширения SubGroupBy, который может быть прикован для создания подгрупп. SubGroupBy будет таким же, как GroupBy, но результатом будет предыдущая группировка, далее разбитая на разделы. Например:

var groupings = data
    .GroupBy(x=>x.A)
    .SubGroupBy(y=>y.B)
    .SubGroupBy(z=>z.C)

// This version has a custom result type that would be the grouping data.
// The element data at each stage would be the custom data at this point
// as the original data would be lost when projected to the results type.
var groupingsWithCustomResultType = data
    .GroupBy(a=>a.A, x=>new { ... })
    .SubGroupBy(b=>b.B, y=>new { ... })
    .SubGroupBy(c=>c.C, c=>new { ... })

Сложность этого заключается в том, как эффективно реализовать методы, как с моим нынешним пониманием, каждый уровень будет воссоздавать новые объекты для расширения предыдущих объектов. Первая итерация создавала бы группы A, вторая создавала бы объекты, у которых есть ключ A и группировки B, третий повторил бы все это и добавит группировки C. Это кажется ужасно неэффективным (хотя я подозреваю, что мои текущие варианты на самом деле все равно). Было бы неплохо, если бы звонки проходили вокруг мета-описания того, что требовалось, и экземпляры были созданы только на последнем проходе, но это тоже сложно. Обратите внимание, что он похож на то, что можно сделать с помощью GroupBy, но без вызовов вложенных методов.

Надеюсь, все это имеет смысл. Я ожидаю, что здесь преследую радуги, но, возможно, нет.

Обновление - еще один вариант
Другая возможность, которая, по моему мнению, более элегантна, чем мои предыдущие предложения, зависит от того, что каждая родительская группа является всего лишь ключом и последовательностью дочерних элементов (как в примерах), так же, как IGrouping. Это означает, что одним из вариантов построения этой группировки будет серия селекторов ключей и один селектор результатов.

Если все ключи были ограничены типом набора, что не является необоснованным, тогда это может быть сгенерировано как последовательность селекторов клавиш и селектор результатов, или селектор результатов, и params селекторов клавиш. Конечно, если ключи должны были быть разных типов и разных уровней, это становится трудным снова, за исключением конечной глубины иерархии из-за того, как работает параметризация параметрических данных.

Вот несколько иллюстративных примеров того, что я имею в виду:

Например:

public static /*<grouping type>*/ SubgroupBy(
    IEnumerable<Func<TElement, TKey>> keySelectors,
    this IEnumerable<TElement> sequence,
    Func<TElement, TResult> resultSelector)
{
    ...
}

var hierarchy = data.SubgroupBy(
                    new [] {
                        x => x.A,
                        y => y.B,
                        z => z.C },
                    a => new { /*custom projection here for leaf items*/ })

Или:

public static /*<grouping type>*/ SubgroupBy(
    this IEnumerable<TElement> sequence,
    Func<TElement, TResult> resultSelector,
    params Func<TElement, TKey>[] keySelectors)
{
    ...
}

var hierarchy = data.SubgroupBy(
                    a => new { /*custom projection here for leaf items*/ },
                    x => x.A,
                    y => y.B,
                    z => z.C)

Это не решает проблему неэффективности реализации, но она должна решить сложную вложенность. Однако каков будет тип возврата этой группировки? Мне нужен мой собственный интерфейс или я могу как-то использовать IGrouping. Сколько мне нужно определить или переменная глубина иерархии все еще делает это невозможным?

Я предполагаю, что это должно быть то же самое, что и тип возврата из любого вызова IGrouping, но как система типов выводит этот тип, если он не участвует ни в одном из переданных параметров?

Эта проблема растягивает мое понимание, которое здорово, но мой мозг болит.

Ответ 1

Вот описание, как вы можете реализовать иерархический механизм группировки.

Из этого описания:

Класс результата:

public class GroupResult
{
    public object Key { get; set; }
    public int Count { get; set; }
    public IEnumerable Items { get; set; }
    public IEnumerable<GroupResult> SubGroups { get; set; }
    public override string ToString() 
    { return string.Format("{0} ({1})", Key, Count); }
}

Метод расширения:

public static class MyEnumerableExtensions
{
    public static IEnumerable<GroupResult> GroupByMany<TElement>(
        this IEnumerable<TElement> elements,
        params Func<TElement, object>[] groupSelectors)
    {
        if (groupSelectors.Length > 0)
        {
            var selector = groupSelectors.First();

            //reduce the list recursively until zero
            var nextSelectors = groupSelectors.Skip(1).ToArray();
            return
                elements.GroupBy(selector).Select(
                    g => new GroupResult
                    {
                        Key = g.Key,
                        Count = g.Count(),
                        Items = g,
                        SubGroups = g.GroupByMany(nextSelectors)
                    });
        }
        else
            return null;
    }
}

Применение:

var result = customers.GroupByMany(c => c.Country, c => c.City);

Edit:

Вот улучшенная и правильно напечатанная версия кода.

public class GroupResult<TItem>
{
    public object Key { get; set; }
    public int Count { get; set; }
    public IEnumerable<TItem> Items { get; set; }
    public IEnumerable<GroupResult<TItem>> SubGroups { get; set; }
    public override string ToString() 
    { return string.Format("{0} ({1})", Key, Count); }
}

public static class MyEnumerableExtensions
{
    public static IEnumerable<GroupResult<TElement>> GroupByMany<TElement>(
        this IEnumerable<TElement> elements,
        params Func<TElement, object>[] groupSelectors)
    {
        if (groupSelectors.Length > 0)
        {
            var selector = groupSelectors.First();

            //reduce the list recursively until zero
            var nextSelectors = groupSelectors.Skip(1).ToArray();
            return
                elements.GroupBy(selector).Select(
                    g => new GroupResult<TElement> {
                        Key = g.Key,
                        Count = g.Count(),
                        Items = g,
                        SubGroups = g.GroupByMany(nextSelectors)
                    });
        } else {
            return null;
        }
    }
}

Ответ 2

Вам нужна рекурсивная функция. Рекурсивная функция вызывает себя для каждого node в дереве.

Для этого в Linq вы можете использовать Y-combinator.