Caffeine-Powered Life

Server-Side Sorting With Dynamic LINQ

Before I even start, this article applies to any LINQ provider. This includes LINQ-to-SQL, Entity Framework, and NHibernate.Linq. It even works with LINQ-to-objects.

In one of my applications, I have a paged list. I’ve been using jQuery’s tablesorter plugin for the last 2 years. It’s been great, right up until the users figured out the difference between a client-side and server-side sort. This doesn’t really matter unless you’re paging your data. Without paging, this is the same problem, so any solution will work. In a client-side sort, only the data that’s visible to the user is being sorted. With a server-side sort, everything is sorted before the paging operation takes place. It’s a totally legitimate user request, so let’s find a solution.

I’ve already got my UI set up to send requests like the following.

  • /products?sort=Name — sort by product name
  • /products?sort=LastOrderDate — sort by last order date

Simple, right? But I also want to send sort queries like this…

  • /products?sort=Manufacturer.Name — sort by product manufacturer’s name
  • /products?sort=Manufacturer.BillingAddress.State — sort by product manufacturer’s billing address

OMG! Dots!

First of all, let’s look at resources that already exist. If you’re familiar with the MVC Contrib project, you’d be happy to know that there’s a solution that meets about 90% of what we require in a single class. It looks like this.


public static IQueryable<T> OrderBy<T>(this IQueryable<T> datasource, string propertyName, SortDirection direction)

{

    //http://msdn.microsoft.com/en-us/library/bb882637.aspx

    if(string.IsNullOrEmpty(propertyName))

    {

        return datasource;

    }



    var type = typeof(T);

    var property = type.GetProperty(propertyName);



    if(property == null)

    {

        throw new InvalidOperationException(string.Format("Could not find a property called '{0}' on type {1}", propertyName, type));

    }



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

    var propertyAccess = Expression.MakeMemberAccess(parameter, property);

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



    const string orderBy = "OrderBy";

    const string orderByDesc = "OrderByDescending";



    string methodToInvoke = direction == SortDirection.Ascending ? orderBy : orderByDesc;



    var orderByCall = Expression.Call(typeof(Queryable), methodToInvoke, new[] { type, property.PropertyType }, datasource.Expression, Expression.Quote(orderByExp));



    return datasource.Provider.CreateQuery(orderByCall);

}

The only problem with this solution is that it only goes one object layer deep (i.e. you cannot sort by nested properties). If you don’t need to sort on nested properties, then you can stop reading now, I suppose. But I need those nested properties.

The solution from MVC Contrib is close. We only need to aggregate the properties and we’re there. Take note of the LINQ Aggregate() method.


public static IQueryable<T> OrderBy<T>(this IQueryable<T> collection, string propertyPath, SortDirection sortDirection)

{

  if (string.IsNullOrEmpty(propertyPath))

  {

    return collection;

  }

  

  Type collectionType = typeof(T);

  

  ParameterExpression parameterExpression = Expression.Parameter(collectionType, "p");

  Expression seedExpression = parameterExpression;            

  Expression aggregateExpression = propertyPath.Split('.').Aggregate(seedExpression, Expression.Property);

  MemberExpression memberExpression = aggregateExpression as MemberExpression;

  

  if (memberExpression == null)

  {

    throw new NullReferenceException(string.Format("Unable to cast Member Expression for given path: {0}.", propertyPath));

  }

  

  LambdaExpression orderByExp = Expression.Lambda(memberExpression, parameterExpression);

  

  const string orderBy = "OrderBy";

  const string orderByDesc = "OrderByDescending";

  Type childPropertyType = ((PropertyInfo)(memberExpression.Member)).PropertyType;

  

  string methodToInvoke = sortDirection == SortDirection.Ascending ? orderBy : orderByDesc;

  var orderByCall = Expression.Call(typeof(Queryable), methodToInvoke, new[] { collectionType, childPropertyType }, collection.Expression, Expression.Quote(orderByExp));

  return collection.Provider.CreateQuery(orderByCall);

}

That’s all it takes!

Comments