2009-02-17 16 views
24

Sto provando a creare codice per l'ordinamento dinamico di Linq IQueryable <>.Ordinamento Linq dinamico fortemente tipizzato

Il modo più ovvio è qui, che ordina un elenco utilizzando una stringa per il nome del campo
http://dvanderboom.wordpress.com/2008/12/19/dynamically-composing-linq-orderby-clauses/

Tuttavia voglio un cambio - tempo di compilazione il controllo dei nomi di campo, e la possibilità di utilizzare il refactoring/Trova Tutto Riferimenti per supportare la manutenzione successiva. Ciò significa che voglio definire i campi come f => f.Name, anziché come stringhe.

Per il mio uso specifico, desidero incapsulare un codice che decida quale di un elenco di espressioni denominate "OrderBy" debba essere utilizzato in base all'input dell'utente, senza scrivere codice diverso ogni volta.

Ecco l'essenza di quello che ho scritto:

var list = from m Movies select m; // Get our list 

var sorter = list.GetSorter(...); // Pass in some global user settings object 

sorter.AddSort("NAME", m=>m.Name); 
sorter.AddSort("YEAR", m=>m.Year).ThenBy(m=>m.Year); 

list = sorter.GetSortedList(); 

... 
public class Sorter<TSource> 
... 
public static Sorter<TSource> GetSorter(this IQueryable<TSource> source, ...) 

La funzione GetSortedList determina quale dei tipi di nome da usare, che si traduce in un oggetto List, in cui ogni FieldData contiene i valori di tipo MethodInfo e dei campi passati in AddSort:

public SorterItem<TSource> AddSort(Func<T, TKey> field) 
{ 
    MethodInfo ... = field.Method; 
    Type ... = TypeOf(TKey); 
    // Create item, add item to diction, add fields to item's List<> 
    // The item has the ThenBy method, which just adds another field to the List<> 
} 

non sono sicuro se c'è un modo per memorizzare l'intero oggetto campo in modo tale da permettere che essere restituito dopo (sarebbe impossibile per lanciare, dal momento che è un tipo generico)

Esiste un modo per adattare il codice di esempio o creare codice completamente nuovo, per ordinare i nomi di campo fortemente tipizzati dopo sono stati memorizzati in alcuni contenitori e recuperati (perdendo qualsiasi tipo generico di trasmissione)

+2

vedi post: (. Può aiutare altri che cerca ordine "magico" da) http://stackoverflow.com/questions/41244/dynamic-linq-orderby – Nordes

risposta

17

Il modo più semplice per farlo sarebbe quello di fare in modo che la funzione AddSort() esegua un'espressione < Func < >> anziché solo un Func. Ciò consente al metodo di ordinamento di ispezionare l'espressione per estrarre il nome della proprietà su cui si desidera ordinare. È quindi possibile memorizzare questo nome internamente come stringa, quindi la memorizzazione è molto semplice ed è possibile utilizzare l'algoritmo di ordinamento a cui è collegato, ma si ottiene anche la sicurezza del tipo e il controllo della compilazione per nomi di proprietà validi.

static void Main(string[] args) 
{ 
    var query = from m in Movies select m; 

    var sorter = new Sorter<Movie>(); 
    sorter.AddSort("NAME", m => m.Name); 
} 

class Sorter<T> 
{ 
    public void AddSort(string name, Expression<Func<T, object>> func) 
    { 
     string fieldName = (func.Body as MemberExpression).Member.Name; 
    } 
} 

In questo caso, ho usato oggetto come il tipo di ritorno del func, perché la sua facilmente convertibili automaticamente, ma è possibile implementare che con diversi tipi, o generici, se del caso, se avete bisogno di più funzionalità . In questo caso, dal momento che Expression è lì solo per essere ispezionato, non ha molta importanza.

L'altro modo possibile è di prendere un Func e di memorizzarlo nel dizionario stesso. Quindi, quando si tratta di ordinare, e devi ottenere il valore per ordinare, puoi chiamare qualcosa come:

