基于字符串构建LINQ排序表达式的类型与比较异常问题
问题描述
我需要搭建一套可根据外部应用传入的排序数据列表生成排序表达式的系统,排序信息以对象形式提供,包含两个参数:
- 排序键:格式为
Object.待排序属性路径 - 排序方向
当前实现参考公开的LINQ动态排序方案编写,但运行代码时在Lambda构建行抛出异常,初始版本的ToLambda方法代码如下:
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; }
抛出的异常信息:
System.ArgumentException: 'Expression of type 'System.Int32' cannot be used for return type 'System.Object''
第一次修复尝试及问题
尝试通过Expression.Convert(body, typeof(object))将属性表达式转换为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); //<-- 报错行 return func; }
该修改引发新问题:执行排序时无法比较数组中的两个元素,推测是值类型装箱为Object类型后无法完成默认比较,异常信息如下:
System.InvalidOperationException: 'Failed to compare two elements in the array.' ArgumentException: At least one object must implement IComparable.
需求
方案需要兼容任意数据类型,支持对象多层嵌套属性的排序。
以下是可完整复现问题的代码,运行环境为Microsoft Visual Studio Community 2022 (64-bit) - Preview Version 17.3.0 Preview 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; } // 执行OrderBy时抛出System.InvalidOperationException: 'Failed to compare two elements in the array.'异常的ToLambda版本 //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; } }
解决方案
问题核心有三点:
- 直接将值类型属性表达式作为
Func<T, object>的返回值时,缺少值类型到引用类型的装箱转换,导致Lambda构建失败 - 手动加
Convert转object后,LINQ的OrderBy方法无法识别属性原始类型,只能按object比较,导致比较失败 - 原有
OrderBy调用写法本身有误:传入的是返回Expression<Func<T,object>>的委托,入参X没有被使用,实际是按表达式对象本身排序,根本没有执行构建的属性访问逻辑
正确实现不需要强制将属性转为object,而是动态构造对应属性类型的Lambda表达式,通过反射调用OrderBy/OrderByDescending方法即可,完整可运行代码如下:
using System.Linq.Expressions; using System.Reflection; internal class Program { private static void Main(string[] args) { Console.WriteLine("Hello, World!"); string propertyForExpression = "Product.randomNumber"; bool isAscending = true; // 对接外部传入的排序方向参数,true升序、false降序 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.DynamicOrderBy(propertyForExpression, isAscending).ToList(); foreach(var order in sortedOrders) { Console.WriteLine(order.Product.randomNumber); } Console.ReadKey(); } /// <summary> /// 动态排序扩展方法 /// </summary> /// <typeparam name="T">待排序实体类型</typeparam> /// <param name="source">源数据集</param> /// <param name="propertyPath">属性路径,支持多层嵌套</param> /// <param name="isAscending">是否升序</param> public static IOrderedEnumerable<T> DynamicOrderBy<T>(IEnumerable<T> source, string propertyPath, bool isAscending = true) { // 1. 构建多层属性访问表达式 var parameter = Expression.Parameter(typeof(T), "x"); Expression propertyBody = parameter; foreach (var propName in propertyPath.Split('.')) { propertyBody = Expression.Property(propertyBody, propName); } Type propertyType = propertyBody.Type; // 2. 构造与属性类型匹配的Func<,>委托类型,不做object转换 Type funcType = typeof(Func<,>).MakeGenericType(typeof(T), propertyType); // 3. 创建强类型Lambda表达式 LambdaExpression keySelector = Expression.Lambda(funcType, propertyBody, parameter); // 4. 反射获取对应的排序方法 string methodName = isAscending ? nameof(Enumerable.OrderBy) : nameof(Enumerable.OrderByDescending); MethodInfo orderMethod = typeof(Enumerable) .GetMethods() .First(m => m.Name == methodName && m.GetParameters().Length == 2) .MakeGenericMethod(typeof(T), propertyType); // 5. 执行排序返回结果 return (IOrderedEnumerable<T>)orderMethod.Invoke(null, new object[] { source, keySelector.Compile() }); } } 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; } }
该实现的特性:
- 全程使用属性原始类型构造表达式,不存在装箱导致的比较失败问题
- 支持任意实现了
IComparable接口的属性类型,所有原生值类型、string等常用类型均默认满足要求 - 支持任意层级的嵌套属性访问,只要路径填写正确即可
- 可直接对接外部传入的排序键、排序方向参数
如果需要对接EF Core等IQueryable场景,只需要把方法内的Enumerable替换为Queryable,去掉最后的Compile()调用即可,逻辑完全一致。
内容的提问来源于stack exchange,提问作者Bart Kuijer
相关产品推荐
相关产品推荐

