Grupo LINQ dynamic por múltiples columnas

Necesito traducir la siguiente consulta LINQ a Dynamic LINQ que acepta varias columnas de agrupación basadas en la entrada del usuario. Básicamente tengo un montón de listas desplegables que aplican agrupaciones y no quiero enumerar todas las combinaciones de agrupaciones. Si el LINQ dynamic falla, es posible que tenga que construir una consulta SQL manualmente, y nadie quiere eso.

var grouping = ( from entry in ObjectContext.OmniturePageModules where entry.StartOfWeek >= startDate && entry.StartOfWeek  p.Clicks ) } ); 

No tengo ni idea de cómo hacerlo, ya que Dynamic LINQ está totalmente sin documentar fuera del “hola mundo”. Seleccione / donde / ordenar por casos. Simplemente no puedo entender la syntax.

Algo como:(?)

 var grouping = ObjectContext.OmniturePageModules.Where(entry => entry.StartOfWeek >= startDate && entry.StartOfWeek <= endDate && ( section == "Total" || section == "All" || entry.Section == section ) && ( page == "Total" || page == "All" || entry.Page == page ) && ( module == "Total" || module == "All" || entry.Module == module )) .GroupBy("new (StartOfWeek,Page,Module)", "it") .Select("new (Sum(Clicks) as Clicks, SeriesName = section + key.Page + Key.Module, Week = it.Key.StartOfWeek)"); 

Estoy usando la clase DynamicQueryable en System.Linq.Dynamic. Consulte: http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx

Seguimiento: La solución de Enigmativity funcionó principalmente . Por alguna razón, no quiere agrupar por la columna datetime “StartOfWeek” – la solución es hacer una agrupación secundaria:

 var entries = ( from entry in ObjectContext.OmniturePageModules where entry.StartOfWeek >= startDate && entry.StartOfWeek  p.Clicks ), }; var grouping2 = (from groups in grouping group groups by new {groups.SeriesName, groups.Date } into entryGroup select new { entryGroup.Key.SeriesName, entryGroup.Key.Date, Clicks = entryGroup.Sum( p => p.Clicks ), } ); 

pero esto parece degradar seriamente el rendimiento … = /

Si desea usar la biblioteca de consultas dinámicas de LINQ explícitamente, mi respuesta no será la que usted desea, pero si desea el comportamiento deseado y está feliz de usar LINQ, creo que puedo ayudarlo.

Esencialmente, he creado una clase EntryGrouper que maneja la lógica de agrupación por los valores seleccionados en las listas desplegables y asumí que la section variables, page y module contienen esos valores. También asumí que ObjectContext.OmniturePageModules es un enumerable de tipo Entry .

Entonces tu consulta LINQ ahora se convierte en estas dos:

 var entries = (from entry in ObjectContext.OmniturePageModules where entry.StartOfWeek >= startDate && entry.StartOfWeek <= endDate && (section == "Total" || section == "All" || entry.Section == section) && (page == "Total" || page == "All" || entry.Page == page) && (module == "Total" || module == "All" || entry.Module == module) select entry).ToArray(); // Force query execution var grouping = from entry in entries let grouper = new EntryGrouper(entry, section, page, module) group entry by grouper into entryGroup select new { SeriesName = entryGroup.Key.SeriesName, Week = entryGroup.Key.StartOfWeek, Clicks = entryGroup.Sum(p => p.Clicks), }; 

La primera consulta se usa para forzar una consulta de selección simple en la base de datos y devolver solo los registros que desea agrupar. En general group by consultas group by llaman a la base de datos varias veces, por lo que la consulta de esta manera suele ser mucho más rápida.

La segunda consulta agrupa los resultados de la primera consulta al crear instancias de la clase EntryGrouper como la clave de agrupación.

He incluido una propiedad SeriesName en la clase EntryGrouper para que toda la lógica de agrupación esté claramente definida en un solo lugar.

Ahora, la clase EntryGrouper es bastante grande ya que, para permitir que la agrupación funcione, necesita tener propiedades para StartOfWeek , Section , Page & Module , y contener sobrecargas de los métodos Equals & GetHashCode , e implementar la IEquatable .

Aquí está:

 public class EntryGrouper : IEquatable { private Entry _entry; private string _section; private string _page; private string _module; public EntryGrouper(Entry entry, string section, string page, string module) { _entry = entry; _section = section; _page = page; _module = module; } public string SeriesName { get { return String.Format("{0}:{1}:{2}", this.Section, this.Page, this.Module); } } public DateTime StartOfWeek { get { return _entry.StartOfWeek; } } public string Section { get { if (_section == "Total" || _section == "All") return _section; return _entry.Section; } } public string Page { get { if (_page == "Total" || _page == "All") return _page; return _entry.Page; } } public string Module { get { if (_module == "Total" || _module == "All") return _module; return _entry.Module; } } public override bool Equals(object other) { if (other is Entry) return this.Equals((Entry)other); return false; } public bool Equals(Entry other) { if (other == null) return false; if (!EqualityComparer.Default.Equals(this.StartOfWeek, other.StartOfWeek)) return false; if (!EqualityComparer.Default.Equals(this.Section, other.Section)) return false; if (!EqualityComparer.Default.Equals(this.Page, other.Page)) return false; if (!EqualityComparer.Default.Equals(this.Module, other.Module)) return false; return true; } public override int GetHashCode() { var hash = 0; hash ^= EqualityComparer.Default.GetHashCode(this.StartOfWeek); hash ^= EqualityComparer.Default.GetHashCode(this.Section); hash ^= EqualityComparer.Default.GetHashCode(this.Page); hash ^= EqualityComparer.Default.GetHashCode(this.Module); return hash; } public override string ToString() { var template = "{{ StartOfWeek = {0}, Section = {1}, Page = {2}, Module = {3} }}"; return String.Format(template, this.StartOfWeek, this.Section, this.Page, this.Module); } } 

