Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Return Expression from a method to be used in OrderBy

I want to write an abstract method to be overridden by child classes, and what the method does is return an expression to be subsequently used in LINQ OrderBy(). Something like this:

Note: Message inherits from Notes class, and MyModel inherits from MsgModel.

public class Notes
{
    // abstract definition; using object so that I can (I hope) order by int, string, etc.
    public abstract Expression<Func<MsgModel, object>> OrderByField();
    // ...

    private string GetOrderByFieldName()
    {
        // I am not sure how to write this
        // This is my problem 2. Please see below for problem 1 :-(

        var lambda = OrderByField() as LambdaExpression;

        MemberExpression member = lambda.Body as MemberExpression;

        PropertyInfo propInfo = member.Member as PropertyInfo;

        return propInfo.Name;
    }
}

public class Message : Notes
{
    // second type parameter is object because I don't know the type
    // of the orderby field beforehand
    public override Expression<Func<MyModel, object>> OrderByField()
    {
        return m => m.item_no;
    }
}

Now if I try to order by this way:

var orderedQuery = myQueryable.OrderBy(OrderByField());

I get this error:

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

I can rightaway say that type parameter object is the cause of the problem. So, when I change the type parameter to int, it works fine as long as I am ordering by an int field (e.g. the field item_no).

Q1. How can I get this to work? Surely, I can use a string property OrderByField instead of that expression-returning method and order by it, probably by writing some extension method for IQueryable (maybe using this great answer). But I want to have more intellisense while setting the order by.

Q2. How can I get the name of the order by column from the expression returned by the method OrderByField(). Obviously what I have tried with doesn't work. The member always gets null.


Edit: I have made some changes to the type parameters of the methods. Sorry for not doing it the first time.

like image 201
Sнаđошƒаӽ Avatar asked Jul 24 '17 09:07

Sнаđошƒаӽ


3 Answers

Apparently the Expression<Func<T, object>> is not equivalent of Expression<Func<T, K>>, hence cannot be used as direct replacement of the later required by Queryable.OrderBy<T, K> and similar methods.

Still it's possible to make it work with the help of Expression class by creating a non generic LambdaExpression via Expression.Lambda method and dynamically emitting a call to the corresponding Queryable method.

Here is all that encapsulated in a custom extension methods (a modified version of my answer to How to use a string to create a EF order by expression?):

public static partial class QueryableExtensions
{
    public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, Expression<Func<T, object>> keySelector)
    {
        return source.OrderBy(keySelector, "OrderBy");
    }
    public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> source, Expression<Func<T, object>> keySelector)
    {
        return source.OrderBy(keySelector, "OrderByDescending");
    }
    public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source, Expression<Func<T, object>> keySelector)
    {
        return source.OrderBy(keySelector, "ThenBy");
    }
    public static IOrderedQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> source, Expression<Func<T, object>> keySelector)
    {
        return source.OrderBy(keySelector, "ThenByDescending");
    }
    private static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, Expression<Func<T, object>> keySelector, string method)
    {
        var parameter = keySelector.Parameters[0];
        var body = keySelector.Body;
        if (body.NodeType == ExpressionType.Convert)
            body = ((UnaryExpression)body).Operand;
        var selector = Expression.Lambda(body, parameter);
        var methodCall = Expression.Call(
            typeof(Queryable), method, new[] { parameter.Type, body.Type },
            source.Expression, Expression.Quote(selector));
        return (IOrderedQueryable<T>)source.Provider.CreateQuery(methodCall);
    }
}

One important detail here is that Expression<Func<T, object>> introduces Expression.Convert for value type returning expressions, so it needs to be stripped from the actual lambda body, which is accomplished with the following part of the code:

var body = keySelector.Body;
if (body.NodeType == ExpressionType.Convert)
    body = ((UnaryExpression)body).Operand;
like image 188
Ivan Stoev Avatar answered Nov 18 '22 06:11

Ivan Stoev


You need a couple of generic types for your Notes class. The first is so you can derive from it and still allow the expression to filter on the derived class. The second is to specify the type of the property you wish to use to order by. For example:

public abstract class Notes<T, TProperty> where T : Notes<T, TProperty>
{
    public abstract Expression<Func<T, TProperty>> OrderByField();

    public string GetOrderByFieldName()
    {
        //snip
    }
}

public class Message : Notes<Message, int>
{
    public int item_no { get; set; }

    public override Expression<Func<Message, int>> OrderByField()
    {
        return m => m.item_no;
    }
}

This should also allow the GetOrderByFieldName method to work.

like image 37
DavidG Avatar answered Nov 18 '22 06:11

DavidG


Here's a solution that does (almost) not use reflection or expression stuff: It exploits the fact that the ordering LINQ functions have just one generic type in their result.

1st, create an interface (only one generic parameter) and an implementation (with two generic parameters):

