Clasificación dinámica Linq fuertemente tipada

Estoy intentando construir un código para ordenar dinámicamente un Linq IQueryable .

La forma obvia es 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: compruebe el tiempo de verificación de los nombres de los campos y la posibilidad de utilizar refactorización / Buscar todas las referencias para permitir 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 algún código que decida cuál de una lista de expresiones nombradas “OrderBy” debería usarse según la entrada del usuario, sin escribir código diferente cada vez.

Aquí está 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 ... public static Sorter GetSorter(this IQueryable source, ...) 

La función GetSortedList determina cuál de los géneros nombrados se utilizará, lo que da como resultado un objeto List, donde cada FieldData contiene los valores MethodInfo y Type de los campos pasados ​​en AddSort:

 public SorterItem AddSort(Func 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 todo el objeto de campo de una manera que permita que se devuelva más tarde (sería imposible realizar el lanzamiento, 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 nombres de campos fuertemente tipados después de que se hayan almacenado en algún contenedor y se hayan recuperado (perdiendo cualquier tipo de conversión genérica)?

La forma más fácil de hacer esto sería hacer que su función AddSort () tome una Expresión > 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 comstack 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(); sorter.AddSort("NAME", m => m.Name); } class Sorter { public void AddSort(string name, Expression> func) { string fieldName = (func.Body as MemberExpression).Member.Name; } } 

En este caso, he usado object como el tipo de retorno del func, porque es fácilmente convertible automáticamente, pero puede implementarlo con diferentes tipos, o generics, según corresponda, 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 diccionario. Luego, cuando se trata de ordenar, y necesita obtener el valor para ordenar, puede llamar algo como:

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

¡Gorrón! Debo aprender a leer las especificaciones de extremo a extremo 🙁

Sin embargo, ahora que he pasado demasiado tiempo jugando 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 generics, lambdas y cosas divertidas de Linq.

Un buen truco que descubrí durante este ejercicio, son esas clases internas privadas que se derivan de Dictionary . Todo su propósito es eliminar todos los corchetes angulares para mejorar la legibilidad.

Ah, casi olvido el código:

ACTUALIZACIÓN: Hizo que el código sea genérico y use IQueryable 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 sorter; [SetUp] public void Setup() { var unsorted = from m in Movies select m; sorter = new Sorter(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 Movies { get { return CreateMovies().AsQueryable(); } } private static IEnumerable 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 { public Sorter(IQueryable unsorted) { this.unsorted = unsorted; } public void Define

(string name, Expression> selector) { firstPasses.Add(name, s => s.OrderBy(selector)); nextPasses.Add(name, s => s.ThenBy(selector)); } public IOrderedQueryable SortBy(params string[] names) { IOrderedQueryable result = null; foreach (var name in names) { result = result == null ? SortFirst(name, unsorted) : SortNext(name, result); } return result; } private IOrderedQueryable SortFirst(string name, IQueryable source) { return firstPasses[name].Invoke(source); } private IOrderedQueryable SortNext(string name, IOrderedQueryable source) { return nextPasses[name].Invoke(source); } private readonly IQueryable unsorted; private readonly FirstPasses firstPasses = new FirstPasses(); private readonly NextPasses nextPasses = new NextPasses(); private class FirstPasses : Dictionary, IOrderedQueryable>> {} private class NextPasses : Dictionary, IOrderedQueryable>> {} } internal class Movie { public string Name { get; set; } public int Year { get; set; } } }

Basado en lo que todos han contribuido, he encontrado lo siguiente.

Proporciona clasificación bidireccional y soluciona 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 que podríamos crear una instancia signelton del Clasificador para nuestros diferentes tipos …

Solo una idea:

 [TestClass] public class SpecifyUserDefinedSorting { private Sorter sorter; private IQueryable unsorted; [TestInitialize] public void Setup() { unsorted = from m in Movies select m; sorter = new Sorter(); sorter.Register("Name", m1 => m1.Name); sorter.Register("Year", m2 => m2.Year); } [TestMethod] public void SortByNameThenYear() { var instructions = new List() { 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() { 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() { 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() { 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() { new SortInstrcution() {Name = "Year"} }; var sorted = sorter.SortBy(unsorted, instructions); var movies = sorted.ToArray(); Assert.AreEqual(movies[0].Name, "B"); } private static IQueryable Movies { get { return CreateMovies().AsQueryable(); } } private static IEnumerable 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 SortBy(this IQueryable source, Sorter sorter, IEnumerable instrcutions) { return sorter.SortBy(source, instrcutions); } } public class Sorter { 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(string name, Expression> 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 SortBy(IQueryable source, IEnumerable instrcutions) { IOrderedQueryable result = null; foreach (var instrcution in instrcutions) result = result == null ? this.SortFirst(instrcution, source) : this.SortNext(instrcution, result); return result; } private IOrderedQueryable SortFirst(SortInstrcution instrcution, IQueryable source) { if (instrcution.Direction == SortDirection.Ascending) return this._FirstPasses[instrcution.Name].Invoke(source); return this._FirstDescendingPasses[instrcution.Name].Invoke(source); } private IOrderedQueryable SortNext(SortInstrcution instrcution, IOrderedQueryable source) { if (instrcution.Direction == SortDirection.Ascending) return this._NextPasses[instrcution.Name].Invoke(source); return this._NextDescendingPasses[instrcution.Name].Invoke(source); } private class FirstPasses : Dictionary, IOrderedQueryable>> { } private class NextPasses : Dictionary, IOrderedQueryable>> { } } 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 } 

Tenga en cuenta que si no desea tener una dependencia en SortInstrcution no sería tan difícil de cambiar.

Espero que esto ayude a alguien.

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

  1. Dirección de clasificación agregada.

  2. Hecho registrando y llamando dos preocupaciones diferentes.

Uso:

 var censusSorter = new Sorter(); censusSorter.AddSortExpression("SubscriberId", e=>e.SubscriberId); censusSorter.AddSortExpression("LastName", e => e.SubscriberId); View.CensusEntryDataSource = censusSorter.Sort(q.AsQueryable(), new Tuple("SubscriberId", SorterSortDirection.Descending), new Tuple("LastName", SorterSortDirection.Ascending)) .ToList(); internal class Sorter { public Sorter() { } public void AddSortExpression

(string name, Expression> 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 Sort(IQueryable list, params Tuple[] names) { IOrderedQueryable 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 SortFirst(string name, SorterSortDirection direction, IQueryable source) { return direction == SorterSortDirection.Descending ? firstPassesDesc[name].Invoke(source) : firstPasses[name].Invoke(source); } private IOrderedQueryable SortNext(string name, SorterSortDirection direction, IOrderedQueryable 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, IOrderedQueryable>> { } private class NextPasses : Dictionary, IOrderedQueryable>> { } }