grokky grokky - 10 months ago 43
C# Question

How can I specify a predicate's type, which I won't know until runtime?

My repository method fetches stuff from the database. It accepts the sort order as an argument:

IEnumerable<Car> getCars<TSortKey>(Expression<Func<Car, TSortKey>> sort);

I use
, because I won't know which property will be used until runtime, it could be
x => x.Name
x => x.Make
which are strings, but it could also be
x => x.History.Age
which is an integer.

The user chooses the sort order, then I set up the sort predicate in a switch and call into that method.

Expression<Func<Car, object>> sortPredicate;
switch (sortOption) {
case SortOption.Name: sortPredicate = s => s.Name; break;
case SortOption.Make: sortPredicate = s => s.Make; break;
case SortOption.Age: sortPredicate = s => s.History.Age; break;
default: sortPredicate = s => s.Name; break;
var cars = repo.getCars(sortPredicate);

I use
in the predicate, as I won't know the type until runtime. But that generates the wrong SQL, and throws.

So how can I fix this?

Answer Source

The problem is that Expression<Func<T, object>> generates additional Convert for value type properties, which EF does not like and throws NotSupportedException.

Instead of OrderBy, you can use the following helper method inside your repository class. What it does is stripping the Convert expression if needed and calling the Queryable.OrderBy method dynamically:

public static partial class EFExtensions
    public static IOrderedQueryable<T> SortBy<T>(this IQueryable<T> source, Expression<Func<T, object>> keySelector)
        var body = keySelector.Body;
        if (body.NodeType == ExpressionType.Convert)
            body = ((UnaryExpression)keySelector.Body).Operand;
        var selector = Expression.Lambda(body, keySelector.Parameters);
        var orderByCall = Expression.Call(
            typeof(Queryable), "OrderBy", new[] { typeof(T), body.Type },
            source.Expression, Expression.Quote(selector));
        return (IOrderedQueryable<T>)source.Provider.CreateQuery(orderByCall);