Ordenar una lista usando Lambda / Linq para objetos

276

Tengo el nombre de "ordenar por propiedad" en una cadena. Necesitaré usar Lambda / Linq para ordenar la lista de objetos.

Ex:

public class Employee
{
  public string FirstName {set; get;}
  public string LastName {set; get;}
  public DateTime DOB {set; get;}
}


public void Sort(ref List<Employee> list, string sortBy, string sortDirection)
{
  //Example data:
  //sortBy = "FirstName"
  //sortDirection = "ASC" or "DESC"

  if (sortBy == "FirstName")
  {
    list = list.OrderBy(x => x.FirstName).toList();    
  }

}
  1. En lugar de usar un montón de ifs para verificar el nombre del campo (sortBy), ¿hay una forma más limpia de ordenar?
  2. ¿Es consciente el tipo de datos?
DotnetDude
fuente
3
Dupe: stackoverflow.com/questions/606997/…
Mehrdad Afshari
Veo sortBy == "Nombre" . ¿El OP quería hacer .Equals () en su lugar?
Pieter
3
@Pieter, probablemente quiso comparar la igualdad, pero dudo que "pretendiera hacer .Equals ()". Los errores tipográficos generalmente no generan código que funcione.
C.Evenhuis
1
@Pieter Tu pregunta solo tiene sentido si crees que hay algo mal con ==... ¿qué?
Jim Balter

Respuestas:

367

Esto se puede hacer como

list.Sort( (emp1,emp2)=>emp1.FirstName.CompareTo(emp2.FirstName) );

El marco .NET está convirtiendo el lambda (emp1,emp2)=>intcomo unComparer<Employee>.

Esto tiene la ventaja de ser fuertemente tipado.

gls123
fuente
A menudo se me ocurrió escribir operadores de comparación complejos, que implican múltiples criterios de comparación y una comparación GUID a prueba de fallas al final para garantizar la antisimetría. ¿Usaría una expresión lambda para una comparación compleja como esa? Si no, ¿significa esto que las comparaciones de expresiones lambda solo deberían limitarse a casos simples?
Simone
44
si yo tampoco lo veo algo como esto? list.Sort ((emp1, emp2) => emp1.GetType (). GetProperty (sortBy) .GetValue (emp1, null) .CompareTo (emp2.GetType (). GetProperty (sortBy) .GetValue (emp2, null))) ;
Sábado
1
¿Cómo ordenar en reversa?
JerryGoyal
1
@JerryGoyal intercambia los parámetros ... emp2.FirstName.CompareTo (emp1.FirstName) etc.
Chris Hynes
3
El hecho de que sea una referencia de función no tiene que ser un trazador de líneas. Podrías escribirlist.sort(functionDeclaredElsewhere)
The Hoff
74

Una cosa que podría hacer es cambiar Sortpara que aproveche mejor las lambdas.

public enum SortDirection { Ascending, Descending }
public void Sort<TKey>(ref List<Employee> list,
                       Func<Employee, TKey> sorter, SortDirection direction)
{
  if (direction == SortDirection.Ascending)
    list = list.OrderBy(sorter);
  else
    list = list.OrderByDescending(sorter);
}

Ahora puede especificar el campo para ordenar al llamar al Sortmétodo.

Sort(ref employees, e => e.DOB, SortDirection.Descending);
Samuel
fuente
77
Dado que la columna de clasificación está en una cadena, aún necesitará un interruptor / bloques if-else para determinar qué función pasar.
tvanfosson
1
No puedes hacer esa suposición. Quién sabe cómo lo llama su código.
Samuel
3
Dijo en la pregunta que "ordenar por propiedad" está en una cadena. Solo voy por su pregunta.
tvanfosson
66
Creo que es más probable porque proviene de un control de clasificación en una página web que pasa la columna de clasificación como un parámetro de cadena. Ese sería mi caso de uso, de todos modos.
tvanfosson
2
@tvanfosson - Tienes razón, tengo un control personalizado que tiene el orden y el nombre del campo como una cadena
DotnetDude
55

Puede usar Reflection para obtener el valor de la propiedad.

list = list.OrderBy( x => TypeHelper.GetPropertyValue( x, sortBy ) )
           .ToList();

