Как я могу иерархически сгруппировать данные с помощью 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>&nbsp;последовательность называется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&nbsp;с селектором результата, другой с помощьюGroupBy&nbsp;с последующимSelect&nbsp;вызов. И то, и другое можно отформатировать так, чтобы оно было более читабельным, чем при использовании языка запросов, но все равно плохо масштабируется.

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&nbsp;расширение, которое принимает ряд пар функций для выбора ключа и выбора результата,Func<TElement, TKey>&nbsp;а такжеFunc<TElement, TResult>, Каждая пара описывает следующую подгруппу. Эта опция падает, потому что каждая пара потенциально потребуетTKey&nbsp;а такжеTResult&nbsp;отличаться от других, что означало быGroupBy&nbsp;потребуются конечные параметры и сложное объявление.

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

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&nbsp;но без вложенных вызовов методов.

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

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

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

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

Например:

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&nbsp;как-то. Сколько мне нужно определить или переменная глубина иерархии все еще делает это невозможным?

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

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