通过字符串生成 EF orderby 表达式

Posted

技术标签:

【中文标题】通过字符串生成 EF orderby 表达式【英文标题】:Generate EF orderby expression by string [duplicate] 【发布时间】:2015-11-04 11:03:55 【问题描述】:

我想通过字符串参数生成表达式,一些代码如:

private Expression<Func<Task, T>> Generate(string orderby)

    switch (orderby)
    
        case "Time":  
            return t => t.Time;
        case "Money":
            return t => t.RewardMoney;
        default:
            return t => t.Id;
    

然后调用它:

_context.Items.OrderBy(Generate("Money"));

但它无法编译!我把 T 改成 object。

private Expression<Func<Task, object>> Generate(string orderby)

然后它可以编译,但它不起作用。

System.NotSupportedException:无法将类型“System.Int32”转换为类型“System.Object”。 LINQ to Entities 仅支持转换 EDM 基元或枚举类型。

【问题讨论】:

How do I specify the Linq OrderBy argument dynamically?的可能重复 【参考方案1】:

使用reflection和expression-trees可以提供参数然后调用OrderBy函数,而不是返回Expression&lt;Func&lt;Task, T&gt;&gt;然后调用OrderBy

请注意,OrderBy 是一种扩展方法,并已在 System.Linq.EnumarableSystem.Linq.Queryable 类中实现。第一个用于linq-to-objects,后者用于linq-to-entities。 entity-framework 需要查询的表达式树才能将其转换为 SQL 命令。所以我们使用Queryable 实现。

可以通过扩展方法来完成(添加为cmets的解释):

public static IOrderedQueryable<TSource> OrderBy<TSource>(
       this IQueryable<TSource> query, string propertyName)

    var entityType = typeof(TSource);

    //Create x=>x.PropName
    var propertyInfo = entityType.GetProperty(propertyName);
    ParameterExpression arg = Expression.Parameter(entityType, "x");
    MemberExpression property = Expression.Property(arg, propertyName);
    var selector = Expression.Lambda(property, new ParameterExpression[]  arg );

    //Get System.Linq.Queryable.OrderBy() method.
    var enumarableType = typeof(System.Linq.Queryable);
    var method = enumarableType.GetMethods()
         .Where(m => m.Name == "OrderBy" && m.IsGenericMethodDefinition)
         .Where(m =>
         
            var parameters = m.GetParameters().ToList();
            //Put more restriction here to ensure selecting the right overload                
            return parameters.Count == 2;//overload that has 2 parameters
         ).Single();
    //The linq's OrderBy<TSource, TKey> has two generic types, which provided here
    MethodInfo genericMethod = method
         .MakeGenericMethod(entityType, propertyInfo.PropertyType);

    /*Call query.OrderBy(selector), with query and selector: x=> x.PropName
      Note that we pass the selector as Expression to the method and we don't compile it.
      By doing so EF can extract "order by" columns and generate SQL for it.*/
    var newQuery = (IOrderedQueryable<TSource>)genericMethod
         .Invoke(genericMethod, new object[]  query, selector );
    return newQuery;

现在您可以像其他任何重载一样调用 OrderBy 的重载。 例如:

var cheapestItems = _context.Items.OrderBy("Money").Take(10).ToList();

翻译为:

SELECT TOP (10)  coulmn names FROM  [dbo].[Items] AS [Extent1] 
       ORDER BY [Extent1].[Money] ASC

此方法可用于定义 OrderByOrderByDescending 方法的所有重载以具有 string 属性选择器。

【讨论】:

优秀的扩展。我会注意到第一个参数应该是IQueryable&lt;T&gt;,而不是IEnumerable&lt;T&gt;。如果 OP 或任何其他读者需要更多,NuGet 上提供了一个完整的基于字符串的 Linq 方法库(搜索 System.Linq.Dynamic)或此处:msdn.microsoft.com/en-us/vstudio/bb894665.aspx 有机会,我们可以扩展订单 asc 或 desc 吗?谢谢 @sairfan : 默认情况下,“OrderBy”是 ASC,通过在 where 子句中将其替换为“OrderByDescending”,您将获得 DESC。 谢谢,还有一个问题,它支持导航属性吗?我正在尝试这样items.OrderBy("Category.Name"); 并得到错误 @PurTahan 您需要复制此扩展名并将m.Name == "OrderBy" 更改为m.Name == "OrderByDescending"。另外,不要忘记将新方法名称更改为OrderByDescending【参考方案2】:

您可以尝试将Generate 方法转换为泛型方法:

private Expression<Func<Task, TResult>> Generate<TResult>(string orderby)

     switch (orderby)
     
        case "Time":  
          return t => t.Time;
        case "Money":
          return t => t.RewardMoney;
        default:
         return t => t.Id;
     

所以,如果你调用这个方法,你需要指定你想要排序的属性的类型:

