¿Cómo puedo agrupar datos jerárquicamente usando LINQ?

Tengo algunos datos que tienen varios atributos y quiero agruparlos jerárquicamente. Por ejemplo:

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

Me gustaría esto agrupado como:

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

Actualmente, he podido agrupar esto usando LINQ de modo que el grupo superior divide los datos por A, luego cada subgrupo se divide por B, luego cada subgrupo B contiene subgrupos por C, etc. El LINQ se ve así (suponiendo queIEnumerable<Data> secuencia llamadadata):

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 puede ver, esto se vuelve un poco desordenado cuanto más subgrupo se requiere. ¿Hay alguna manera mejor de realizar este tipo de agrupación? Parece que debería haberlo y simplemente no lo estoy viendo.

Actualizar
Hasta ahora, he descubierto que expresar esta agrupación jerárquica utilizando las fluidas API de LINQ en lugar de lenguaje de consulta podría mejorar la legibilidad, pero no se siente muy SECO.

Había dos formas de hacer esto: una usandoGroupBy con un selector de resultados, el otro usandoGroupBy seguido de unSelect llamada. Ambos podrían formatearse para que sean más legibles que usar el lenguaje de consulta, pero no escalen bien.

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

Lo que me gustaría ...
Puedo imaginar un par de formas en que esto podría expresarse (suponiendo que el lenguaje y el marco lo respalden). El primero sería unGroupBy extensión que toma una serie de pares de funciones para la selección de teclas y la selección de resultados,Func<TElement, TKey> yFunc<TElement, TResult>. Cada par describe el siguiente subgrupo. Esta opción se cae porque cada par potencialmente requeriríaTKey yTResult ser diferente a los demás, lo que significaríaGroupBy necesitaría parámetros finitos y una declaración compleja.

La segunda opción sería unSubGroupBy Método de extensión que podría encadenarse para producir subgrupos.SubGroupBy sería lo mismo queGroupBy pero el resultado sería la agrupación anterior más particionada. Por ejemplo:

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 { ... })

La dificultad con esto es cómo implementar los métodos de manera eficiente como con mi comprensión actual, cada nivel volvería a crear nuevos objetos para extender los objetos anteriores. La primera iteración crearía agrupaciones de A, la segunda crearía objetos que tienen una clave de A y agrupaciones de B, la tercera rehacería todo eso y agregaría las agrupaciones de C. Esto parece terriblemente ineficiente (aunque sospecho que mis opciones actuales en realidad hacer esto de todos modos). Sería bueno si las llamadas pasaran una metadescripción de lo que se requería y las instancias solo se crearan en la última pasada, pero eso también parece difícil. Tenga en cuenta que el suyo es similar a lo que se puede hacer conGroupBy pero sin el método anidado llama.

Ojalá todo eso tenga sentido. Espero estar persiguiendo arcoiris aquí, pero tal vez no.

Actualización: otra opción
Otra posibilidad que creo que es más elegante que mis sugerencias anteriores se basa en que cada grupo de padres sea solo una clave y una secuencia de elementos secundarios (como en los ejemplos), muy similar aIGrouping proporciona ahora. Eso significa que una opción para construir esta agrupación sería una serie de selectores clave y un único selector de resultados.

Si las claves se limitaran a un tipo de conjunto, lo que no es irrazonable, entonces esto podría generarse como una secuencia de selectores de claves y un selector de resultados, o un selector de resultados y unparams de selectores clave. Por supuesto, si las claves debían ser de diferentes tipos y niveles, esto se vuelve difícil nuevamente, excepto por una profundidad de jerarquía finita debido a la forma en que funciona la parametrización genérica.

Aquí hay algunos ejemplos ilustrativos de lo que quiero decir:

Por ejemplo:

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

O:

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)

Esto no resuelve las ineficiencias de implementación, pero debería resolver el complejo anidamiento. Sin embargo, ¿cuál sería el tipo de retorno de esta agrupación? ¿Necesitaría mi propia interfaz o puedo usarIGrouping de alguna manera. ¿Cuánto necesito definir o la profundidad variable de la jerarquía todavía lo hace imposible?

Supongo que esto debería ser el mismo que el tipo de retorno de cualquierIGrouping llame, pero ¿cómo infiere el sistema de tipos ese tipo si no está involucrado en ninguno de los parámetros que se pasan?

Este problema está ampliando mi comprensión, lo cual es genial, pero me duele el cerebro.

Respuestas a la pregunta(2)

Su respuesta a la pregunta