¿Cómo aplico OrderBy en un IQueryable usando un nombre de columna de cadena dentro de un método de extensión genérico?

85
public static IQueryable<TResult> ApplySortFilter<T, TResult>(this IQueryable<T> query, string columnName)
  where T : EntityObject
{
  var param = Expression.Parameter(typeof(T), "o");
  var body = Expression.PropertyOrField(param,columnName);

  var sortExpression = Expression.Lambda(body, param);
  return query.OrderBy(sortExpression);
}

Debido a que el tipo de OrderBy no se infiere de sortExpression, necesito especificar algo como esto en tiempo de ejecución:

var sortExpression = Expression.Lambda<T, TSortColumn>(body, param);

O

return query.OrderBy<T, TSortColumn>(sortExpression);

Sin embargo, no creo que esto sea posible, ya que TSortColumn solo se puede determinar durante el tiempo de ejecución.

¿Hay alguna forma de evitar esto?

JTew
fuente
No estoy seguro de si esto es lo que estás buscando, pero echa un vistazo. Saludos
joaopintocruz
@JTew ¿Cómo puedo implementar un segundo pedido por cláusula ... decir orderby id y luego por fecha
SRJ

Respuestas:

113

Hicimos algo similar (no 100% igual, pero similar) en un proyecto LINQ to SQL. Aquí está el código:

public static IQueryable<T> OrderBy<T>(this IQueryable<T> source, string ordering, params object[] values) {
    var type = typeof(T);
    var property = type.GetProperty(ordering);
    var parameter = Expression.Parameter(type, "p");
    var propertyAccess = Expression.MakeMemberAccess(parameter, property);
    var orderByExp = Expression.Lambda(propertyAccess, parameter);
    MethodCallExpression resultExp = Expression.Call(typeof(Queryable), "OrderBy", new Type[] { type, property.PropertyType }, source.Expression, Expression.Quote(orderByExp));
    return source.Provider.CreateQuery<T>(resultExp);
}

En realidad, no usamos un genérico, teníamos una clase conocida, pero debería funcionar en un genérico (he puesto el marcador de posición genérico donde debería estar).

Editar: para orden descendente, pase en OrderByDescendinglugar de "OrderBy":

MethodCallExpression resultExp = Expression.Call(typeof(Queryable), "OrderByDescending", new Type[] { type, property.PropertyType }, source.Expression, Expression.Quote(orderByExp));
Aaron Powell
fuente
Je no hay problema, no puedo asignar la respuesta a mí mismo de todos modos :)
JTew
1
para orden descendente, pase "OrderByDescending" en lugar de "OrderBy" MethodCallExpression resultExp = Expression.Call (typeof (Queryable), "OrderByDescending", ...
Garry English
3
Esto funcionó bien, pero el siguiente fue solo un buen ejemplo de código limpio: stackoverflow.com/questions/41244/dynamic-linq-orderby
BenSwayne
@Aaron Powell ¿Cómo puedo implementar un segundo pedido por cláusula ... decir orderby id y luego por fecha
SRJ
3
¿Para qué es el parámetro values?
Frank Fajardo
31

También puede utilizar Dynamic Linq

Información aquí http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx

C # descargar aquí http://msdn.microsoft.com/en-us/vcsharp/bb894665.aspx

Luego, simplemente agregue el uso de Linq.Dynamic; y automáticamente obtienes 2 métodos de extensión adicionales que se pueden usar así

return query.OrderBy("StringColumnName");
Jeremy Coenen
fuente
Gracias, había visto Linq.Dynamic en una muestra en el sitio de Phil Haack, pero no estaba seguro al respecto. Jugaré con esto durante el fin de semana.
JTew
Como alternativa, Systems.Linq.Dynamic.dll se puede descargar desde aquí: github.com/kahanu/System.Linq.Dynamic
Baig
12

He ampliado sus funciones para agregar compatibilidad con propiedades secundarias.

private static LambdaExpression GenerateSelector<TEntity>(String propertyName, out Type resultType) where TEntity : class
{
    // Create a parameter to pass into the Lambda expression (Entity => Entity.OrderByField).
    var parameter = Expression.Parameter(typeof(TEntity), "Entity");
    //  create the selector part, but support child properties
    PropertyInfo property;
    Expression propertyAccess;
    if (propertyName.Contains('.'))
    {
            // support to be sorted on child fields.
            String[] childProperties = propertyName.Split('.');
            property = typeof(TEntity).GetProperty(childProperties[0]);
            propertyAccess = Expression.MakeMemberAccess(parameter, property);
            for (int i = 1; i < childProperties.Length; i++)
            {
                    property = property.PropertyType.GetProperty(childProperties[i]);
                    propertyAccess = Expression.MakeMemberAccess(propertyAccess, property);
            }
    }
    else
    {
            property = typeof(TEntity).GetProperty(propertyName);
            propertyAccess = Expression.MakeMemberAccess(parameter, property);
    }
    resultType = property.PropertyType;                     
    // Create the order by expression.
    return Expression.Lambda(propertyAccess, parameter);
}

private static MethodCallExpression GenerateMethodCall<TEntity>(IQueryable<TEntity> source, string methodName, String fieldName) where TEntity : class
{
    Type type = typeof(TEntity);
    Type selectorResultType;
    LambdaExpression selector = GenerateSelector<TEntity>(fieldName, out selectorResultType);
    MethodCallExpression resultExp = Expression.Call(typeof(Queryable), methodName,
                                    new Type[] { type, selectorResultType },
                                    source.Expression, Expression.Quote(selector));
    return resultExp;
}

Puede utilizar estas funciones como:

GenerateMethodCall<TEntity>(source, "OrderByDescending", fieldName);
Davy Landman
fuente
1
Eres mi héroe !!
Sebastián Guerrero
1
Tengo que amar a las personas inteligentes
Rod Johnson
Probé este código y funciona con un niño, pero no con más de uno, por ejemplo, funciona con la clasificación en x.String y x.Object.String, pero no con la clasificación en x.Object.Object.String.
Robbert Raats
8

Usé tu idea como método de extensión para OrderBy. Pero en el caso de "muchos a muchos", aparece un error. Por ejemplo, tiene la tabla Sitio, Cliente y Sitio_cliente. Para un sitio dado, quiero ordenar por nombre de cliente y en la extensión OrderBy (cuando paso "site.customer" donde el cliente es propiedad de navegación) aparece un error en la línea: propertyAccess = Expression.MakeMemberAccess (propertyAccess, property);

Esto es lo que uso (con algunas mejoras :-)):