_context.Items.OrderBy(Generate<decimal>("Money"));

现在请记住,TResult 只能是基本类型或枚举类型。

【讨论】:

感谢您的回答,但是当您将“时间”作为参数传递时,调用将更改:_context.Items.OrderBy(Generate("Time"))。这个函数失去作用了?【参考方案3】:

我参考了 CodePlex 中旧的System.Linq.Dynamic codebase,从实现和调用的角度创建了一个相当简单的版本。当然是IQueryable&lt;T&gt;上的扩展方法

/*
using System;
using System.Linq;
using System.Linq.Expressions;
*/

public static IQueryable<T> OrderBy<T>(this IQueryable<T> query, string orderByExpression)

    if (string.IsNullOrEmpty(orderByExpression))
        return query;

    string propertyName, orderByMethod;
    string[] strs = orderByExpression.Split(' ');
    propertyName = strs[0];

    if (strs.Length == 1)
        orderByMethod = "OrderBy";
    else
        orderByMethod = strs[1].Equals("DESC", StringComparison.OrdinalIgnoreCase) ? "OrderByDescending" : "OrderBy";

    ParameterExpression pe = Expression.Parameter(query.ElementType);
    MemberExpression me = Expression.Property(pe, propertyName);

    MethodCallExpression orderByCall = Expression.Call(typeof(Queryable), orderByMethod, new Type[]  query.ElementType, me.Type , query.Expression
        , Expression.Quote(Expression.Lambda(me, pe)));

    return query.Provider.CreateQuery(orderByCall) as IQueryable<T>;

这里是如何使用它的示例,已针对 Entity Framework Core 3 进行了测试:

IQueryable<Person> query = dbContext.People;
query = query.OrderBy("FirstName"); // ORDER BY FirstName
IQueryable<Person> query = dbContext.People;
query = query.OrderBy("FirstName ASC"); // ORDER BY FirstName
IQueryable<Person> query = dbContext.People;
query = query.OrderBy("FirstName DESC"); // ORDER BY FirstName DESC

【讨论】:

【参考方案4】:

使用通用方法。由于 lambda 表达式只能分配给强类型的委托或表达式,因此我们必须使用相应的 temp。然后我们可以将此 temp 分配给类型为 object 的变量。最后我们可以通过强制转换为结果类型来返回结果。

public Expression<Func<Task, TResult>> Generate<TResult>(string orderby)

    object result;
    switch (orderby) 
        case "Time":
            Expression<Func<Task, DateTime>> temp1 = t => t.Time;
            result = temp1;
            break;
        case "Money":
            Expression<Func<Task, decimal>> temp2 = t => t.RewardMoney;
            result = temp2;
            break;
        default:
            Expression<Func<Task, int>> temp3 = t => t.Id;
            result = temp3;
            break;
    
    return (Expression<Func<Task, TResult>>)result;

【讨论】:

@Oliver 这个怎么用 @ArunPrasadES, _context.Items.OrderBy(Generate("Money"));,就像 OP 想要的那样。【参考方案5】:
public static IQueryable<T> OrderByHelper<T>(this IQueryable<T> source, string propertyName, string sortDirection)
    

        try
        
            if (source == null)
            
                return source;
            
            if (propertyName == null)
            
                return source;
            

            propertyName = propertyName.First().ToString().ToUpper(new CultureInfo("en-US", false)) + propertyName.Substring(1);
            var type = typeof(T);
            var arg = Expression.Parameter(type, "x");

            var propertyInfo = type.GetProperty(propertyName);
            var mExpr = Expression.Property(arg, propertyInfo);
            type = propertyInfo.PropertyType;

            var delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);
            var lambda = Expression.Lambda(delegateType, mExpr, arg);

            var methodName = !string.IsNullOrEmpty(sortDirection) && sortDirection.ToLower(new CultureInfo("en-US", false)) == "desc" ? "OrderByDescending" : "OrderBy";
            var orderedSource = typeof(Queryable).GetMethods().Single(
                method => method.Name == methodName
                        && method.IsGenericMethodDefinition
                        && method.GetGenericArguments().Length == 2
                        && method.GetParameters().Length == 2)
                .MakeGenericMethod(typeof(T), type)
                .Invoke(null, new object[]  source, lambda );

            return (IQueryable<T>)orderedSource;
        
        catch (Exception)
        

            return source;
        
    

【讨论】:

以上是关于通过字符串生成 EF orderby 表达式的主要内容,如果未能解决你的问题,请参考以下文章

基于子实体的属性构建 OrderBy Lambda 表达式

mvc+ef6多字段排序表达式怎么写

.net core efcore 动态 orderby

EF中执行sql语句

LINQ to Entities OrderBy 表达式树

Apache Spark 2.0:按降序排列到 orderBy() / sort() 列的表达式字符串