Home > database >  Building a Linq sort expression from string results in 'Expression of "system.int32"
Building a Linq sort expression from string results in 'Expression of "system.int32"

Time:06-14

I have a system that should create a sort expression based on a list of sort data that is supplied from another application. The sort info is given as an object that contains a sort key (Object.thingtosort) and a direction.

My current solution was built around a solution I got from another stack overflow question, found over here: original question. Whenever I run the following code:

    static Expression<Func<T, object>> ToLambda<T>(string propertyName)
    {
        var propertyNames = propertyName.Split('.');
        var parameter = Expression.Parameter(typeof(T));
        Expression body = parameter;
        foreach (var propName in propertyNames)
            body = Expression.Property(body, propName);
        var func = Expression.Lambda<Func<T, object>>(body, parameter); //<-- line of error
        return func;
    }

I get the error:

System.ArgumentException: 'Expression of type 'System.Int32' cannot be used for return type 'System.Object''

I tried solving this by converting the parameter to an Object by using

    Expression.Convert(body, typeof(object));

Resulting in the following function:

static Expression<Func<T, object>> ToLambda<T>(string propertyName)
    {
        var propertyNames = propertyName.Split('.');
        var parameter = Expression.Parameter(typeof(T));
        Expression body = parameter;
        foreach (var propName in propertyNames)
            body = Expression.Property(body, propName);
        var convertedBody = Expression.Convert(body, typeof(object));
        var func = Expression.Lambda<Func<T, object>>(convertedBody, parameter); //<-- line of error
        return func;
    }

This Creates creates another problem where it fails to compare two elements in the array (probably because it doesn't know how to compare object to object in this case).

System.InvalidOperationException: 'Failed to compare two elements in the array.'
ArgumentException: At least one object must implement IComparable.

I want it to work with any type, as the sorting should work on any field of my object (these can be nested multiple layers deep). Included below is the full code required to recreate my problem.

Microsoft Visual Studio Community 2022 (64-bit) - Preview Version 17.3.0 Preview 1.1

The project is a console application using .net 6.


    using System.Linq.Expressions;

    internal class Program
    {
        private static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");

            string propertyForExpression = "Product.randomNumber";

            Random random = new Random();

            List<OrderEntity> orders = new List<OrderEntity>();

            for (int i = 1; i < 11; i  )
            {
                var orderToAdd = new OrderEntity();
                orderToAdd.id = i;
                orderToAdd.name = "order number "   i;

                var productToAdd = new ProductEntity();
                productToAdd.id = i;
                productToAdd.productName = "product "   i;
                productToAdd.description = "this is a product";
                productToAdd.randomNumber = random.Next(1, 100);

                orderToAdd.Product = productToAdd;
                orders.Add(orderToAdd);
            }

            var sortedOrders = orders.OrderBy(X => ToLambda<OrderEntity> (propertyForExpression));

            foreach(var order in sortedOrders)
            {
                Console.WriteLine(order.Product.randomNumber);
            }
            Console.ReadKey();
        }

        static Expression<Func<T, object>> ToLambda<T>(string propertyName)
        {
            var propertyNames = propertyName.Split('.');
            var parameter = Expression.Parameter(typeof(T));
            Expression body = parameter;
            foreach (var propName in propertyNames)
                body = Expression.Property(body, propName);
            var func = Expression.Lambda<Func<T, object>>(body, parameter);
            return func;
        }


    // ToLambda function that crashes on the OrderBy with error: System.InvalidOperationException: 'Failed to compare two elements in the array.'

    //static Expression<Func<T, object>> ToLambda<T>(string propertyName)
    //{
    //    var propertyNames = propertyName.Split('.');
    //    var parameter = Expression.Parameter(typeof(T));
    //    Expression body = parameter;
    //    foreach (var propName in propertyNames)
    //        body = Expression.Property(body, propName);
    //    var convertedBody = Expression.Convert(body, typeof(object));
    //    var func = Expression.Lambda<Func<T, object>>(convertedBody, parameter);
    //    return func;
    //}
    }


    public class OrderEntity
    {
        public int id { get; set; }
        public string name { get; set; }
        public ProductEntity Product { get; set; }
    }

    public class ProductEntity
    {
        public int id { get; set; }
        public string productName { get; set; }
        public string description { get; set; }
        public int randomNumber { get; set; }
    }

CodePudding user response:

It seems like this is ultimately a minor mistake.

  1. You're trying to order by the Expression<Func<T, object>> (as in the value of that, not the value of randomNumber determines the sort order.
  2. .OrderBy for enumerables (not queryables) expects a Func<TSource, TKey> where TSource is OrderEntity and TKey is supposed to be your object value in this case.

So we need to do two things:

  1. Compile the expression.
  2. Use it for sorting.

Essentially we need this

Func<OrderEntity, object> sortAccessor = ToLambda<OrderEntity>(propertyForExpression).Compile();
var sortedOrders = orders.OrderBy(sortAccessor);

or

Func<OrderEntity, object> sortAccessor = ToLambda<OrderEntity>(propertyForExpression).Compile();
var sortedOrders = orders.OrderBy(x => sortAccessor(x));

or

var sortedOrders = orders.OrderBy(x => ToLambda<OrderEntity>(propertyForExpression).Compile()(x));

or

var sortedOrders = orders.OrderBy(ToLambda<OrderEntity>(propertyForExpression).Compile());

You could also change the method to return the compiled Func<T, object> instead:

static Expression<Func<T, object>> ToLambda<T>(string propertyName)
{
    var propertyNames = propertyName.Split('.');
    var parameter = Expression.Parameter(typeof(T));
    Expression body = parameter;
    foreach (var propName in propertyNames)
        body = Expression.Property(body, propName);

    var convertedResult = Expression.Convert(body, typeof(object));

    var func = Expression.Lambda<Func<T, object>>(convertedResult, parameter);
    return func;
}

and then use it more like this:

Func<OrderEntity, object> sortAccessor = ToLambda<OrderEntity>(propertyForExpression);
var sortedOrders = orders.OrderBy(sortAccessor);

or this:

var sortedOrders = orders.OrderBy(ToLambda<OrderEntity>(propertyForExpression));

Note: the reason I'm suggesting compiling the expression outside of the loop and caching it as a Func<OrderEntity, object> variable is because otherwise it will be evaluated multiple times for a single .OrderBy.

  • Related