September 3rd 2009

Dynamic Sorting and Filtering with Linq To SQL

A common paradigm in my current work involves separating sort logic from the data access layer that applies it. The end user may sort a table/view by any number of criteria and in fairly complex ways.

One simple way to achieve this is through a switch statement:

private IOrderedQueryable<Customer> SortData(String sortExpression, IQueryable<Customer> query)
  {
   sortExpression = sortExpression ?? "CompanyName";
   IOrderedQueryable<Customer> orderedQuery = null;
   switch (sortExpression)
   {
    case "CompanyName":
     orderedQuery = query.OrderBy(x => x.CompanyName);
     break;
    case "Count":
     orderedQuery = query.OrderBy(x => x.Orders.Count());
     break;
    default:
     orderedQuery = query.OrderBy(x => x);
     break;
   }
   return orderedQuery;
  }

This quickly breaks down because we would need to include a case for every possible sort expression. There’s a fairly common approach at http://weblogs.asp.net/davidfowler/archive/2008/12/11/dynamic-sorting-with-linq.aspx to dynamically sort on a table’s columns. This works well but the approach does not lend itself to more complicated expressions e.g. sort customers by their number of orders.

We sought to create a framework for developers to filter and sort listings by any combination of criteria. As long as Linq to Sql supported the lamda expression, the developer should be free to populate a custom “filter” object with a list of “where” and “order by” information needed to act upon the table or view.

When filtering a sequence, Linq to Sql has no trouble converting lamda expressions that return booleans into t-sql “where” clauses – the boolean return value matches the necessary return value of the sql expression generated. The filter expression then can simply be:

Expression<Func<T, Boolean>> filterExpression

When sorting a sequence, however, the lamdba expression must return a Type that supports IComparable. A simplistic approach to dynamic sorting would then be to use sort expressions of the form:

Expression<Func<T, IComparable>> sortExpression

and apply them as

sortedQry = qry.OrderBy(sortExpression);

This will work just fine when querying objects, but will fail when used with Linq to Sql. Depending on the return type, the runtime may force a cast to IComparable – something Linq to Sql doesn’t know how to do.

icompareableerror

The error specifically ocurrs when the return value of the expression is a DateTime, Int32, etc. 

To work around this we need to avoid casting the return value of the lamdba expression and instead let Linq to Sql know explicitly what type the expression returns.

templatedlamdbal2sqltooltip-noicompare

instead of this:

icompareablel2sqltooltip-icomparablecast

We could, for instance, create a variable for each return type:

Expression<Func<T, DateTime>> sortExpressionDate

Expression<Func<T, Int32>> sortExpressionInt32

...

Such a framework is too cumbersome to use effectively. Instead, we can avoid the cast by telling .net at compile time the return type of the lamda expression.

Expression<Func<T, TKey>> SortExpression

 for example:

Expression<Func<Customer, Int32>> sortExpression = x => x.Orders.Count();

In my specific case, I used a filter to encapsulate the logic needed to filter and sort a sequence.

public class Filter<T>
 {
  /// <summary>
  /// the list of filters applied to the sequence. The filters will act as a series of "and" clauses against the data source.
  /// </summary>
  public List<Expression<Func<T, Boolean>>> Filters...

   /// <summary>
  /// the list of sorts to apply to the resulting sequence of items.
  /// </summary>
  public List<ISortOrder<T>> SortCriteria...
  }

public interface ISortOrder<T>
 {
  SortDirection Direction { get; set; }
  IOrderedQueryable<T> ApplyOrdering(IQueryable<T> query, Boolean useThenBy);
 }

For example, the following code filters by companyName and a minimum number of orders; then sorts by the column name specified by the “propertyNameToSortBy” variable followed by the count of orders:

var filter = new Filter<Customer>();

filter.Filters.Add(x => x.CompanyName.Contains(searchText));

filter.Filters.Add(x => x.Orders.Count < numberOfOrders);

filter.SortCriteria.Add(new FieldSortOrder<Customer>(propertyNameToSortBy));

var sortBy = new ExpressionSortOrder<Customer, Int32>()
     {
      SortExpression = x => x.Orders.Count,
      Direction = (sortExpression.ToLowerInvariant().EndsWith("desc")) ? DynamicSorter.SortDirection.Descending : DynamicSorter.SortDirection.Ascending
     };
     filter.SortCriteria.Add(sortBy);

return FilterItems(filter);

 the code for the FilterItems method then is relatively simple:

public static IEnumerable<Customer> FilterItems(Filter<Customer> filter)
  {
   var dc = new NorthwindDataContext();

   IQueryable<Customer> sequence = dc.Customers;
   foreach (var filterClause in filter.Filters)
   {
    sequence = sequence.Where(filterClause);
   }
   if (filter.SortCriteria.Count > 0)
   {
    var sortCriteria = filter.SortCriteria[0];
    var orderedSequence = sortCriteria.ApplyOrdering(sequence, false);
    if (filter.SortCriteria.Count > 1)
    {
     for (var i = 1; i < filter.SortCriteria.Count; i++)
     {
      sortCriteria = filter.SortCriteria[i];
      orderedSequence = sortCriteria.ApplyOrdering(sequence, false);
     }
    }
    sequence = orderedSequence;
   }
   return sequence;
  }

In the end, we used T4 templates to codegen entity specific filter objects and static methods to filter tables and views – more about that in my next post.

A sample northwind based sample can be found here.

No Comments yet »