public static IQueryable<TEntity> OrderBy<TEntity>(this IQueryable<TEntity> source, string orderByValues) where TEntity : class
{
  IQueryable<TEntity> returnValue = null;

  string orderPair = orderByValues.Trim().Split(',')[0];
  string command = orderPair.ToUpper().Contains("DESC") ? "OrderByDescending" : "OrderBy";

  var type = typeof(TEntity);
  var parameter = Expression.Parameter(type, "p");

  string propertyName = (orderPair.Split(' ')[0]).Trim();

  System.Reflection.PropertyInfo property;
  MemberExpression propertyAccess;

  if (propertyName.Contains('.'))
  {
    // support to be sorted on child fields. 
    String[] childProperties = propertyName.Split('.');
    property = typeof(TEntity).GetProperty(childProperties[0]);
    propertyAccess = Expression.MakeMemberAccess(parameter, property);

    for (int i = 1; i < childProperties.Length; i++)
    {
      Type t = property.PropertyType;
      if (!t.IsGenericType)
      {
        property = t.GetProperty(childProperties[i]);
      }
      else
      {
        property = t.GetGenericArguments().First().GetProperty(childProperties[i]);
      }

      propertyAccess = Expression.MakeMemberAccess(propertyAccess, property);
    }
  }
  else
  {
    property = type.GetProperty(propertyName);
    propertyAccess = Expression.MakeMemberAccess(parameter, property);
  }

  var orderByExpression = Expression.Lambda(propertyAccess, parameter);

  var resultExpression = Expression.Call(typeof(Queryable), command, new Type[] { type, property.PropertyType },

  source.Expression, Expression.Quote(orderByExpression));

  returnValue = source.Provider.CreateQuery<TEntity>(resultExpression);

  if (orderByValues.Trim().Split(',').Count() > 1)
  {
    // remove first item
    string newSearchForWords = orderByValues.ToString().Remove(0, orderByValues.ToString().IndexOf(',') + 1);
    return source.OrderBy(newSearchForWords);
  }

  return returnValue;
}

Saludos

Slobodan

Slobodan
fuente
6

Parece que esta es la forma de hacerlo, ahora para verificar que:

// ***** OrderBy(company => company) *****
// Create an expression tree that represents the expression
// 'whereCallExpression.OrderBy(company => company)'
MethodCallExpression orderByCallExpression = Expression.Call(
    typeof(Queryable),
    "OrderBy",
    new Type[] { queryableData.ElementType, queryableData.ElementType },
    whereCallExpression,
    Expression.Lambda<Func<string, string>>(pe, new ParameterExpression[] { pe }));
// ***** End OrderBy *****
JTew
fuente
1
maldita sea, 34 segundos por detrás! : P
Aaron Powell
3

Si puede agregar el paquete "System.Linq.Dynamic", entonces, demasiado fácil sin ninguna complicación,

fisrt insatll package "System.Linq.Dynamic" del administrador de paquetes NuGet y luego intente lo siguiente según sus necesidades,

Ex:

