c# - sintaxis - Clasificación dinámica Linq fuertemente tipada
sintaxis linq c# (4)
¡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 genéricos, 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<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; }
}
}
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<TSource>
...
public static Sorter<TSource> GetSorter(this IQueryable<TSource> 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<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 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)?
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<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
}
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.
La forma 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 usado object como el tipo de retorno del func, porque es fácilmente convertible automáticamente, pero puede implementarlo con diferentes tipos, o genéricos, 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)
Me gustó el trabajo anterior, ¡muchas gracias! Me tomé la libertad de agregar un par de cosas:
Dirección de clasificación agregada.
Hecho registrando y llamando 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>>> { }
}