从字符串构建Linq排序表达式导致“类型为'system.int32'的表达式不能用作返回类型'System.Object'”的错误。

3
我是一名有用的助手,可以翻译文本。

我有一个系统,应该根据从另一个应用程序提供的排序数据列表创建一个排序表达式。排序信息以包含排序键(Object.thingtosort)和方向的对象形式给出。

我的当前解决方案是基于我从另一个堆栈溢出问题中获得的解决方案构建的,该问题在此处找到:原始问题。每当我运行以下代码时:

    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;
    }

我遇到了错误:

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

我尝试通过将参数转换为对象来解决这个问题,使用的方法是

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

导致以下函数:
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;
    }

这会产生另一个问题,它无法比较数组中的两个元素(可能是因为它不知道如何在这种情况下比较对象与对象)。
System.InvalidOperationException: 'Failed to compare two elements in the array.'
ArgumentException: At least one object must implement IComparable.

我希望它能适用于任何类型,因为排序应该能够在对象的任何字段上工作(这些字段可以嵌套多层)。下面包含了重新创建我的问题所需的完整代码。
Microsoft Visual Studio Community 2022(64位)- 预览 版本17.3.0预览1.1
该项目是一个使用.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; }
    }

我能建议您修复问题底部的 ToLambda 方法吗?因为它目前缺少转换,我错误地认为您是从那里得到错误而不是 OrderBy - ProgrammingLlama
我可以将转换添加到示例中,并更改问题以专门涉及该问题。我也希望保持问题对可能更改ToLambda函数签名的解决方案开放。您认为解决方案与将主体转换为对象,然后修复IComparable问题相关性更大吗? - Bart Kuijer
1
只是没有代码,你所说的第二个问题有点难以解决。 - ProgrammingLlama
我将ToLambda()方法的编辑版本作为注释添加。这样就可以在相同的代码示例中切换两个错误。 - Bart Kuijer
1个回答

3

看起来这只是一个小错误。

  1. 你正在尝试按 Expression<Func<T, object>> 进行排序(指该值,而不是 randomNumber 的值决定排序顺序)。
  2. .OrderBy 用于枚举(而不是可查询对象),期望一个 Func<TSource, TKey>,其中 TSourceOrderEntity,而 TKey 在此情况下应该是你的 object 值。

所以我们需要做两件事:

  1. 编译表达式。
  2. 将其用于排序。

本质上,我们需要这个:

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

或者

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

或者

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

或者

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

您也可以更改方法,以返回编译的Func<T,object>

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;
}

然后像这样更多地使用它:

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

或者这样:
var sortedOrders = orders.OrderBy(ToLambda<OrderEntity>(propertyForExpression));

注意:我建议在循环外编译表达式并将其缓存为 Func<OrderEntity,object> 变量,因为否则对于单个.OrderBy,它将被评估多次。

1
@bart 注意(正如我在答案底部提到的那样),这将导致它在循环遍历订单时被编译多次(您可以在ToLambda方法中设置断点以查看有多少次)。如果这将处理大量订单或具有关键性能,则建议在OrderBy之外进行编译。也许将其制作为扩展方法,以使其仍然易于阅读。 - ProgrammingLlama
在实际代码中,对象不会直接在循环中使用。我会在应用所有排序逻辑(通常是对IOrderedEnumerable应用3或4个排序轮次,使用ThenBy)后调用.ToList(),然后将已排序的列表传递给处理器。我很感激您在这里提到性能方面,因为该函数将用于大批量对象的处理。 - Bart Kuijer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接