2009-02-17 14 views
24

Estoy intentando construir algún código para ordenar dinámicamente un IQueryable de Linq <>.Clasificación dinámica Linq fuertemente tipada

La forma más obvia es que aquí, que ordena una lista usando una cadena para el nombre del campo
http://dvanderboom.wordpress.com/2008/12/19/dynamically-composing-linq-orderby-clauses/

Sin embargo, quiero un cambio - tiempo de compilación comprobación de nombres de campo, y la capacidad de utilizar la refactorización/Encontrar Todos Referencias para apoyar el mantenimiento posterior. Eso significa que quiero definir los campos como f => f.Name, en lugar de como cadenas.

Para mi uso específico, deseo encapsular un código que decida cuál de una lista de expresiones nombradas "OrderBy" debería usarse en función de la entrada del usuario, sin escribir código diferente cada vez.

Aquí es la esencia de lo que he escrito:

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 función GetSortedList determina cuál de los tipos mencionados de usar, lo que se traduce en un objeto de lista, donde cada fielddata contiene los valores Tipo y MethodInfo de los campos pasados ​​en 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<> 
} 

no estoy seguro de si hay una manera de almacenar la totalidad del objeto de campo de una manera que le permita ser devuelto después (que sería imposible fundido, ya que es un tipo genérico)

¿Hay alguna manera de adaptar el código de muestra o crear un código completamente nuevo para ordenar usando los nombres de campos fuertemente tipados después de que se han almacenado en algún contenedor y recuperado (perdiendo cualquier tipo genérico de conversión)

+2

ver post: (. Puede ayudar a otra que busca la orden "mágica" por) http://stackoverflow.com/questions/41244/dynamic-linq-orderby – Nordes

Respuesta

17

La manera más fácil de hacer esto sería hacer que su función AddSort() tome una Expresión < Func < Película >> en lugar de solo una Func. Esto permite que su método de clasificación inspeccione Expression para extraer el nombre de la propiedad que desea ordenar. A continuación, puede almacenar este nombre internamente como una cadena, por lo que el almacenamiento es muy fácil y puede usar el algoritmo de clasificación al que se vinculó, pero también obtiene la seguridad del tipo y compila la verificación del tiempo de los nombres de propiedad válidos.

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

En este caso, he utilizado objeto como el tipo de retorno de la func, debido a su facilidad automáticamente convertible, pero se podría poner en práctica que, con diferentes tipos genéricos, o, en su caso, si necesita más funcionalidad . En este caso, dado que la Expresión está allí para ser inspeccionada, en realidad no importa.

La otra forma posible es tomar un Func y almacenarlo en el mismo diccionario. Luego, cuando se trata de ordenar, y necesita obtener el valor para ordenar, puede llamar a algo como:

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

Hola Ch00k, ¡este código se ve increíble! Tengo la misma necesidad, a excepción de un GroupBy ... ¿te importaría ayudarme? ¡Gracias! – ibiza

+0

Pero, ¿cuál es el código para ordenar por cada 'fieldName'? –

8

Bummer! Debo aprender a leer las especificaciones de un extremo a otro :-(

Sin embargo, ahora que he pasado demasiado tiempo bromeando en lugar de trabajar, publicaré mis resultados de todos modos con la esperanza de que esto inspire a la gente a leer, pensar , entender (importante) y luego actuar. O cómo ser demasiado inteligente con genéricos, lambdas y cosas divertidas de Linq.

Un truco que descubrí durante este ejercicio, son aquellas privadas internas clases que se deriva de Dictionary. Todo su propósito es eliminar todos los los soportes angulares con el fin de mejorar la legibilidad.

Ah, se me olvidó el código:

ACTUALIZACIÓN: Hecho el código genérico y utilizar IQueryable en lugar de 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

Esta solución es mucho menos código que lo que tengo, solo que creo que infringe un requisito no escrito. Necesito un enlace tardío IQueryable ya que las listas ordenadas pasan por Omitir/Tomar a la página de grandes conjuntos de datos SQL. Sin embargo, me ayudó: no me gusta "AddSort" para este propósito, "Definir" es mucho mejor. – David

+0

Sí, vi que usaste la interfaz IQueryable, pero me emocioné tanto que lo olvidé por completo. Veré si puedo conseguirlo ... –

8

Sobre la base de lo que todos han contribuido he llegado con el seguimiento.

Proporciona clasificación bidireccional y resuelve el problema de adentro hacia afuera. Lo que significa que no tenía mucho sentido para mí que un nuevo Clasificador necesita ser creado para cada lista sin clasificar de un tipo determinado. ¿Por qué no puede esto pasar la lista sin clasificar al clasificador? Esto significa, entonces, que se podría crear una instancia signelton del Clasificador de nuestros diferentes tipos ...

Sólo una 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 si no quiere tener una dependencia de SortInstrcution que wouldn No será tan difícil de cambiar.

Espero que esto ayude a alguien.

+4

necesitas cambiar SortInstrcution a SortInstruction – Timmerz

3

Me gustó el trabajo anterior, ¡muchas gracias! Me tomé la libertad de agregar un par de cosas:

  1. Se agregó la dirección de clasificación.

  2. Hizo registrar y llamar a dos preocupaciones diferentes.

Uso:

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

Esto es realmente bueno. Supongo que la razón por la que desacopla AddSortExpression y Sort es para poder deshabilitar la clasificación en ciertas 'columnas' en su conjunto de datos. –

Cuestiones relacionadas