Donde TypeHelper tiene un método estático como:

public static class TypeHelper
{
    public static object GetPropertyValue( object obj, string name )
    {
        return obj == null ? null : obj.GetType()
                                       .GetProperty( name )
                                       .GetValue( obj, null );
    }
}

También es posible que desee ver Dynamic LINQ de la biblioteca de muestras VS2008 . Puede usar la extensión IEnumerable para emitir la Lista como IQueryable y luego usar la extensión de enlace dinámico OrderBy.

 list = list.AsQueryable().OrderBy( sortBy + " " + sortDirection );
tvanfosson
fuente
1
Si bien esto resuelve su problema, es posible que deseemos evitar que use una cadena para ordenarlo. Buena respuesta, no obstante.
Samuel
Puede usar Dynamic linq sin Linq to Sql para hacer lo que necesita ... Me encanta
JoshBerke
Por supuesto. Puedes convertirlo a IQueryable. No pensé en eso. Actualizando mi respuesta.
tvanfosson
@Samuel Si el ordenamiento se presenta como una variable de ruta, no hay otra forma de ordenarlo.
Chev
1
@ChuckD: lleve la colección a la memoria antes de intentar usarla, por ejemplocollection.ToList().OrderBy(x => TypeHelper.GetPropertyValue( x, sortBy)).ToList();
tvanfosson,
20

Así es como resolví mi problema:

List<User> list = GetAllUsers();  //Private Method

if (!sortAscending)
{
    list = list
           .OrderBy(r => r.GetType().GetProperty(sortBy).GetValue(r,null))
           .ToList();
}
else
{
    list = list
           .OrderByDescending(r => r.GetType().GetProperty(sortBy).GetValue(r,null))
           .ToList();
}
Cornel Urian
fuente
16

La construcción del orden por expresión se puede leer aquí

Descaradamente robado de la página en el enlace:

// First we define the parameter that we are going to use
// in our OrderBy clause. This is the same as "(person =>"
// in the example above.
var param = Expression.Parameter(typeof(Person), "person");

// Now we'll make our lambda function that returns the
// "DateOfBirth" property by it's name.
var mySortExpression = Expression.Lambda<Func<Person, object>>(Expression.Property(param, "DateOfBirth"), param);

// Now I can sort my people list.
Person[] sortedPeople = people.OrderBy(mySortExpression).ToArray();
Rashack
fuente
Hay problemas asociados con esto: ordenación de fecha y hora.
CrazyEnigma
Además, ¿qué hay de las clases compuestas, es decir, Person.Employer.CompanyName?
davewilliams459
Básicamente estaba haciendo lo mismo y esta respuesta lo resolvió.
Jason.Net
8

Puede usar la reflexión para acceder a la propiedad.

public List<Employee> Sort(List<Employee> list, String sortBy, String sortDirection)
{
   PropertyInfo property = list.GetType().GetGenericArguments()[0].
                                GetType().GetProperty(sortBy);

   if (sortDirection == "ASC")
   {
      return list.OrderBy(e => property.GetValue(e, null));
   }
   if (sortDirection == "DESC")
   {
      return list.OrderByDescending(e => property.GetValue(e, null));
   }
   else
   {
      throw new ArgumentOutOfRangeException();
   }
}

Notas

  1. ¿Por qué pasas la lista por referencia?
  2. Debe usar una enumeración para la dirección de clasificación.
  3. Podría obtener una solución mucho más limpia si pasara una expresión lambda que especificara la propiedad por la que ordenar en lugar del nombre de la propiedad como una cadena.
  4. En mi lista de ejemplo == null causará una NullReferenceException, debe detectar este caso.
Daniel Brückner
fuente
¿Alguien más ha notado que este es un tipo de retorno nulo pero devuelve listas?
emd
Al menos a nadie le importó arreglarlo y no me di cuenta porque no escribí el código usando un IDE. Gracias por señalar eso.
Daniel Brückner
6

Ordenar utiliza la interfaz IComparable, si el tipo lo implementa. Y puede evitar los ifs implementando un IComparer personalizado:

class EmpComp : IComparer<Employee>
{
    string fieldName;
    public EmpComp(string fieldName)
    {
        this.fieldName = fieldName;
    }

