通过字符串生成EF排序表达式

32
我希望可以通过字符串参数生成表达式,类似下面的代码:
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 基元类型或枚举类型强制转换。


https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/advanced?view=aspnetcore-7.0 - Joseph Wambura
6个回答

65
使用,您可以提供参数,然后调用OrderBy函数,而不是返回Expression<Func<Task, T>>,然后调用OrderBy
请注意,OrderBy是一个扩展方法,已在System.Linq.EnumarableSystem.Linq.Queryable类中实现。前者用于,后者用于需要查询的表达式树以将其转换为SQL命令。因此,我们使用Queryable实现。
它可以通过扩展方法完成(添加了注释作为解释):
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 属性选择器。

5
优秀的扩展。我要指出第一个参数应该是IQueryable<T>,而不是IEnumerable<T>。如果OP或任何其他读者需要更多信息,则可以在NuGet上搜索基于字符串的Linq方法的整个库(搜索System.Linq.Dynamic),或访问此处:https://msdn.microsoft.com/en-us/vstudio/bb894665.aspx - KeithS
谢谢,我还有一个问题,它支持导航属性吗?我尝试像这样 items.OrderBy("Category.Name"); 但是出现了错误。 - sairfan
不,它不会。如果您想要更多的灵活性,最好选择KeithS在第一条评论中提到的动态库。 - Taher Rahgooy
1
@PurTahan 你需要复制这个扩展并将 m.Name == "OrderBy" 更改为 m.Name == "OrderByDescending"。另外,别忘了将新方法名更改为 OrderByDescending - Taher Rahgooy
1
顺便提一下,.ThenBy() 和 .ThenByDescending() 也是如此。非常感谢您的回答...这为我解决了所有问题!! - IdahoB
显示剩余3条评论

9

对于那些在EF Core中寻找解决方案的人:

Microsoft.EntityFrameworkCore.EF中提供了一组函数,用于动态访问和查询编译。您可以使用EF.Property方法按属性名称或甚至阴影属性对可查询对象进行排序。 例如:

bool descending = false;
if (sortOrder.EndsWith("_desc"))
{
    sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
    descending = true;
}

if (descending)
{
    students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
}
else
{
    students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
}

来源:教程:学习高级场景 - ASP.NET MVC与EF Core


5

我参考了 CodePlex 上旧的 System.Linq.Dynamic 代码库,从实现和调用的角度创建了一个相当简单的版本。当然,它是作为 IQueryable<T> 的扩展方法。

/*
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

您可以尝试将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 只能是原始类型或枚举类型。

1
谢谢您的回答,但是当您将“Time”作为参数传递时,调用将会改变:_context.Items.OrderBy(Generate<DataTime>("Time"))。这个函数失去了它的功能吗? - yubaolee

2

使用通用方法。由于lambda表达式只能赋值给强类型的委托或表达式,因此我们必须使用相应的临时变量。然后,我们可以将此临时变量分配给以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,如何使用这个? - Arun Prasad E S
1
@ArunPrasadES,_context.Items.OrderBy(Generate("Money"));,正如OP所希望的那样。 - Olivier Jacot-Descombes
这个答案不会产生 _context.Items.OrderBy(Generate("Money"));,它会产生 _context.Items.OrderBy(Generate<decimal>("Money"));,这是不好的。 - Idan

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

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