Como agrupar hierarquicamente dados usando o LINQ?

Eu tenho alguns dados que possuem vários atributos e quero agrupar hierarquicamente esses dados. Por exemplo:

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

Eu gostaria que isso agrupasse como:

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

Atualmente, eu pude agrupar isso usando o LINQ, de modo que o grupo superior divida os dados por A, então cada subgrupo divide por B, então cada subgrupo B contém subgrupos por C, etc. O LINQ se parece com isso (assumindo umaIEnumerable<Data> sequência chamadadata):

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

Como você pode ver, isso fica um pouco confuso quanto mais subgrupos forem necessários. Existe uma maneira melhor de executar esse tipo de agrupamento? Parece que deveria haver e eu simplesmente não estou vendo.

Atualizar
Até agora, descobri que expressar esse agrupamento hierárquico usando as APIs LINQ fluentes, em vez da linguagem de consulta, sem dúvida melhora a legibilidade, mas não parece muito SECO.

Havia duas maneiras de fazer isso: uma usandoGroupBy com um seletor de resultados, o outro usandoGroupBy seguido por umSelect ligar. Ambos podem ser formatados para serem mais legíveis do que usar a linguagem de consulta, mas ainda não são dimensionados bem.

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

O que eu gostaria ...
Eu posso imaginar algumas maneiras pelas quais isso pode ser expresso (assumindo que a linguagem e a estrutura o suportem). O primeiro seria umGroupBy extensão que utiliza uma série de pares de funções para seleção de teclas e seleção de resultados,Func<TElement, TKey> eFunc<TElement, TResult>. Cada par descreve o próximo subgrupo. Essa opção cai porque cada par exigiria potencialmenteTKey eTResult ser diferente dos outros, o que significariaGroupBy precisaria de parâmetros finitos e uma declaração complexa.

A segunda opção seria umaSubGroupBy método de extensão que poderia ser encadeado para produzir subgrupos.SubGroupBy seria o mesmo queGroupBy mas o resultado seria o agrupamento anterior ainda particionado. Por exemplo:

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 dificuldade disso é como implementar os métodos com eficiência, como no meu entendimento atual, cada nível recriaria novos objetos para estender os objetos anteriores. A primeira iteração criaria agrupamentos de A, a segunda criaria objetos que possuem uma chave de A e agrupamentos de B, a terceira refizeria tudo isso e acrescentaria os agrupamentos de C. Isso parece terrivelmente ineficiente (embora eu suspeite de minhas opções atuais faça isso mesmo). Seria bom se as chamadas passassem por uma meta-descrição do que era necessário e as instâncias fossem criadas apenas na última passagem, mas isso também parece difícil. Observe que o dele é semelhante ao que pode ser feito comGroupBy mas sem as chamadas de método aninhado.

Espero que tudo isso faça sentido. Espero estar perseguindo arco-íris aqui, mas talvez não.

Atualização - outra opção
Outra possibilidade que eu acho mais elegante do que minhas sugestões anteriores depende de cada grupo de pais, sendo apenas uma chave e uma sequência de itens filhos (como nos exemplos), bem comoIGrouping fornece agora. Isso significa que uma opção para construir esse agrupamento seria uma série de seletores de chave e um único seletor de resultados.

Se todas as chaves estiverem limitadas a um tipo de conjunto, o que não é razoável, isso poderá ser gerado como uma sequência de seletores de chave e um seletor de resultados, ou um seletor de resultados e umparams seletores de teclas. Obviamente, se as chaves precisassem ser de tipos e níveis diferentes, isso se tornaria difícil novamente, exceto por uma profundidade hierárquica finita devido à maneira como a parametrização genérica funciona.

Aqui estão alguns exemplos ilustrativos do que quero dizer:

Por exemplo:

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*/ })

Ou:

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)

Isso não resolve as ineficiências de implementação, mas deve resolver o aninhamento complexo. No entanto, qual seria o tipo de retorno desse agrupamento? Eu precisaria de minha própria interface ou posso usarIGrouping de alguma forma. Quanto eu preciso definir ou a profundidade variável da hierarquia ainda torna isso impossível?

Meu palpite é que esse deve ser o mesmo que o tipo de retorno de qualquerIGrouping chamar, mas como o sistema de tipos infere esse tipo se não estiver envolvido em nenhum dos parâmetros que são passados?

Esse problema está ampliando minha compreensão, o que é ótimo, mas meu cérebro dói.

questionAnswers(2)

yourAnswerToTheQuestion