Search code examples
c#entity-frameworksortinglinq-to-entitiesspecification-pattern

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.


Solution

  • 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.