【问题标题】:Create LINQ to entities OrderBy expression on the fly动态创建 LINQ 到实体 OrderBy 表达式
【发布时间】:2020-09-29 07:43:40
【问题描述】:

我正在尝试动态添加 orderby 表达式。但是当执行下面的查询时,我得到以下异常:

System.NotSupportedException:无法 创建一个类型的常量值 '闭合类型'。只有原始类型 ('例如 Int32、String 和 Guid') 在这种情况下受支持。

奇怪的是,我只查询那些原始类型。

string sortBy = HttpContext.Current.Request.QueryString["sidx"];
ParameterExpression prm = Expression.Parameter(typeof(buskerPosting), "posting");
Expression orderByProperty = Expression.Property(prm, sortBy);

// get the paged records
IQueryable<PostingListItemDto> query =
   (from posting in be.buskerPosting
    where posting.buskerAccount.cmsMember.nodeId == m.Id
    orderby orderByProperty
    //orderby posting.Created 
    select new PostingListItemDto { Set = posting }).Skip<PostingListItemDto>((page -   1) * pageSize).Take<PostingListItemDto>(pageSize);

希望有人能对此有所了解!

【问题讨论】:

    标签: c# linq-to-entities


    【解决方案1】:

    由于它们的翻译方式,您基本上不能使用这样的查询表达式。但是,您可以使用扩展方法明确地做到这一点:

    string sortBy = HttpContext.Current.Request.QueryString["sidx"];
    ParameterExpression prm = Expression.Parameter(typeof(buskerPosting), "posting");
    Expression orderByProperty = Expression.Property(prm, sortBy);
    
    // get the paged records
    IQueryable<PostingListItemDto> query = be.buskerPosting
        .Where(posting => posting.buskerAccount.cmsMember.nodeId == m.Id)
        .OrderBy(orderByExpression)
        .Select(posting => new PostingListItemDto { Set = posting })
        .Skip<PostingListItemDto>((page -   1) * pageSize)
        .Take<PostingListItemDto>(pageSize);
    

    棘手的一点是获得正确的表达式树类型 - 这将在编辑中出现 :)

    编辑:由于各种原因,编辑会有所延迟。基本上你可能需要使用反射调用一个泛型方法,因为Queryable.OrderBy 需要一个泛型Expression&lt;Func&lt;TSource, TKey&gt;&gt;,虽然看起来你在编译时知道 source 类型,但你可能不知道关键类型。如果你确实知道它总是按(比如说)一个 int 排序,你可以使用:

    Expression orderByProperty = Expression.Property(prm, sortBy);
    var orderByExpression = Expression.Lambda<Func<buskerPosting, int>>
        (orderByProperty, new[] { prm });
    

    编辑:好吧,看来我毕竟有时间。下面是一个使用反射调用OrderBy 的简短示例:

    using System;
    using System.Reflection;
    using System.Linq;
    using System.Linq.Expressions;
    
    public class Test
    {
        static void Main()
        {
            string[] names = { "Jon", "Holly", "Tom", "Robin", "Will" };
            var query = names.AsQueryable();
            query = CallOrderBy(query, "Length");
            foreach (var name in query)
            {
                Console.WriteLine(name);
            }
        }
    
        private static readonly MethodInfo OrderByMethod =
            typeof(Queryable).GetMethods()
                .Where(method => method.Name == "OrderBy")
                .Where(method => method.GetParameters().Length == 2)
                .Single();
    
        public static IQueryable<TSource> CallOrderBy<TSource>
            (IQueryable<TSource> source, string propertyName)
        {
            ParameterExpression parameter = Expression.Parameter(typeof(TSource), "posting");
            Expression orderByProperty = Expression.Property(parameter, propertyName);
    
            LambdaExpression lambda = Expression.Lambda(orderByProperty, new[] { parameter });
            Console.WriteLine(lambda);
            MethodInfo genericMethod = OrderByMethod.MakeGenericMethod
                (new[] { typeof(TSource), orderByProperty.Type });
            object ret = genericMethod.Invoke(null, new object[] {source, lambda});
            return (IQueryable<TSource>) ret;
        }
    }
    

    您可以像这样轻松地将CallOrderBy 重构为扩展方法(例如OrderByProperty):

    public static class ReflectionQueryable
    {
        private static readonly MethodInfo OrderByMethod =
            typeof(Queryable).GetMethods()
                .Where(method => method.Name == "OrderBy")
                .Where(method => method.GetParameters().Length == 2)
                .Single();
    
        public static IQueryable<TSource> OrderByProperty<TSource>
            (this IQueryable<TSource> source, string propertyName)
        {
            ParameterExpression parameter = Expression.Parameter(typeof(TSource), "posting");
            Expression orderByProperty = Expression.Property(parameter, propertyName);
    
            LambdaExpression lambda = Expression.Lambda(orderByProperty, new[] { parameter });
            Console.WriteLine(lambda);
            MethodInfo genericMethod = OrderByMethod.MakeGenericMethod
                (new[] { typeof(TSource), orderByProperty.Type });
            object ret = genericMethod.Invoke(null, new object[] {source, lambda});
            return (IQueryable<TSource>) ret;
        }    
    }
    

    然后您的原始代码变为:

    string sortBy = HttpContext.Current.Request.QueryString["sidx"];
    // get the paged records
    IQueryable<PostingListItemDto> query = be.buskerPosting
        .Where(posting => posting.buskerAccount.cmsMember.nodeId == m.Id)
        .OrderByProperty(sortBy)
        .Select(posting => new PostingListItemDto { Set = posting })
        .Skip<PostingListItemDto>((page -   1) * pageSize)
        .Take<PostingListItemDto>(pageSize);
    

    (对于涉及水平滚动条的格式表示歉意......如果有人关心,我稍后会重新格式化。或者如果你有足够的代表,你可以为我做;)

    【讨论】:

    • OrderBy 不期望 lambda 表达式吗?例如:Expression.Lambda&lt;Func&lt;T,U&gt;&gt;(orderByProperty, prm)
    • @Michael:是的,它需要Expression&lt;...&gt;——这就是我使用orderByExpression而不是orderByProperty的原因。我希望填写其余的代码,但出现了一些问题,所以我现在无法完成。
    • 太棒了!拯救了我的一天。
    • 这是黄金。您可以使用MethodInfo genericMethod = listSortDirection == ListSortDirection.Ascending ? OrderByMethod.MakeGenericMethod(new[] { typeof(TSource), orderByProperty.Type }): OrderByDescendingMethod.MakeGenericMethod(new[] {typeof(TSource), orderByProperty.Type }); 之类的代码轻松制作降序版本,其中OrderByDescendingMethodOrderByMethod 基本相同,但文本中有OrderByDescending。然后你只需添加一个参数ListSortDirection 就可以了。
    【解决方案2】:

    我想以上面 Jon 的回答为起点分享我的实现。在这种情况下,我不是按来自表示层的字符串属性名称进行排序(因为这个问题的标题并不具体),而是构建一个实体框架数据层并希望允许其使用者指定顺序通过属性作为 lambda 表达式。 IE。我不想通过"sidx",而是希望能够使用p =&gt; p.sidx。我还希望能够通过属性传递无限数量的顺序,并能够指定升序或降序。

    我的方法可以接受 Expression&lt;Func&lt;T, object&gt;&gt; 这样的 lambda 表达式。那让我按照我想要的方式来称呼它,但问题是,实体框架无法将表达式转换为 SQL,除非第二个泛型参数是强类型的。 OrderBy 扩展方法需要两个泛型参数:T - 属性所属的类型,以及 TKey - 属性返回的类型。所以第一步是修改 Jon 的示例,将给定的Expression&lt;Func&lt;T, object&gt;&gt; 转换为Expression&lt;Func&lt;T, Tkey&gt;&gt;(一旦我们在查询的上下文中工作,我们就可以确定TKey 的类型):

    internal static IQueryable<T> OrderByDynamic<T>(this IQueryable<T> source, Expression<Func<T, object>> sortExp)
    {
        //We need to convert the key selector from Expression<Func<T, object>> to a strongly typed Expression<Func<T, TKey>>
        //in order for Entity Framework to be able to translate it to SQL
        MemberExpression orderByMemExp = ExpressionHelpers.RemoveConvert(sortExp.Body) as MemberExpression;
        ParameterExpression sourceParam = sortExp.Parameters[0];
    
        LambdaExpression orderByLamda = Expression.Lambda(orderByMemExp, new[] { sourceParam });
    
        MethodInfo orderByMethod = OrderByMethod.MakeGenericMethod(new[] { typeof(T), orderByMemExp.Type });
    
        //Call OrderBy or OrderByDescending on the source IQueryable<T>
        return (IQueryable<T>)orderByMethod.Invoke(null, new object[] { source, orderByLamda });
    }
    

    正如我所提到的,我希望通过键选择器接受无限数量的排序,并且还能够指定升序或降序方向,因此我为 Expression&lt;Func&lt;T, object&gt;&gt; 创建了一个包装类,我将其命名为 DynamicSortExpression:

    public class DynamicSortExpression<T>
    {
        /// <summary>
        /// Creates a new ascending DynamicSortExpression 
        /// </summary>
        /// <param name="keySelector">A MemberExpression identifying the property to sort on</param>
        public DynamicSortExpression(Expression<Func<T, object>> keySelector) : this(keySelector, false)
        {
        }
    
        public DynamicSortExpression(Expression<Func<T, object>> keySelector, bool descending)
        {
            this.KeySelector = keySelector;
            this.Desc = descending;
        }
    
        /// <summary>
        /// Gets the expression that selects the property of T to sort on
        /// </summary>
        public Expression<Func<T, object>> KeySelector { get; }
    
        /// <summary>
        /// Gets sort expression is in ascending or descending order
        /// </summary>
        public bool Desc { get; }
    }
    

    然后我更新了扩展方法以接受这种类型,并为接收List&lt;DynamicSortExpression&lt;T&gt;&gt;OrderBy 创建了一个重载,并使用扩展方法将它们一一添加到查询中。这是最终结果:

    public static class Extensions
    {
        private static readonly MethodInfo OrderByMethod = typeof(Queryable).GetMethods()
                                                            .Where(method => method.Name == "OrderBy")
                                                            .Where(method => method.GetParameters().Length == 2)
                                                            .Single();
    
        private static readonly MethodInfo OrderByDescMethod = typeof(Queryable).GetMethods()
                                                                .Where(method => method.Name == "OrderByDescending")
                                                                .Where(method => method.GetParameters().Length == 2)
                                                                .Single();
    
        private static readonly MethodInfo ThenByMethod = typeof(Queryable).GetMethods()
                                                        .Where(method => method.Name == "ThenBy")
                                                        .Where(method => method.GetParameters().Length == 2)
                                                        .Single();
    
        private static readonly MethodInfo ThenByDescMethod = typeof(Queryable).GetMethods()
                                                        .Where(method => method.Name == "ThenByDescending")
                                                        .Where(method => method.GetParameters().Length == 2)
                                                        .Single();
    
        internal static IQueryable<T> OrderBy<T>(this IQueryable<T> sourceQuery, List<DynamicSortExpression<T>> orderBy)
        {
            bool isFirst = true;
            foreach (var sortExpression in orderBy)
            {
                if (isFirst)
                {
                    sourceQuery = sourceQuery.OrderByDynamic(sortExpression);
                    isFirst = false;
                }
                else
                    sourceQuery = sourceQuery.ThenByDynamic(sortExpression);
            }
    
            return sourceQuery;
        }
    
        internal static IQueryable<T> OrderByDynamic<T>(this IQueryable<T> source, DynamicSortExpression<T> sortExpression)
        {
            //We need to convert the key selector from Expression<Func<T, object>> to a strongly typed Expression<Func<T, TKey>>
            //in order for Entity Framework to be able to translate it to SQL
            MemberExpression orderByMemExp = ExpressionHelpers.RemoveConvert(sortExpression.KeySelector.Body) as MemberExpression;
            ParameterExpression sourceParam = sortExpression.KeySelector.Parameters[0];
    
            LambdaExpression orderByLamda = Expression.Lambda(orderByMemExp, new[] { sourceParam });
    
            MethodInfo orderByMethod = sortExpression.Desc ?
                                            OrderByDescMethod.MakeGenericMethod(new[] { typeof(T), orderByMemExp.Type }) :
                                            OrderByMethod.MakeGenericMethod(new[] { typeof(T), orderByMemExp.Type });
    
            //Call OrderBy or OrderByDescending on the source IQueryable<T>
            return (IQueryable<T>)orderByMethod.Invoke(null, new object[] { source, orderByLamda });
        }
    
        internal static IQueryable<T> ThenByDynamic<T>(this IQueryable<T> source, DynamicSortExpression<T> sortExpression)
        {
            //We need to convert the key selector from Expression<Func<T, object>> to a strongly typed Expression<Func<T, TKey>>
            //in order for Entity Framework to be able to translate it to SQL
            Expression orderByMemExp = ExpressionHelpers.RemoveConvert(sortExpression.KeySelector.Body) as MemberExpression;
            ParameterExpression sourceParam = sortExpression.KeySelector.Parameters[0];
    
            LambdaExpression orderByLamda = Expression.Lambda(orderByMemExp, new[] { sourceParam });
    
            MethodInfo orderByMethod = sortExpression.Desc ?
                                            ThenByDescMethod.MakeGenericMethod(new[] { typeof(T), orderByMemExp.Type }) :
                                            ThenByMethod.MakeGenericMethod(new[] { typeof(T), orderByMemExp.Type });
    
            //Call OrderBy or OrderByDescending on the source IQueryable<T>
            return (IQueryable<T>)orderByMethod.Invoke(null, new object[] { source, orderByLamda });
        }
    }
    

    现在我的数据层可以有一个像List&lt;T&gt; GetList(Expression&lt;Func&lt;T, bool&gt;&gt; where, params DynamicSortExpression&lt;T&gt;[] orderBy)这样的方法,可以像这样调用

    new MyClass<Person>().GetList(p => p.FirstName == "Billy", //where clause
                                  new DynamicSortExpression<Person>(p => p.FirstName),
                                  new DynamicSortExpression<Person>(p => p.LastName, true));
    

    在内部,MyClass&lt;T&gt;.GetList 构建调用我的 OrderBy 扩展方法的查询。比如:

    DBContext.Set<T>().Where(whereParam).OrderBy(orderByParams);
    

    RemoveConvert 方法是我从 EntityFramework 源代码中提取出来的,用于递归删除 MemberExpression 中的 Convert 调用:

    internal static Expression RemoveConvert(Expression expression)
    {
        System.Diagnostics.Debug.Assert(expression != null);
    
        while ((expression != null)
                && (expression.NodeType == ExpressionType.Convert
                    || expression.NodeType == ExpressionType.ConvertChecked))
        {
            expression = RemoveConvert(((UnaryExpression)expression).Operand);
        }
    
        return expression;
    }
    

    我希望这会有所帮助!谢谢乔恩!

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-05-22
      • 1970-01-01
      • 2017-04-25
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多