2010-10-14 26 views
10

Devo tradurre la seguente query LINQ in Dynamic LINQ che accetta diverse colonne di raggruppamento in base all'input dell'utente. Fondamentalmente ho un gruppo di elenchi a discesa che applicano i raggruppamenti e non desidero enumerare ogni combinazione di raggruppamenti. Se Dynamic LINQ fallisce, potrei dover costruire manualmente una query SQL, e nessuno lo vuole.Dynamic LINQ GroupBy Multiple Columns

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 
    { 
     entry.Page, // I want to be able to tell this anonymous type 
     entry.Module, // which columns to group by 
     entry.StartOfWeek // at runtime 
    } 
    into entryGroup 
    select new 
    { 
     SeriesName = section + ":" + entryGroup.Key.Page + ":" + entryGroup.Key.Module, 
     Week = entryGroup.Key.StartOfWeek, 
     Clicks = entryGroup.Sum(p => p.Clicks) 
    }); 

non ho idea di come fare questo, come dinamica LINQ è totalmente documentato al di fuori del "Ciao mondo!" selezionare/where/orderby cases. Non riesco a capire la sintassi.

Qualcosa di simile :(?)

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

Sto utilizzando la classe DynamicQueryable in System.Linq.Dynamic. Vedere: http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx

Follow-up: la soluzione di Enigmativity ha lavorato per lo più . Per qualche motivo non vuole gruppo da parte del datetime "StartOfWeek" colonna - soluzione è solo per fare un raggruppamento secondario:

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 
          { 
           entryGroup.Key.SeriesName, 
           entryGroup.Key.Date, 
           Clicks = entryGroup.Sum(p => 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), 
          }); 

ma questo sembra a degradare seriamente le prestazioni ... =/

risposta

3

Se si desidera utilizzare esplicitamente la libreria di query dinamica LINQ, la mia risposta non sarà quella che si desidera, ma se si desidera il comportamento desiderato e si è felici di utilizzare LINQ regolare, penso di poter essere d'aiuto.

Essenzialmente ho creato una classe EntryGrouper che gestisce la logica di raggruppamento per i valori selezionati nelle elenchi a discesa e ho assunto che le variabili section, page & module tenere quei valori. Ho anche ipotizzato che ObjectContext.OmniturePageModules sia una enumerabile di tipo Entry.

Così la vostra query LINQ diventa ora questi due:

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 prima query viene utilizzato per forzare una semplice query di selezione sulla base di dati e restituire solo i record che si desidera raggruppare. Generalmente le query group by chiamano il database più volte, pertanto le query in questo modo sono in genere molto più veloci.

La seconda query raggruppa i risultati della prima query creando istanze della classe EntryGrouper come chiave di raggruppamento.

Ho incluso una proprietà SeriesName nella classe EntryGrouper in modo che tutta la logica di raggruppamento sia definita in modo preciso in un'unica posizione.

Ora, la classe EntryGrouper è abbastanza grande, come, per consentire il raggruppamento di lavorare, ha bisogno di avere proprietà per StartOfWeek, Section, Page & Module, e contengono sovraccarichi delle Equals & GetHashCode metodi, e implementare l'interfaccia IEquatable<Entry>.

Eccolo:

