How can I specify a predicate's type, which I

2019-06-06 15:02发布

问题:

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 TSortKey, because I won't know which property will be used until runtime, it could be x => x.Name or 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 object 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?

回答1:

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);
    }
}