5

I want to be able to apply combo firstby / thenby sorting as follows:

allOrders().sort(s => s.ProductName, s => s.OrderDate)

So using this post as inspiration, I wrote this extension method, which compiles fine:

public static IQueryable<T> sort<T>(this IQueryable<T> entities, params 
  Expression<Func<T, object>>[] predicates) where T : class 
{
  var sorted = entities.OrderBy(predicates[0]);
  for (int i = 1; i < predicates.Length; i++)
    sorted = sorted.ThenBy(predicates[i]);

  return sorted;
}

And I also tried this succint version, which also compiles:

public static IQueryable<T> sort<T>(this IQueryable<T> entities, params 
  Expression<Func<T, object>>[] predicates) where T : class
{
  return predicates.Skip(1).Aggregate(
    entities.OrderBy(predicates[0]),
    (aggregate, currentPredicate) => aggregate.ThenBy(currentPredicate));
}

However if I try sort by a DateTime, I get this exception:

Unable to cast the type 'System.DateTime' to type 'System.Object'. LINQ to Entities only supports casting EDM primitive or enumeration types.

What am I doing wrong? I am using EF5.

Community
  • 1
  • 1
Bobby B
  • 2,287
  • 2
  • 24
  • 47

2 Answers2

6

When you return a value type (such as int or DateTime) from a lambda expression that returns object, the compiler generates a Box() call to turn the value type into a boxed object.

The Entity Framework expression parser can't handle this box expression.
The only solution is to pass a strongly-typed lambda expression that returns the value type.

To do that, you can mis-use collection initializers:

public class OrderingCollection<TEntity> : IEnumerable {
    public void Add<TProperty>(Expression<Func<TEntity, TProperty>>) {
        ...
    }
}

public static IQueryable<T> Sort<T>(this IQueryable<T> entities, 
                                    OrderingCollection<T> o) where T : class {
    ...
}


q = q.Sort(new OrderingCollection { s => s.ProductName, s => s.OrderDate });

Collection initializers allow you to use type inference with an arbitrary number of different type parameters.

Leniel Maccaferri
  • 100,159
  • 46
  • 371
  • 480
SLaks
  • 868,454
  • 176
  • 1,908
  • 1,964
  • 1
    this has less "noise" than the tuple idea, but it's a pity the calling code couldn't be simpler. Nice solution though, thanks. – Bobby B Oct 24 '12 at 21:02
0

I think your problem is with Func<T, object>. That means that you're going to return an object no matter what you're sorting on, and EF can't map that to a database column type.

The OrderBy() function takes both a TSource and a TKey, so you'd need to supply the key type for each sort, which quickly gets out of control (see Tuple<T1,T2,T3,T4,T5,T6,T7,T8>).

Bobson
  • 13,498
  • 5
  • 55
  • 80
  • So the tuple's values would be the individual expressions? – Bobby B Oct 24 '12 at 20:46
  • Well, if you want to go with a Tuple directly, yes. `.Sort(Tuple.Create(s => s.ProductName, s => s.OrderDate))` would pass a typed `Tuple` into your sort function. But I don't know if there's a way to make that work with an arbitrary number of expressions. I was using it more by way of example - you'd need `Sort()`, `Sort()`, Sort()`, and so on. – Bobson Oct 24 '12 at 20:51