public class EntryGrouper : IEquatable<Entry> 
{ 
    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<DateTime>.Default.Equals(this.StartOfWeek, other.StartOfWeek)) 
      return false; 
     if (!EqualityComparer<string>.Default.Equals(this.Section, other.Section)) 
      return false; 
     if (!EqualityComparer<string>.Default.Equals(this.Page, other.Page)) 
      return false; 
     if (!EqualityComparer<string>.Default.Equals(this.Module, other.Module)) 
      return false; 
     return true; 
    } 

    public override int GetHashCode() 
    { 
     var hash = 0; 
     hash ^= EqualityComparer<DateTime>.Default.GetHashCode(this.StartOfWeek); 
     hash ^= EqualityComparer<string>.Default.GetHashCode(this.Section); 
     hash ^= EqualityComparer<string>.Default.GetHashCode(this.Page); 
     hash ^= EqualityComparer<string>.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 logica di raggruppamento di questa classe sembra semplicemente in questo modo:

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

Se ho capito male come i valori discesa girare il raggruppamento e si spegne allora si dovrebbe solo bisogno di cambiare questi metodi, ma il punto cruciale di questo codice è che quando il raggruppamento è attivo dovrebbe restituire un valore di gruppo basato sul valore nella voce e altrimenti dovrebbe restituire un valore comune per tutte le voci. Se il valore è comune a tutte le voci, solo logicamente crea un singolo gruppo che è uguale a non raggruppare affatto.

Se si dispone di più elenchi a discesa per il quale si sta raggruppando, è necessario aggiungere ulteriori proprietà alla classe EntryGrouper. Non dimenticare di aggiungere queste nuove proprietà ai metodi Equals & GetHashCode.

Questa logica, pertanto, rappresenta il raggruppamento dinamico desiderato. Per favore fatemi sapere se ho aiutato o se avete bisogno di maggiori dettagli.

Divertiti!

+0

Grazie mille per la tua ampia risposta. Ci proverò domani e ti faccio sapere se funziona per me - una rapida occhiata è incoraggiante. –

+0

Questo non sembra raggruppare per StartOfWeek per qualche motivo.Ho dovuto modificare il codice di raggruppamento per ogni colonna su if (_section == "All") return _entry.Section; return _section; –

+0

@ 'Daniel Coffman' - Non so perché non è stato raggruppato da' StartOfWeek', dovrebbe avere. Ho ricontrollato il codice e i metodi 'Equals' e' GetHashCode' usano il valore 'StartOfWeek'. Dammi un grido se vuoi che guardi oltre. Mi aspettavo che il codice di raggruppamento per ogni colonna avrebbe probabilmente dovuto essere "ottimizzato" un po 'per le tue esigenze. – Enigmativity

8

Qui è in dinamica LINQ - naturalmente si costruisce la GroupBy e Select stringhe a runtime:

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

E qui è il modo normale LINQ che mi sfuggiva finché un collega indicò - questo è fondamentalmente la soluzione di Enigmativity senza la classe di cernia:

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

+1 per seguire la tua soluzione, grazie! –

0

io so che è stato un po 'che questa domanda è stata pubblicata, ma ho avuto a che fare con un problema simile di recente (raggruppamento dinamico da più colonne selezionate dall'utente in fase di esecuzione) in modo ecco la mia opinione su di esso.

  1. Funzione di supporto per la creazione di lambda raggruppamento

    static Expression<Func<T, Object>> GetGroupBy<T>(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<Func<T, Object>>(conversion, data); 
    } 
    
  2. funzione per fare il raggruppamento in memoria. Restituisce gruppi.

    static IEnumerable<IEnumerable<T>> Group<T>(IEnumerable<T> ds, params Func<T, object>[] groupSelectors) 
    { 
        Func<IEnumerable<T>, Func<T, object>[], IEnumerable<IEnumerable<T>>> 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. Come sarebbe essere utilizzato:

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

Per quanto riguarda le prestazioni degradanti, non credo che sia in realtà un (grosso) problema. Anche se hai creato un SQL di raggruppamento in modo dinamico, la query dovrebbe restituire lo stesso numero di righe di una query senza il raggruppamento. Quindi, anche se in questo approccio il raggruppamento non viene eseguito dal database, il numero di righe restituite dall'esecuzione forzata della query è lo stesso che sarebbe per l'ipotetica query SQL con i criteri di raggruppamento. Certo, il database probabilmente sovraperformare il raggruppamento in memoria fatto dal codice C# ma la quantità di traffico dipende unicamente dal numero di righe (entries) da raggruppare.