public interface ISortCrit<TSource>
{
    string SortFieldName { get; }

    IOrderedEnumerable<TSource> MakeOrderBy(IEnumerable<TSource> source);
    IOrderedEnumerable<TSource> MakeOrderByDescending(IEnumerable<TSource> source);
    IOrderedEnumerable<TSource> MakeThenBy(IOrderedEnumerable<TSource> source);
    IOrderedEnumerable<TSource> MakeThenByDescending(IOrderedEnumerable<TSource> source);

    IOrderedQueryable<TSource> MakeOrderBy(IQueryable<TSource> source);
    IOrderedQueryable<TSource> MakeOrderByDescending(IQueryable<TSource> source);
    IOrderedQueryable<TSource> MakeThenBy(IOrderedQueryable<TSource> source);
    IOrderedQueryable<TSource> MakeThenByDescending(IOrderedQueryable<TSource> source);
}

public class SortCrit<TSource, TSort> : ISortCrit<TSource>
{
    private readonly Expression<Func<TSource, TSort>> _sortExpression;
    private readonly Lazy<Func<TSource, TSort>> _sortDelegate;
    private readonly Lazy<string> _sortFieldName;

    public SortCrit(Expression<Func<TSource, TSort>> sortExpression)
    {
        _sortExpression = sortExpression;
        _sortDelegate = new Lazy<Func<TSource, TSort>>(() => sortExpression.Compile());
        _sortFieldName = new Lazy<string>(() => ((MemberExpression)sortExpression.Body).Member.Name);
    }

    public string SortFieldName => _sortFieldName.Value;

    public IOrderedEnumerable<TSource> MakeOrderBy(IEnumerable<TSource> source) => source.OrderBy(_sortDelegate.Value);
    public IOrderedEnumerable<TSource> MakeOrderByDescending(IEnumerable<TSource> source) => source.OrderByDescending(_sortDelegate.Value);
    public IOrderedEnumerable<TSource> MakeThenBy(IOrderedEnumerable<TSource> source) => source.ThenBy(_sortDelegate.Value);
    public IOrderedEnumerable<TSource> MakeThenByDescending(IOrderedEnumerable<TSource> source) => source.ThenBy(_sortDelegate.Value);

    public IOrderedQueryable<TSource> MakeOrderBy(IQueryable<TSource> source) => source.OrderBy(_sortExpression);
    public IOrderedQueryable<TSource> MakeOrderByDescending(IQueryable<TSource> source) => source.OrderByDescending(_sortExpression);
    public IOrderedQueryable<TSource> MakeThenBy(IOrderedQueryable<TSource> source) => source.ThenBy(_sortExpression);
    public IOrderedQueryable<TSource> MakeThenByDescending(IOrderedQueryable<TSource> source) => source.ThenByDescending(_sortExpression);
}

2nd some conveniance extensions:

public static class SortCrit
{
    public static ISortCrit<TSource> Create<TSource, TSort>(Expression<Func<TSource, TSort>> sortExpression) => new SortCrit<TSource, TSort>(sortExpression);

    public static IOrderedEnumerable<TSource> OrderBy<TSource>(this IEnumerable<TSource> source, ISortCrit<TSource> crit) => crit.MakeOrderBy(source);
    public static IOrderedEnumerable<TSource> OrderByDescending<TSource>(this IEnumerable<TSource> source, ISortCrit<TSource> crit) => crit.MakeOrderByDescending(source);
    public static IOrderedEnumerable<TSource> ThenBy<TSource>(this IOrderedEnumerable<TSource> source, ISortCrit<TSource> crit) => crit.MakeThenBy(source);
    public static IOrderedEnumerable<TSource> ThenByDescending<TSource>(this IOrderedEnumerable<TSource> source, ISortCrit<TSource> crit) => crit.MakeThenByDescending(source);

    public static IOrderedQueryable<TSource> OrderBy<TSource>(this IQueryable<TSource> source, ISortCrit<TSource> crit) => crit.MakeOrderBy(source);
    public static IOrderedQueryable<TSource> OrderByDescending<TSource>(this IQueryable<TSource> source, ISortCrit<TSource> crit) => crit.MakeOrderByDescending(source);
    public static IOrderedQueryable<TSource> ThenBy<TSource>(this IOrderedQueryable<TSource> source, ISortCrit<TSource> crit) => crit.MakeThenBy(source);
    public static IOrderedQueryable<TSource> ThenByDescending<TSource>(this IOrderedQueryable<TSource> source, ISortCrit<TSource> crit) => crit.MakeThenByDescending(source);
}

Usage:

var messageCrit = SortCrit.Create((Message m) => m.ItemNo);

IEnumerable<Message> msgs = ...;
msgs.OrderBy(messageCrit);
like image 21
tinudu Avatar answered Nov 18 '22 07:11

tinudu