1

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.

malonowa
  • 471
  • 5
  • 17

1 Answers1

5

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.

xanatos
  • 109,618
  • 12
  • 197
  • 280
  • @malonowa Are you using the `OrderByEx` method? – xanatos Apr 30 '15 at 17:22
  • Yes, I have changed query like this query = _dbSet. OrderByEx(sortOptions.KeySelector) and made changes in ctor and property "KeySelector" in SortOption class: keySelector is type of Expression> – malonowa Apr 30 '15 at 17:27
  • @malonowa Fixed. Modified code around big comment. From the big comment to the LambdaExpression keySelector2 (included) – xanatos Apr 30 '15 at 18:03
  • Xanatos, I don't understand how, but it works! Thank you a lot! – malonowa Apr 30 '15 at 18:36
  • This worked great for OrderBy and OrderByDescending, but I tried to reuse for ThenBy and ThenByDescending and get "Sequence contains no elements." on the line where `MethodInfor orderbyMethod` is assigned. Any help would be appreciated. I've duplicated the code as a ThenByHelper so I could play with it a bit. – Brandon Lewis May 16 '17 at 22:45
  • @xanatos. To get this working for ThenBy, I commented out the following lines: `where parameters[0].ParameterType == typeof(IQueryable<>).MakeGenericType(generics[0]) && parameters[1].ParameterType == typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(generics[0], generics[1]))`. I tested for both OrderBy methods and ThenBy methods and only 1 was returned. It worked well. Any side effect from this that you see? – Brandon Lewis May 16 '17 at 23:07
  • @LúfarSolutions You should look at the signature of `ThenBy` more carefully. Replace the `parameters[0].ParameterType == typeof(IQueryable<>).MakeGenericType(generics[0]) && ` with `parameters[0].ParameterType == typeof(IOrderedQueryable<>).MakeGenericType(generics[0]) && ` – xanatos May 17 '17 at 05:23
  • @LúfarSolutions The complex query is for future-proofing: you don't have any guarantee that in one of the next versions of .NET Microsoft won't add a second `ThenBy` overload with slightly different parameters. By looking for the exact signature you want, you protect yourself from this. – xanatos May 17 '17 at 07:50
  • @xanatos I tried altering the query as you suggested prior to my comment as I did notice the signature difference but it didn't get it to work. I'll give it another look though. Thanks! – Brandon Lewis May 17 '17 at 07:55