malonowa
malonowa

Reputation: 471

Specification pattern using Entity Framework (order by property)

I want to write some API to sort entities on server side (SQLServer) using LINQ2Entities.

I have class which contains expression represents sorting field of entity and sorting direction :

    public class SortOption<TEntity>
    {
       public SortOption(Expression<Func<TEntity, dynamic>> keySelector, 
            bool ascending = true)
        {
            KeySelector = keySelector;
            Ascending = ascending;
        }

       public Expression<Func<TEntity, dynamic>> KeySelector { get; private set; }
       public bool Ascending { get; private set; }
    }

For each of my entities I have class which inherits from above. For example:

    public class PostSorting: SortOption<PostEntity>
    {
        public PostSorting(): base(p => p.Published)
        {
        }
    }

    public class PostEntity
    {
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public Guid Id { get; set; }
        public DateTime? Published { get; set; }
        public DateTime Modified { get; set; }
        public string Title { get; set; }
    }

The main goal is to use properties of the SortOption class in my repository's method, which returns entities:

  public class Repository<TEntity>
     {
        public IEnumerable<TEntity> List(SortOption<TEntity> sortOptions)
         {
            IQueryable<TEntity> query;

            if (sortOptions.Ascending)
              query = dbSet.OrderBy(sortOptions.KeySelector);
            else
              query = dbSet.OrderByDescending(sortOptions.KeySelector);

            return query;
         }
     }

*"dbSet" field is System.Data.Entity.DbSet<TEntity>

If I try to sort entities by any property which have type that differs from string type using PostSorting class I get an error like this:

"LINQ to Entities only supports casting EDM primitive or enumeration types.".

For example(sorting by Published field):

 "Unable to cast the type 'System.Nullable`1[[System.DateTime, mscorlib, 
    Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]' to type
     'System.Object'. LINQ to Entities only supports casting EDM primitive or enumeration types."

or (if I want to order by Modified field)

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

of (if I want to order by Id field)

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

I worked on this task for several days but I can not find the answer to solve the problem.

Upvotes: 1

Views: 1963

Answers (1)

xanatos
xanatos

Reputation: 111870

Try using this:

public static class QueryableEx
{
    public static IOrderedQueryable<TSource> OrderByEx<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, object>> keySelector)
    {
        if (source == null)
        {
            throw new ArgumentNullException("source");
        }
        if (keySelector == null)
        {
            throw new ArgumentNullException("keySelector");
        }

        // While the return type of keySelector is object, the "type" of 
        // keySelector.Body is the "real" type *or* it is a
        // Convert(body). We rebuild a new Expression with this "correct" 
        // Body (removing the Convert if present). The return type is
        // automatically chosen from the type of the keySelector.Body .
        Expression body = keySelector.Body;

        if (body.NodeType == ExpressionType.Convert)
        {
            body = ((UnaryExpression)body).Operand;
        }

        LambdaExpression keySelector2 = Expression.Lambda(body, keySelector.Parameters);
        Type tkey = keySelector2.ReturnType;

        MethodInfo orderbyMethod = (from x in typeof(Queryable).GetMethods()
                                    where x.Name == "OrderBy"
                                    let parameters = x.GetParameters()
                                    where parameters.Length == 2
                                    let generics = x.GetGenericArguments()
                                    where generics.Length == 2
                                    where parameters[0].ParameterType == typeof(IQueryable<>).MakeGenericType(generics[0]) && 
                                        parameters[1].ParameterType == typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(generics[0], generics[1]))
                                    select x).Single();

        return (IOrderedQueryable<TSource>)source.Provider.CreateQuery<TSource>(Expression.Call(null, orderbyMethod.MakeGenericMethod(new Type[]
        {
            typeof(TSource),
            tkey
        }), new Expression[]
        {
            source.Expression,
            Expression.Quote(keySelector2)
        }));
    }
}

You'll have to write a OrderByAscending, but it is the same just with replacing of OrderBy with OrderByAscending. The method rewrites the Expression to use the "right" type.

The code is heavily inspired from the Queryable.OrderBy.

Upvotes: 5

Related Questions