public IQueryable<TEntity> GetWithInclude(Expression<Func<TEntity, bool>> predicate,
                    List<string> sortBy, int pageNo, int pageSize = 12, params string[] include)
        {
            try
            {
                var numberOfRecordsToSkip = pageNo * pageSize;
                var dynamic = DbSet.AsQueryable();

                foreach (var s in include)
                {
                    dynamic.Include(s);
                }
                 return dynamic.OrderBy("CreatedDate").Skip(numberOfRecordsToSkip).Take(pageSize);


            }
            catch (Exception e)
            {
                throw new Exception(e.Message);
            }
        }

Espero que esto ayude

dush88c
fuente
2

Arreglé este código un poco: https://stackoverflow.com/a/1670085/5852630

Este código funciona con ordenación secuencial: primero ejecute "OrderBy", luego "ThenBy" (¡no "OrderBy"!)

public static IQueryable<TEntity> OrderBy<TEntity>(this IQueryable<TEntity> source, string orderByValues) where TEntity : class
{
    IQueryable<TEntity> returnValue = null;

    string[] orderPairs = orderByValues.Trim().Split(',');

    Expression resultExpression = source.Expression;

    string strAsc = "OrderBy";
    string strDesc = "OrderByDescending";

    foreach (string orderPair in orderPairs)
    {
        if (string.IsNullOrWhiteSpace(orderPair))
            continue;

        string[] orderPairArr = orderPair.Trim().Split(' ');

        string propertyName = orderPairArr[0].Trim();
        string orderNarrow = orderPairArr.Length > 1 ? orderPairArr[1].Trim() : string.Empty;

        string command = orderNarrow.ToUpper().Contains("DESC") ? strDesc : strAsc;

        Type type = typeof(TEntity);
        ParameterExpression parameter = Expression.Parameter(type, "p");

        System.Reflection.PropertyInfo property;
        Expression propertyAccess;

        if (propertyName.Contains('.'))
        {
            // support to be sorted on child fields. 
            String[] childProperties = propertyName.Split('.');
            property = typeof(TEntity).GetProperty(childProperties[0]);
            propertyAccess = Expression.MakeMemberAccess(parameter, property);

            for (int i = 1; i < childProperties.Length; i++)
            {
                Type t = property.PropertyType;
                if (!t.IsGenericType)
                {
                    property = t.GetProperty(childProperties[i]);
                }
                else
                {
                    property = t.GetGenericArguments().First().GetProperty(childProperties[i]);
                }

                propertyAccess = Expression.MakeMemberAccess(propertyAccess, property);
            }
        }
        else
        {
            property = type.GetProperty(propertyName);
            propertyAccess = Expression.MakeMemberAccess(parameter, property);
        }

        if (property.PropertyType == typeof(object))
        {
            propertyAccess = Expression.Call(propertyAccess, "ToString", null);
        }

        LambdaExpression orderByExpression = Expression.Lambda(propertyAccess, parameter);

        resultExpression = Expression.Call(typeof(Queryable), command, new Type[] { type, property.PropertyType == typeof(object) ? typeof(string) : property.PropertyType },
            resultExpression, Expression.Quote(orderByExpression));

        strAsc = "ThenBy";
        strDesc = "ThenByDescending";
    }

    returnValue = source.Provider.CreateQuery<TEntity>(resultExpression);

    return returnValue;
}
SeroJah
fuente
0

Aquí está mi adaptación de la respuesta de @Davy Landman (quería un método de extensión) y lo simplifiqué un poco.

public static IQueryable<T> SortBy<T>(this IQueryable<T> source, 
                                      String propertyName, 
                                      WebControls.SortDirection direction)
    {
        if (source == null) throw new ArgumentNullException("source");
        if (String.IsNullOrEmpty(propertyName)) return source;

        // Create a parameter to pass into the Lambda expression
        //(Entity => Entity.OrderByField).
        var parameter = Expression.Parameter(typeof(T), "Entity");

        //  create the selector part, but support child properties (it works without . too)
        String[] childProperties = propertyName.Split('.');
        MemberExpression property = Expression.Property(parameter, childProperties[0]);
        for (int i = 1; i < childProperties.Length; i++)
        {
            property = Expression.Property(property, childProperties[i]);
        }

        LambdaExpression selector = Expression.Lambda(property, parameter);

        string methodName = (direction > 0) ? "OrderByDescending" : "OrderBy";

        MethodCallExpression resultExp = Expression.Call(typeof(Queryable), methodName,
                                        new Type[] { source.ElementType, property.Type },
                                        source.Expression, Expression.Quote(selector));

        return source.Provider.CreateQuery<T>(resultExp);
    }

Se puede usar así:

gridview1.DataSource = DbContext.TB_CARS.SortBy("model", SortDirection.Descending);
//OR
gridview1.DataSource = DbContext.TB_CARS.SortBy("owner.first_name", 0);
M Granja
fuente