Как я могу иерархически сгруппировать данные с помощью 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 вызов, но как система типов выводит этот тип, если он не участвует ни в одном из передаваемых параметров?

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

Ответы на вопрос(2)

Ваш ответ на вопрос