    public int Compare(Employee x, Employee y)
    {
        // compare x.fieldName and y.fieldName
    }
}

y entonces

list.Sort(new EmpComp(sortBy));
Serguei
fuente
FYI: Sort es un método de List <T> y no es una extensión de Linq.
Serguei
5

Respuesta para 1 .:

Debería poder construir manualmente un árbol de expresión que se pueda pasar a OrderBy utilizando el nombre como una cadena. O podría usar la reflexión como se sugiere en otra respuesta, que podría ser menos trabajo.

Editar : Aquí hay un ejemplo práctico de cómo construir un árbol de expresión manualmente. (Ordenando en X.Value, solo conociendo el nombre "Value" de la propiedad) Podría (debería) construir un método genérico para hacerlo.

using System;
using System.Linq;
using System.Linq.Expressions;

class Program
{
    private static readonly Random rand = new Random();
    static void Main(string[] args)
    {
        var randX = from n in Enumerable.Range(0, 100)
                    select new X { Value = rand.Next(1000) };

        ParameterExpression pe = Expression.Parameter(typeof(X), "value");
        var expression = Expression.Property(pe, "Value");
        var exp = Expression.Lambda<Func<X, int>>(expression, pe).Compile();

        foreach (var n in randX.OrderBy(exp))
            Console.WriteLine(n.Value);
    }

    public class X
    {
        public int Value { get; set; }
    }
}

Sin embargo, construir un árbol de expresión requiere que conozca los tipos de participación. Eso podría o no ser un problema en su escenario de uso. Si no sabe en qué tipo debe ordenar, será más fácil usar la reflexión.

Respuesta para 2 .:

Sí, dado que Comparer <T> .Default se usará para la comparación, si no define explícitamente el comparador.

driis
fuente
¿Tiene un ejemplo de construcción de un árbol de expresión para pasar a OrderBy?
DotnetDude
4
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Linq.Expressions;

public static class EnumerableHelper
{

    static MethodInfo orderBy = typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public).Where(x => x.Name == "OrderBy" && x.GetParameters().Length == 2).First();

    public static IEnumerable<TSource> OrderBy<TSource>(this IEnumerable<TSource> source, string propertyName)
    {
        var pi = typeof(TSource).GetProperty(propertyName, BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance);
        var selectorParam = Expression.Parameter(typeof(TSource), "keySelector");
        var sourceParam = Expression.Parameter(typeof(IEnumerable<TSource>), "source");
        return 
            Expression.Lambda<Func<IEnumerable<TSource>, IOrderedEnumerable<TSource>>>
            (
                Expression.Call
                (
                    orderBy.MakeGenericMethod(typeof(TSource), pi.PropertyType), 
                    sourceParam, 
                    Expression.Lambda
                    (
                        typeof(Func<,>).MakeGenericType(typeof(TSource), pi.PropertyType), 
                        Expression.Property(selectorParam, pi), 
                        selectorParam
                    )
                ), 
                sourceParam
            )
            .Compile()(source);
    }

    public static IEnumerable<TSource> OrderBy<TSource>(this IEnumerable<TSource> source, string propertyName, bool ascending)
    {
        return ascending ? source.OrderBy(propertyName) : source.OrderBy(propertyName).Reverse();
    }

}

Otro, esta vez para cualquier IQueryable:

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

public static class IQueryableHelper
{

    static MethodInfo orderBy = typeof(Queryable).GetMethods(BindingFlags.Static | BindingFlags.Public).Where(x => x.Name == "OrderBy" && x.GetParameters().Length == 2).First();
    static MethodInfo orderByDescending = typeof(Queryable).GetMethods(BindingFlags.Static | BindingFlags.Public).Where(x => x.Name == "OrderByDescending" && x.GetParameters().Length == 2).First();

    public static IQueryable<TSource> OrderBy<TSource>(this IQueryable<TSource> source, params string[] sortDescriptors)
    {
        return sortDescriptors.Length > 0 ? source.OrderBy(sortDescriptors, 0) : source;
    }

