How do I apply OrderBy on an IQueryable using a string column name within a generic extension method?

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

Because the type for OrderBy is not inferred from sortExpression I need to specify it something like this at run time:

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

Or

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

I don't think this is possible however as TSortColumn can only be determined during runtime.

Is there a way around this?

95225 次浏览

It seems that this is the way to do it, now to verify that:

// ***** 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 *****

We did something similar (not 100% the same, but similar) in a LINQ to SQL project. Here's the code:

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

We didn't actually use a generic, we had a known class, but it should work on a generic (I've put the generic placeholder where it should be).

Edit: For descending order, pass in OrderByDescending instead of "OrderBy":

MethodCallExpression resultExp = Expression.Call(typeof(Queryable), "OrderByDescending", new Type[] { type, property.PropertyType }, source.Expression, Expression.Quote(orderByExp));

I've extended your functions to add support for Child Properties.

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

You can use these functions like:

GenerateMethodCall<TEntity>(source, "OrderByDescending", fieldName);

You can also use Dynamic Linq

Info here http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx

C# download here http://msdn.microsoft.com/en-us/vcsharp/bb894665.aspx

Then just add the using Linq.Dynamic; and you automatically get 2 additional extension methods that can be used like this

return query.OrderBy("StringColumnName");

I used your idea for extension method for OrderBy. But in case of "many to many" I am getting error. For example you have table Site, Customer and Customer_site. For given Site I want to sort by customer name and in OrderBy extension (when I pass "site.customer" where customer is navigation property) I get error in line: propertyAccess = Expression.MakeMemberAccess(propertyAccess, property);

This is what I use (with some enhancements :-) ):

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

Regards

Slobodan

If you are able to add "System.Linq.Dynamic" package then, Too easy without any complication,

fisrt insatll package "System.Linq.Dynamic" from NuGet package manager then try as below as your need,

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

Hope this will help

I fixed this code a bit: https://stackoverflow.com/a/1670085/5852630

This code works with sequential sorting: first execute "OrderBy", then "ThenBy"(Not "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;
}

Here is my adaptation from @Davy Landman's answer (I wanted an extension method) and I simplified a bit.

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

It can be used like this:

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