// assuming a dictionary of fields to sort for, called m_fields 
m_fields[fieldName](currentItem) 
+0

Ciao Ch00k, questo codice sembra fantastico! Ho lo stesso bisogno, ad eccezione di un GroupBy ... ti dispiacerebbe aiutarmi? Grazie! – ibiza

+0

Ma qual è il codice da ordinare per ogni 'fieldName'? –

8

Bummer! - Devo imparare a leggere le specifiche da capo a capo :-(

Tuttavia, ora che ho passato troppo tempo a fare scherzi piuttosto che lavorare, pubblicherò comunque i miei risultati sperando che questo ispiri le persone a leggere, pensare , capisci (importante) e poi agisci, o come essere troppo furbo con i generici, i lambda e le cose divertenti di Linq.

Un trucco che ho scoperto nel corso di questo esercizio , sono quelle private interne classi che deriva da Dictionary. Il loro unico scopo è quello di rimuovere tutte le parentesi angolari al fine di migliorare la leggibilità dello .

Ah, quasi dimenticavo il codice:

UPDATE: Fatto il codice generico e di utilizzare IQueryable invece di IEnumerable

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Linq.Expressions; 
using NUnit.Framework; 
using NUnit.Framework.SyntaxHelpers; 


namespace StackOverflow.StrongTypedLinqSort 
{ 
    [TestFixture] 
    public class SpecifyUserDefinedSorting 
    { 
     private Sorter<Movie> sorter; 

     [SetUp] 
     public void Setup() 
     { 
      var unsorted = from m in Movies select m; 
      sorter = new Sorter<Movie>(unsorted); 

      sorter.Define("NAME", m1 => m1.Name); 
      sorter.Define("YEAR", m2 => m2.Year); 
     } 

     [Test] 
     public void SortByNameThenYear() 
     { 
      var sorted = sorter.SortBy("NAME", "YEAR"); 
      var movies = sorted.ToArray(); 

      Assert.That(movies[0].Name, Is.EqualTo("A")); 
      Assert.That(movies[0].Year, Is.EqualTo(2000)); 
      Assert.That(movies[1].Year, Is.EqualTo(2001)); 
      Assert.That(movies[2].Name, Is.EqualTo("B")); 
     } 

     [Test] 
     public void SortByYearThenName() 
     { 
      var sorted = sorter.SortBy("YEAR", "NAME"); 
      var movies = sorted.ToArray(); 

      Assert.That(movies[0].Name, Is.EqualTo("B")); 
      Assert.That(movies[1].Year, Is.EqualTo(2000)); 
     } 

     [Test] 
     public void SortByYearOnly() 
     { 
      var sorted = sorter.SortBy("YEAR"); 
      var movies = sorted.ToArray(); 

      Assert.That(movies[0].Name, Is.EqualTo("B")); 
     } 

     private static IQueryable<Movie> Movies 
     { 
      get { return CreateMovies().AsQueryable(); } 
     } 

     private static IEnumerable<Movie> CreateMovies() 
     { 
      yield return new Movie {Name = "B", Year = 1990}; 
      yield return new Movie {Name = "A", Year = 2001}; 
      yield return new Movie {Name = "A", Year = 2000}; 
     } 
    } 


    internal class Sorter<E> 
    { 
     public Sorter(IQueryable<E> unsorted) 
     { 
      this.unsorted = unsorted; 
     } 

     public void Define<P>(string name, Expression<Func<E, P>> selector) 
     { 
      firstPasses.Add(name, s => s.OrderBy(selector)); 
      nextPasses.Add(name, s => s.ThenBy(selector)); 
     } 

     public IOrderedQueryable<E> SortBy(params string[] names) 
     { 
      IOrderedQueryable<E> result = null; 

      foreach (var name in names) 
      { 
       result = result == null 
          ? SortFirst(name, unsorted) 
          : SortNext(name, result); 
      } 

      return result; 
     } 

     private IOrderedQueryable<E> SortFirst(string name, IQueryable<E> source) 
     { 
      return firstPasses[name].Invoke(source); 
     } 

     private IOrderedQueryable<E> SortNext(string name, IOrderedQueryable<E> source) 
     { 
      return nextPasses[name].Invoke(source); 
     } 

     private readonly IQueryable<E> unsorted; 
     private readonly FirstPasses firstPasses = new FirstPasses(); 
     private readonly NextPasses nextPasses = new NextPasses(); 


     private class FirstPasses : Dictionary<string, Func<IQueryable<E>, IOrderedQueryable<E>>> {} 


     private class NextPasses : Dictionary<string, Func<IOrderedQueryable<E>, IOrderedQueryable<E>>> {} 
    } 


    internal class Movie 
    { 
     public string Name { get; set; } 
     public int Year { get; set; } 
    } 
} 
+1

Questa soluzione è molto meno codice di quello che ho, solo penso che violi un requisito non scritto - Ho bisogno di un binding tardivo IQuery mentre gli elenchi ordinati passano attraverso Skip/Take per visualizzare grandi dataset SQL. Comunque hai aiutato - non mi piaceva "AddSort" per questo scopo, "Definisci" è molto meglio. – David

+0

Sì, ho visto che hai usato l'interfaccia IQueryable, ma mi sono così emozionato che ho dimenticato tutto. Vedrò se riesco a farlo ... –

8

In base a quello che tutti hanno contribuito sono venuto su con il seguente.

Fornisce l'ordinamento bidirezionale e risolve il problema al suo interno. Significa che per me non aveva alcun senso che fosse necessario creare un nuovo fascicolatore per ogni elenco non ordinato di un determinato tipo. Perché questa lista non ordinata non può essere passata nel sorter. Questo significa allora che potremmo creare un'istanza signelton del Sorter per i nostri diversi tipi ...

Solo un'idea:

[TestClass] 
public class SpecifyUserDefinedSorting 
{ 
    private Sorter<Movie> sorter; 
    private IQueryable<Movie> unsorted; 

    [TestInitialize] 
    public void Setup() 
    { 
     unsorted = from m in Movies select m; 
     sorter = new Sorter<Movie>(); 
     sorter.Register("Name", m1 => m1.Name); 
     sorter.Register("Year", m2 => m2.Year); 
    } 

    [TestMethod] 
    public void SortByNameThenYear() 
    { 
     var instructions = new List<SortInstrcution>() 
           { 
            new SortInstrcution() {Name = "Name"}, 
            new SortInstrcution() {Name = "Year"} 
           }; 
     var sorted = sorter.SortBy(unsorted, instructions); 
     var movies = sorted.ToArray(); 

     Assert.AreEqual(movies[0].Name, "A"); 
     Assert.AreEqual(movies[0].Year, 2000); 
     Assert.AreEqual(movies[1].Year, 2001); 
     Assert.AreEqual(movies[2].Name, "B"); 
    } 

    [TestMethod] 
    public void SortByNameThenYearDesc() 
    { 
     var instructions = new List<SortInstrcution>() 
           { 
            new SortInstrcution() {Name = "Name", Direction = SortDirection.Descending}, 
            new SortInstrcution() {Name = "Year", Direction = SortDirection.Descending} 
           }; 
     var sorted = sorter.SortBy(unsorted, instructions); 
     var movies = sorted.ToArray(); 

     Assert.AreEqual(movies[0].Name, "B"); 
     Assert.AreEqual(movies[0].Year, 1990); 
     Assert.AreEqual(movies[1].Name, "A"); 
     Assert.AreEqual(movies[1].Year, 2001); 
     Assert.AreEqual(movies[2].Name, "A"); 
     Assert.AreEqual(movies[2].Year, 2000); 
    } 

    [TestMethod] 
    public void SortByNameThenYearDescAlt() 
    { 
     var instructions = new List<SortInstrcution>() 
           { 
            new SortInstrcution() {Name = "Name", Direction = SortDirection.Descending}, 
            new SortInstrcution() {Name = "Year"} 
           }; 
     var sorted = sorter.SortBy(unsorted, instructions); 
     var movies = sorted.ToArray(); 

     Assert.AreEqual(movies[0].Name, "B"); 
     Assert.AreEqual(movies[0].Year, 1990); 
     Assert.AreEqual(movies[1].Name, "A"); 
     Assert.AreEqual(movies[1].Year, 2000); 
     Assert.AreEqual(movies[2].Name, "A"); 
     Assert.AreEqual(movies[2].Year, 2001); 
    } 

    [TestMethod] 
    public void SortByYearThenName() 
    { 
     var instructions = new List<SortInstrcution>() 
           { 
            new SortInstrcution() {Name = "Year"}, 
            new SortInstrcution() {Name = "Name"} 
           }; 
     var sorted = sorter.SortBy(unsorted, instructions); 
     var movies = sorted.ToArray(); 

     Assert.AreEqual(movies[0].Name, "B"); 
     Assert.AreEqual(movies[1].Year, 2000); 
    } 

    [TestMethod] 
    public void SortByYearOnly() 
    { 
     var instructions = new List<SortInstrcution>() 
           { 
            new SortInstrcution() {Name = "Year"} 
           }; 
     var sorted = sorter.SortBy(unsorted, instructions); 
     var movies = sorted.ToArray(); 

     Assert.AreEqual(movies[0].Name, "B"); 
    } 

    private static IQueryable<Movie> Movies 
    { 
     get { return CreateMovies().AsQueryable(); } 
    } 

    private static IEnumerable<Movie> CreateMovies() 
    { 
     yield return new Movie { Name = "B", Year = 1990 }; 
     yield return new Movie { Name = "A", Year = 2001 }; 
     yield return new Movie { Name = "A", Year = 2000 }; 
    } 
} 


public static class SorterExtension 
{ 
    public static IOrderedQueryable<T> SortBy<T>(this IQueryable<T> source, Sorter<T> sorter, IEnumerable<SortInstrcution> instrcutions) 
    { 
     return sorter.SortBy(source, instrcutions); 
    } 
} 

public class Sorter<TSource> 
{ 
    private readonly FirstPasses _FirstPasses; 
    private readonly FirstPasses _FirstDescendingPasses; 
    private readonly NextPasses _NextPasses; 
    private readonly NextPasses _NextDescendingPasses; 

    public Sorter() 
    { 
     this._FirstPasses = new FirstPasses(); 
     this._FirstDescendingPasses = new FirstPasses(); 
     this._NextPasses = new NextPasses(); 
     this._NextDescendingPasses = new NextPasses(); 
    } 


    public void Register<TKey>(string name, Expression<Func<TSource, TKey>> selector) 
    { 
     this._FirstPasses.Add(name, s => s.OrderBy(selector)); 
     this._FirstDescendingPasses.Add(name, s => s.OrderByDescending(selector)); 
     this._NextPasses.Add(name, s => s.ThenBy(selector)); 
     this._NextDescendingPasses.Add(name, s => s.ThenByDescending(selector)); 
    } 


    public IOrderedQueryable<TSource> SortBy(IQueryable<TSource> source, IEnumerable<SortInstrcution> instrcutions) 
    { 
     IOrderedQueryable<TSource> result = null; 

     foreach (var instrcution in instrcutions) 
      result = result == null ? this.SortFirst(instrcution, source) : this.SortNext(instrcution, result); 

     return result; 
    } 

    private IOrderedQueryable<TSource> SortFirst(SortInstrcution instrcution, IQueryable<TSource> source) 
    { 
     if (instrcution.Direction == SortDirection.Ascending) 
      return this._FirstPasses[instrcution.Name].Invoke(source); 
     return this._FirstDescendingPasses[instrcution.Name].Invoke(source); 
    } 

    private IOrderedQueryable<TSource> SortNext(SortInstrcution instrcution, IOrderedQueryable<TSource> source) 
    { 
     if (instrcution.Direction == SortDirection.Ascending) 
      return this._NextPasses[instrcution.Name].Invoke(source); 
     return this._NextDescendingPasses[instrcution.Name].Invoke(source); 
    } 

    private class FirstPasses : Dictionary<string, Func<IQueryable<TSource>, IOrderedQueryable<TSource>>> { } 

    private class NextPasses : Dictionary<string, Func<IOrderedQueryable<TSource>, IOrderedQueryable<TSource>>> { } 
} 


internal class Movie 
{ 
    public string Name { get; set; } 
    public int Year { get; set; } 
} 

public class SortInstrcution 
{ 
    public string Name { get; set; } 

    public SortDirection Direction { get; set; } 
} 

public enum SortDirection 
{ 
    //Note I have created this enum because the one that exists in the .net 
    // framework is in the web namespace... 
    Ascending, 
    Descending 
} 

Nota se non si vuole avere una dipendenza da SortInstrcution esso wouldn essere così difficile da cambiare

Spero che questo aiuti qualcuno.

+4

devi cambiare SortInstrcution a SortInstruction – Timmerz

3

Mi è piaciuto il lavoro sopra - grazie mille! Mi sono permesso di aggiungere un paio di cose:

  1. Aggiunto ordine di ordinamento.

  2. Effettuati registrando e chiamando due diversi dubbi.

Usage:

var censusSorter = new Sorter<CensusEntryVM>(); 
censusSorter.AddSortExpression("SubscriberId", e=>e.SubscriberId); 
censusSorter.AddSortExpression("LastName", e => e.SubscriberId); 

View.CensusEntryDataSource = censusSorter.Sort(q.AsQueryable(), 
    new Tuple<string, SorterSortDirection>("SubscriberId", SorterSortDirection.Descending), 
    new Tuple<string, SorterSortDirection>("LastName", SorterSortDirection.Ascending)) 
    .ToList(); 



internal class Sorter<E> 
{ 
    public Sorter() 
    { 
    } 
    public void AddSortExpression<P>(string name, Expression<Func<E, P>> selector) 
    { 
     // Register all possible types of sorting for each parameter 
     firstPasses.Add(name, s => s.OrderBy(selector)); 
     nextPasses.Add(name, s => s.ThenBy(selector)); 
     firstPassesDesc.Add(name, s => s.OrderByDescending(selector)); 
     nextPassesDesc.Add(name, s => s.OrderByDescending(selector)); 
    } 

    public IOrderedQueryable<E> Sort(IQueryable<E> list, 
            params Tuple<string, SorterSortDirection>[] names) 
    { 
     IOrderedQueryable<E> result = null; 
     foreach (var entry in names) 
     { 
      result = result == null 
        ? SortFirst(entry.Item1, entry.Item2, list) 
        : SortNext(entry.Item1, entry.Item2, result); 
     } 
     return result; 
    } 
    private IOrderedQueryable<E> SortFirst(string name, SorterSortDirection direction, 
              IQueryable<E> source) 
    { 
     return direction == SorterSortDirection.Descending 
      ? firstPassesDesc[name].Invoke(source) 
      : firstPasses[name].Invoke(source); 
    } 

    private IOrderedQueryable<E> SortNext(string name, SorterSortDirection direction, 
              IOrderedQueryable<E> source) 
    { 
     return direction == SorterSortDirection.Descending 
      ? nextPassesDesc[name].Invoke(source) 
      : nextPasses[name].Invoke(source); 
    } 

    private readonly FirstPasses firstPasses = new FirstPasses(); 
    private readonly NextPasses nextPasses = new NextPasses(); 
    private readonly FirstPasses firstPassesDesc = new FirstPasses(); 
    private readonly NextPasses nextPassesDesc = new NextPasses(); 

    private class FirstPasses : Dictionary<string, Func<IQueryable<E>, IOrderedQueryable<E>>> { } 
    private class NextPasses : Dictionary<string, Func<IOrderedQueryable<E>, IOrderedQueryable<E>>> { } 
} 
+0

Questo è veramente bello. Sto indovinando il motivo per cui hai disaccoppiato AddSortExpression e Sort in modo da poter disabilitare l'ordinamento su determinate "colonne" nel tuo set di dati? –

Problemi correlati