    static IQueryable<TSource> OrderBy<TSource>(this IQueryable<TSource> source, string[] sortDescriptors, int index)
    {
        if (index < sortDescriptors.Length - 1) source = source.OrderBy(sortDescriptors, index + 1);
        string[] splitted = sortDescriptors[index].Split(' ');
        var pi = typeof(TSource).GetProperty(splitted[0], BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance | BindingFlags.IgnoreCase);
        var selectorParam = Expression.Parameter(typeof(TSource), "keySelector");
        return source.Provider.CreateQuery<TSource>(Expression.Call((splitted.Length > 1 && string.Compare(splitted[1], "desc", StringComparison.Ordinal) == 0 ? orderByDescending : orderBy).MakeGenericMethod(typeof(TSource), pi.PropertyType), source.Expression, Expression.Lambda(typeof(Func<,>).MakeGenericType(typeof(TSource), pi.PropertyType), Expression.Property(selectorParam, pi), selectorParam)));
    }

}

Puede pasar varios criterios de clasificación, como este:

var q = dc.Felhasznalos.OrderBy(new string[] { "Email", "FelhasznaloID desc" });
Andras Vass
fuente
4

Desafortunadamente, la solución proporcionada por Rashack no funciona para los tipos de valor (int, enums, etc.).

Para que funcione con cualquier tipo de propiedad, esta es la solución que encontré:

public static Expression<Func<T, object>> GetLambdaExpressionFor<T>(this string sortColumn)
    {
        var type = typeof(T);
        var parameterExpression = Expression.Parameter(type, "x");
        var body = Expression.PropertyOrField(parameterExpression, sortColumn);
        var convertedBody = Expression.MakeUnary(ExpressionType.Convert, body, typeof(object));

        var expression = Expression.Lambda<Func<T, object>>(convertedBody, new[] { parameterExpression });

        return expression;
    }
Antoine Jaussoin
fuente
¡Esto es increíble e incluso se traduce correctamente a SQL!
Xavier Poinas
1

Además de lo que hicieron @Samuel y @bluish. Esto es mucho más corto ya que Enum no era necesario en este caso. Además, como una ventaja adicional cuando el Ascendente es el resultado deseado, puede pasar solo 2 parámetros en lugar de 3, ya que verdadero es la respuesta predeterminada al tercer parámetro.

public void Sort<TKey>(ref List<Person> list, Func<Person, TKey> sorter, bool isAscending = true)
{
    list = isAscending ? list.OrderBy(sorter) : list.OrderByDescending(sorter);
}
Stephen Whitlock
fuente
0

Si obtiene el nombre de la columna de clasificación y la dirección de clasificación como una cadena y no desea utilizar el interruptor o la sintaxis if \ else para determinar la columna, este ejemplo puede ser interesante para usted:

private readonly Dictionary<string, Expression<Func<IuInternetUsers, object>>> _sortColumns = 
        new Dictionary<string, Expression<Func<IuInternetUsers, object>>>()
    {
        { nameof(ContactSearchItem.Id),             c => c.Id },
        { nameof(ContactSearchItem.FirstName),      c => c.FirstName },
        { nameof(ContactSearchItem.LastName),       c => c.LastName },
        { nameof(ContactSearchItem.Organization),   c => c.Company.Company },
        { nameof(ContactSearchItem.CustomerCode),   c => c.Company.Code },
        { nameof(ContactSearchItem.Country),        c => c.CountryNavigation.Code },
        { nameof(ContactSearchItem.City),           c => c.City },
        { nameof(ContactSearchItem.ModifiedDate),   c => c.ModifiedDate },
    };

    private IQueryable<IuInternetUsers> SetUpSort(IQueryable<IuInternetUsers> contacts, string sort, string sortDir)
    {
        if (string.IsNullOrEmpty(sort))
        {
            sort = nameof(ContactSearchItem.Id);
        }

        _sortColumns.TryGetValue(sort, out var sortColumn);
        if (sortColumn == null)
        {
            sortColumn = c => c.Id;
        }

        if (string.IsNullOrEmpty(sortDir) || sortDir == SortDirections.AscendingSort)
        {
            contacts = contacts.OrderBy(sortColumn);
        }
        else
        {
            contacts = contacts.OrderByDescending(sortColumn);
        }

        return contacts;
    }

Solución basada en el uso del Diccionario que conecta lo necesario para ordenar la columna a través de Expresión> y su cadena clave.

En línea123321
fuente