La lógica de agrupación de esta clase se ve así:

 if (_page == "Total" || _page == "All") return _page; return _entry.Page; 

Si he entendido mal cómo los valores del menú desplegable activan y desactivan la agrupación, entonces solo debe cambiar estos métodos, pero el quid de este código es que cuando la agrupación está activada debe devolver un valor de grupo basado en el valor en la entrada y de lo contrario, debería devolver un valor común para todas las entradas. Si el valor es común para todas las entradas, lógicamente solo crea un único grupo que es el mismo que no se agrupa en absoluto.

Si tiene más menús desplegables que está agrupando, entonces necesita agregar más propiedades a la clase EntryGrouper . No olvide agregar estas nuevas propiedades a los métodos Equals & GetHashCode también.

Esta lógica, por lo tanto, representa la agrupación dinámica que deseaba. Por favor, avíseme si le he ayudado o si necesita más detalles.

¡Disfrutar!

Aquí está en Dynamic LINQ; por supuesto, usted construye las cadenas GroupBy y Select en tiempo de ejecución:

 var double_grouping = ( ObjectContext.OmniturePageModules.Where( entry => entry.StartOfWeek >= startDate && entry.StartOfWeek <= endDate && ( section == "Total" || section == "All" || entry.Section == section ) && ( page == "Total" || page == "All" || entry.Page == page ) && ( module == "Total" || module == "All" || entry.Module == module ) ) .GroupBy( "new ( it.Section, it.Page, it.StartOfWeek )", "it" ) ) .Select( "new ( Sum(Clicks) as Clicks, Key.Section as SeriesSection, Key.Page as SeriesPage, Key.StartOfWeek as Week )" ); 

Y aquí está la forma normal de LINQ que se me escapó hasta que un compañero de trabajo lo señaló: esta es básicamente la solución de Enigmativity sin la clase de mero:

 var grouping = ( from entry in ObjectContext.OmniturePageModules where entry.StartOfWeek >= startDate && entry.StartOfWeek <= endDate && ( section == "Total" || section == "All" || entry.Section == section ) && ( page == "Total" || page == "All" || entry.Page == page ) && ( module == "Total" || module == "All" || entry.Module == module ) group entry by new { Section = section == "All" ? entry.Section : section, Page = page == "All" ? entry.Page : page, Module = module == "All" ? entry.Module : module, entry.StartOfWeek } into entryGroup select new { SeriesName = entryGroup.Key.Section + ":" + entryGroup.Key.Page + ":" + entryGroup.Key.Module, Week = entryGroup.Key.StartOfWeek, Clicks = entryGroup.Sum( p => p.Clicks ) } ); 

Sé que ha pasado un tiempo desde que se publicó esta pregunta, pero tuve que lidiar con un problema similar recientemente (agrupación dinámica por varias columnas seleccionadas por el usuario en tiempo de ejecución), así que aquí tengo mi opinión.

  1. Función auxiliar para crear lambdas de agrupación.

     static Expression> GetGroupBy( string property ) { var data = Expression.Parameter( typeof( T ), "data" ); var dataProperty = Expression.PropertyOrField( data, property ); var conversion = Expression.Convert( dataProperty, typeof( object ) ); return Expression.Lambda>( conversion, data ); } 
  2. Función para hacer la agrupación en memoria. Devuelve grupos.

     static IEnumerable> Group( IEnumerable ds, params Func[] groupSelectors ) { Func, Func[], IEnumerable>> inner = null; inner = ( d, ss ) => { if ( null == ss || ss.Length == 0 ) { return new[] { d }; } else { var s = ss.First(); return d.GroupBy( s ).Select( g => inner( g.Select( x => x ), ss.Skip( 1 ).ToArray() ) ) .SelectMany( x => x ); } }; return inner( ds, groupSelectors ); } 
  3. ¿Cómo se usaría?

     String[] columnsSelectedByUser = ... // contains names of grouping columns selected by user var entries = ... // Force query execution ie fetch all data var groupBys = columnsSelectedByUser.Select( x => GetGroupBy( x ).Compile()).ToArray(); var grouping = Group(entries, groupBys); // enumerable containing groups of entries 

Con respecto a las actuaciones degradantes, no creo que sea realmente un (gran) problema. Incluso si construyó un SQL de agrupación dinámicamente, la consulta tendría que devolver el mismo número de filas que una consulta sin la agrupación. Entonces, aunque en este enfoque la agrupación no es realizada por la base de datos, el número de filas devueltas por la ejecución forzada de consultas es el mismo que para la hipotética consulta SQL con criterios de agrupación. Claro, la base de datos probablemente superaría la agrupación en memoria realizada por el código C #, pero la cantidad de tráfico depende únicamente de cuántas filas ( entries ) se deben agrupar.