So, it's been a while, but I thought I take moment and do my annual blog post ;).
I've been playing around with ASP.NET MVC and the Linq stuff for NHibernate recently. I was in need of an OrderBy extension method that could take a SQL-Like OrderBy string and sort a IQueryable<> or IEnumerable<> collection. I wrote up an implementation that worked, but I just wasn't satisfied with its internals (quite a bit of reflection to get the correct type to construct a LambdaExpression, etc)
At any rate, I couldn't leave well enough alone, and, after a bit of Googling, I ran across this StackOverflow answer about Dynamic LINQ OrderBy. The extension method wasn't exactly what I was looking for, but that ApplyOrder method is slick, and solved the portion of my implementation that was bothering me.
So, I though I would post up my version in case anybody finds it useful. It handles the following inputs:
list.OrderBy("SomeProperty"); list.OrderBy("SomeProperty DESC"); list.OrderBy("SomeProperty DESC, SomeOtherProperty"); list.OrderBy("SomeSubObject.SomeProperty ASC, SomeOtherProperty DESC");Dynamic SQL-like Linq OrderBy Extension
{
public static IEnumerable<T> OrderBy<T>(this IEnumerable<T> enumerable, string orderBy)
{
return enumerable.AsQueryable().OrderBy(orderBy).AsEnumerable();
}
public static IQueryable<T> OrderBy<T>(this IQueryable<T> collection, string orderBy)
{
foreach(OrderByInfo orderByInfo in ParseOrderBy(orderBy))
collection = ApplyOrderBy<T>(collection, orderByInfo);
return collection;
}
private static IQueryable<T> ApplyOrderBy<T>(IQueryable<T> collection, OrderByInfo orderByInfo)
{
string[] props = orderByInfo.PropertyName.Split('.');
Type type = typeof(T);
ParameterExpression arg = Expression.Parameter(type, "x");
Expression expr = arg;
foreach (string prop in props)
{
// use reflection (not ComponentModel) to mirror LINQ
PropertyInfo pi = type.GetProperty(prop);
expr = Expression.Property(expr, pi);
type = pi.PropertyType;
}
Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);
LambdaExpression lambda = Expression.Lambda(delegateType, expr, arg);
string methodName = String.Empty;
if (!orderByInfo.Initial && collection is IOrderedQueryable<T>)
{
if (orderByInfo.Direction == SortDirection.Ascending)
methodName = "ThenBy";
else
methodName = "ThenByDescending";
}
else
{
if (orderByInfo.Direction == SortDirection.Ascending)
methodName = "OrderBy";
else
methodName = "OrderByDescending";
}
//TODO: apply caching to the generic methodsinfos?
return (IOrderedQueryable<T>)typeof(Queryable).GetMethods().Single(
method => method.Name == methodName
&& method.IsGenericMethodDefinition
&& method.GetGenericArguments().Length == 2
&& method.GetParameters().Length == 2)
.MakeGenericMethod(typeof(T), type)
.Invoke(null, new object[] { collection, lambda });
}
private static IEnumerable<OrderByInfo> ParseOrderBy(string orderBy)
{
if (String.IsNullOrEmpty(orderBy))
yield break;
string[] items = orderBy.Split(',');
bool initial = true;
foreach(string item in items)
{
string[] pair = item.Trim().Split(' ');
if (pair.Length > 2)
throw new ArgumentException(String.Format("Invalid OrderBy string '{0}'. Order By Format: Property, Property2 ASC, Property2 DESC",item));
string prop = pair[0].Trim();
if(String.IsNullOrEmpty(prop))
throw new ArgumentException("Invalid Property. Order By Format: Property, Property2 ASC, Property2 DESC");
SortDirection dir = SortDirection.Ascending;
if (pair.Length == 2)
dir = ("desc".Equals(pair[1].Trim(), StringComparison.OrdinalIgnoreCase) ? SortDirection.Descending : SortDirection.Ascending);
yield return new OrderByInfo() { PropertyName = prop, Direction = dir, Initial = initial };
initial = false;
}
}
private class OrderByInfo
{
public string PropertyName { get; set; }
public SortDirection Direction { get; set; }
public bool Initial { get; set; }
}
private enum SortDirection
{
Ascending = 0,
Descending = 1
}
}
23 comments:
Thanks.
I mean it. Thanks a lot this has saved me HOURS of work at a time when deadlines or tight. If you're ever in North Lincs, give me a shout and I'll buy you a beer!
Great coding, thanks a lot!!!
Great stuff. Caching the PropertyInfos in a a hashtable would make this thing blaze.
This is just awesome. Thank you so much!
Thank you for this brilliant piece of code!
Excellent piece of code, thank you.
Very nice! You just saved my day!!! :) :)
Thanks a lot!!!
Thank you so much. Saved me so much time. Brilliant!
Thanks so much for this. Saved me hours of work. Cheers!
Thanks a lot this has saved me HOURS
of work , Great coding .
Tks a Lot!!
Its a beautiful code!
great code!!
Adam, really this is amazing, thanks a lot for this great code!!!
Very Thanks,
great work!! what about random? like Guid.NewGuid()?
That's great code! Thank you. I would request one small change if you have a few minutes. It would be nice if null or an empty string were passed in for it to have a "Default Sort" (perhaps the first property on the object) and sort on that. The reason would be for following it up with .Skip(). Currently, if you pass in a null or empty, it just returns which would cause a .Skip() to fail. Adding a default sort would avoid that.
It actually turned out to be pretty easy. Just put the following code as the first code in the OrderBy(this IQueryable collection, string orderBy) method:
if (orderBy == null || orderBy == string.Empty)
{
Type type = typeof(T);
orderBy = type.GetProperties()[0].Name;
}
Great job, thank You!
This solution don't works with collections of extended class. Returns me a error of conflict of types. Anybody knows how to resolve?
Simple and Slick. Thanks!
I used this OrderByHelper to gracefully allow jtSorting as input for my controller.
http://www.codeproject.com/Articles/277576/AJAX-based-CRUD-tables-using-ASP-NET-MVC-3-and-jTa
You are awesome men, Good stuff...
Post a Comment