在动态Linq Order By上保留NULL行

时间:2017-02-16 14:49:30

标签: c# .net linq reflection

我正在使用下面的代码片段动态订购我的Linq查询并且效果很好。我在反射或复杂的linq查询方面不是很出色,但我需要一种方法,当使用升序时,NULL值是最后一个,反之亦然。

因此,如果我的属性名称是一个整数且列值为1,3,5,则所有NULL行都将在结尾处,而不是默认情况下的开头。我可以添加什么来表达这种情况?

此代码适用于实体框架,仍然需要进行NULL比较。

示例

list.OrderBy("NAME DESC").ToList()

   public static class OrderByHelper
    {
        public static IOrderedQueryable<T> ThenBy<T>(this IEnumerable<T> enumerable, string orderBy)
        {
            return enumerable.AsQueryable().ThenBy(orderBy);
        }

        public static IOrderedQueryable<T> ThenBy<T>(this IQueryable<T> collection, string orderBy)
        {
            if (string.IsNullOrWhiteSpace(orderBy))
                orderBy = "ID DESC";

            IOrderedQueryable<T> orderedQueryable = null;

            foreach (OrderByInfo orderByInfo in ParseOrderBy(orderBy, false))
                orderedQueryable = ApplyOrderBy<T>(collection, orderByInfo);

            return orderedQueryable;
        }

        public static IOrderedQueryable<T> OrderBy<T>(this IEnumerable<T> enumerable, string orderBy)
        {
            return enumerable.AsQueryable().OrderBy(orderBy);
        }

        public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> collection, string orderBy)
        {
            if (string.IsNullOrWhiteSpace(orderBy))
                orderBy = "ID DESC";

            IOrderedQueryable<T> orderedQueryable = null;

            foreach (OrderByInfo orderByInfo in ParseOrderBy(orderBy, true))
                orderedQueryable = ApplyOrderBy<T>(collection, orderByInfo);

            return orderedQueryable;
        }

        private static IOrderedQueryable<T> ApplyOrderBy<T>(IQueryable<T> collection, OrderByInfo orderByInfo)
        {
            string[] props = orderByInfo.PropertyName.Split('.');
            Type type = typeof(T);

            ParameterExpression arg = Expression.Parameter(type, "x");
            Expression expr = arg;
            foreach (string prop in props)
            {
                // use reflection (not ComponentModel) to mirror LINQ
                PropertyInfo pi = type.GetProperty(prop, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);
                expr = Expression.Property(expr, pi);
                type = pi.PropertyType;
            }
            Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);
            LambdaExpression lambda = Expression.Lambda(delegateType, expr, arg);
            string methodName = String.Empty;



            if (!orderByInfo.Initial && collection is IOrderedQueryable<T>)
            {
                if (orderByInfo.Direction == SortDirection.Ascending)
                    methodName = "ThenBy";
                else
                    methodName = "ThenByDescending";
            }
            else
            {
                if (orderByInfo.Direction == SortDirection.Ascending)
                    methodName = "OrderBy";
                else
                    methodName = "OrderByDescending";
            }

            return (IOrderedQueryable<T>)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[] { collection, lambda });
        }

        private static IEnumerable<OrderByInfo> ParseOrderBy(string orderBy, bool initial)
        {
            if (String.IsNullOrEmpty(orderBy))
                yield break;

            string[] items = orderBy.Split(',');

            foreach (string item in items)
            {
                string[] pair = item.Trim().Split(' ');

                if (pair.Length > 2)
                    throw new ArgumentException(String.Format("Invalid OrderBy string '{0}'. Order By Format: Property, Property2 ASC, Property2 DESC", item));

                string prop = pair[0].Trim();

                if (String.IsNullOrEmpty(prop))
                    throw new ArgumentException("Invalid Property. Order By Format: Property, Property2 ASC, Property2 DESC");

                SortDirection dir = SortDirection.Ascending;

                if (pair.Length == 2)
                    dir = ("desc".Equals(pair[1].Trim(), StringComparison.OrdinalIgnoreCase) ? SortDirection.Descending : SortDirection.Ascending);

                yield return new OrderByInfo() { PropertyName = prop, Direction = dir, Initial = initial };

                initial = false;
            }

        }

        private class OrderByInfo
        {
            public string PropertyName { get; set; }
            public SortDirection Direction { get; set; }
            public bool Initial { get; set; }
        }

        private enum SortDirection
        {
            Ascending = 0,
            Descending = 1
        }

4 个答案:

答案 0 :(得分:12)

这相对简单。对于每个传递的排序选择器,该方法执行以下操作之一:

.OrderBy(x => x.Member)
.ThenBy(x => x.Member)
.OrderByDescending(x => x.Member)
.ThenByDescendiong(x => x.Member)

x.Member类型是引用类型或可空值类型时,可以通过以下表达式按相同方向预排序来实现所需的行为

x => x.Member == null ? 1 : 0

有些人使用bool排序,但我更喜欢明确并使用带有特定整数值的条件运算符。因此上述调用的相应调用将是:

.OrderBy(x => x.Member == null ? 1 : 0).ThenBy(x => x.Member)
.ThenBy(x => x.Member == null ? 1 : 0).ThenBy(x => x.Member)
.OrderByDescending(x => x.Member == null ? 1 : 0).ThenByDescending(x => x.Member)
.ThenByDescending(x => x.Member == null ? 1 : 0).ThenByDescending(x => x.Member)

即。预订表达式上的原始方法后跟ThenBy(Descending)原始表达式。

以下是实施:

public static class OrderByHelper
{
    public static IOrderedQueryable<T> ThenBy<T>(this IEnumerable<T> source, string orderBy)
    {
        return source.AsQueryable().ThenBy(orderBy);
    }

    public static IOrderedQueryable<T> ThenBy<T>(this IQueryable<T> source, string orderBy)
    {
        return OrderBy(source, orderBy, false);
    }

    public static IOrderedQueryable<T> OrderBy<T>(this IEnumerable<T> source, string orderBy)
    {
        return source.AsQueryable().OrderBy(orderBy);
    }

    public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string orderBy)
    {
        return OrderBy(source, orderBy, true);
    }

    private static IOrderedQueryable<T> OrderBy<T>(IQueryable<T> source, string orderBy, bool initial)
    {
        if (string.IsNullOrWhiteSpace(orderBy))
            orderBy = "ID DESC";
        var parameter = Expression.Parameter(typeof(T), "x");
        var expression = source.Expression;
        foreach (var item in ParseOrderBy(orderBy, initial))
        {
            var order = item.PropertyName.Split('.')
                .Aggregate((Expression)parameter, Expression.PropertyOrField);
            if (!order.Type.IsValueType || Nullable.GetUnderlyingType(order.Type) != null)
            {
                var preOrder = Expression.Condition(
                        Expression.Equal(order, Expression.Constant(null, order.Type)),
                        Expression.Constant(1), Expression.Constant(0));
                expression = CallOrderBy(expression, Expression.Lambda(preOrder, parameter), item.Direction, initial);
                initial = false;
            }
            expression = CallOrderBy(expression, Expression.Lambda(order, parameter), item.Direction, initial);
            initial = false;
        }
        return (IOrderedQueryable<T>)source.Provider.CreateQuery(expression);
    }

    private static Expression CallOrderBy(Expression source, LambdaExpression selector, SortDirection direction, bool initial)
    {
        return Expression.Call(
            typeof(Queryable), GetMethodName(direction, initial),
            new Type[] { selector.Parameters[0].Type, selector.Body.Type },
            source, Expression.Quote(selector));
    }

    private static string GetMethodName(SortDirection direction, bool initial)
    {
        return direction == SortDirection.Ascending ?
            (initial ? "OrderBy" : "ThenBy") :
            (initial ? "OrderByDescending" : "ThenByDescending");
    }

    private static IEnumerable<OrderByInfo> ParseOrderBy(string orderBy, bool initial)
    {
        if (String.IsNullOrEmpty(orderBy))
            yield break;

        string[] items = orderBy.Split(',');

        foreach (string item in items)
        {
            string[] pair = item.Trim().Split(' ');

            if (pair.Length > 2)
                throw new ArgumentException(String.Format("Invalid OrderBy string '{0}'. Order By Format: Property, Property2 ASC, Property2 DESC", item));

            string prop = pair[0].Trim();

            if (String.IsNullOrEmpty(prop))
                throw new ArgumentException("Invalid Property. Order By Format: Property, Property2 ASC, Property2 DESC");

            SortDirection dir = SortDirection.Ascending;

            if (pair.Length == 2)
                dir = ("desc".Equals(pair[1].Trim(), StringComparison.OrdinalIgnoreCase) ? SortDirection.Descending : SortDirection.Ascending);

            yield return new OrderByInfo() { PropertyName = prop, Direction = dir, Initial = initial };

            initial = false;
        }

    }

    private class OrderByInfo
    {
        public string PropertyName { get; set; }
        public SortDirection Direction { get; set; }
        public bool Initial { get; set; }
    }

    private enum SortDirection
    {
        Ascending = 0,
        Descending = 1
    }
}

答案 1 :(得分:1)

一种方法是将另一个用于测试null的表达式传递给方法,并将其用于其他OrderBy / ThenBy子句中。

将生成两个OrderBy条款 - 第一个将在nullOrder上,而第二个将在实际属性上。

private static IOrderedQueryable<T> ApplyOrderBy<T>(IQueryable<T> collection, OrderByInfo orderByInfo, Expression<Func<T,int>> nullOrder) {
    ...
    if (!orderByInfo.Initial && collection is IOrderedQueryable<T>) {
        if (orderByInfo.Direction == SortDirection.Ascending)
            methodName = "ThenBy";
        else
            methodName = "ThenByDescending";
    } else {
        if (orderByInfo.Direction == SortDirection.Ascending)
            methodName = "OrderBy";
        else
            methodName = "OrderByDescending";
    }
    if (nullOrder != null) {
        collection = (IQueryable<T>)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[] { collection, nullOrder });
        // We've inserted the initial order by on nullOrder,
        // so OrderBy on the property becomes a "ThenBy"
        if (orderByInfo.Direction == SortDirection.Ascending)
            methodName = "ThenBy";
        else
            methodName = "ThenByDescending";
    }
    // The rest of the method remains the same
    return (IOrderedQueryable<T>)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[] { collection, lambda });
}

调用者需要显式传递null检查器。为不可空字段传递null应该有效。您可以构建它们一次,并根据需要传递:

static readonly Expression<Func<string,int>> NullStringOrder = s => s == null ? 1 : 0;
static readonly Expression<Func<int?,int>> NullIntOrder = i => !i.HasValue ? 1 : 0;
static readonly Expression<Func<long?,int>> NullLongOrder = i => !i.HasValue ? 1 : 0;

答案 2 :(得分:1)

我的方法是创建一个实现IComparer<TClass>的泛型类。这样,您可以在所有LINQ语句中使用您的类和非默认比较器。优点是您将在编译时进行完整类型检查。您不能命名无法比较的属性或不能为null的属性

class NullValueLastComparer<TClass, TKey> : IComparer<TClass>
    where TClass : class
    where TKey : IComparable<TKey>
{

此泛型类有两个Type参数:要比较的类,以及要与之比较的属性的类型。 where子句声明TClass是引用类型,因此您可以访问属性,TKey是实现正常比较的东西。

要为类创建对象,我们有两个Factory函数。这两个函数都需要一个KeySelector,类似于LINQ中可以找到的许多Key Selectors。 KeySelector函数是告诉您在比较中必须使用哪个属性的函数。它类似于函数Enumerable.Where中的KeySelector。

第二个Create函数使您可以提供非默认比较器,类似于Enumerable类中的许多函数:

    public static IComparer<TClass> Create(Func<TClass, TKey> keySelector)
    {   // call the other Create function, with the default TKey comparer
        return Create(keySelector, Comparer<TKey>.Default);
    }

    public static IComparer<TClass> Create(Func<TClass, TKey> keySelector, IComparer<TKey> comparer)
    {   // construct a null value last comparer object
        // initialize with the key selector and the key comparer
        return new NullValueLastComparer<TClass, TKey>()
        {
            KeySelector = keySelector,
            KeyComparer = comparer,
        };
    }

我使用私有构造函数。只有静态创建类可以构造最后比较器的空值

    private NullValueLastComparer() { }

两个属性:键选择器和比较器:

    private Func<TClass, TKey> KeySelector { get; set; }
    private IComparer<TKey> KeyComparer { get; set; }

实际的比较功能。它将使用KeySelector来获取值 必须进行比较,并将它们进行比较,使得空值将是最后的。

    public int Compare(TClass x, TClass y)
    {   
        if (Object.ReferenceEquals(x, null))
            throw new ArgumentNullException(nameof(x));
        if (Object.ReferenceEquals(y, null)
            throw new ArgumentNullException(nameof(y));

        // get the values to compare
        TKey keyX = KeySelector(x);
        TKey keyY = KeySelector(y);
        return this.Compare(keyX, keyY);
    }

比较Keys的私有函数,使得null值将是最后的

    private int Compare(TKey x, TKey y)
    {   // compare such that null values last, or if both not null, use IComparable
        if (Object.ReferenceEquals(x, null))
        {
            if (Object.ReferenceEquals(y, null))
            {   // both null
                return 0;
            }
            else
            {   // x null, y not null => x follows y
                return +1;
            }
        }
        else
        {   // x not null
            if (Object.ReferenceEquals(y, null))
            {   // x not null; y null: x precedes y
                return -1;
            }
            else
            {
                return this.KeyComparer.Compare(x, y);
            }
        }
    }
}

<强>用法:

class Person
{
    public string FirstName {get; set;}
    public string FamilyName {get; set;}
}

// create a comparer that will put Persons without firstName last:
IComparer<Person> myComparer =
    NullValueLastComparer<Person, string>.Create(person => person.FirstName);
Person person1 = ...;
Person person2 = ...;

int compareResult = myComparer.Compare(person1, person2);

此比较将比较人员。比较两个人时,两个人都需要person.FirstName,并且将没有FirstName的人作为最后一个。

在复杂的LINQ语句中使用。 请注意,在编译时有完整的类型检查。

IEnumerable<Person> myPersonCollection = ...
var sortedPersons = myPersonCollection
    .OrderBy(person => person, myComparer)
    .ThenBy(person => person.LastName)
    .Select(person => ...)
    .ToDictonary(...)

答案 3 :(得分:1)

对于动态构建按表达式这样list.OrderBy("NAME DESC").ToList(),您可以使用以下查询助手扩展方法

用法

首先,我们检查以确保给定类中存在属性名称。 如果我们不检查,它会抛出运行时异常。

然后我们使用OrderByPropertyOrderByPropertyDescending

string orderBy = "Name";
if (QueryHelper.PropertyExists<User>(orderBy))
{
   list = list.OrderByProperty(orderBy);
   - OR - 
   list = list.OrderByPropertyDescending(orderBy);
}

以下是my project at GitHub 中的真实世界用法。

Query Helper

public static class QueryHelper
{
    private static readonly MethodInfo OrderByMethod =
        typeof (Queryable).GetMethods().Single(method => 
        method.Name == "OrderBy" && method.GetParameters().Length == 2);

    private static readonly MethodInfo OrderByDescendingMethod =
        typeof (Queryable).GetMethods().Single(method => 
        method.Name == "OrderByDescending" && method.GetParameters().Length == 2);

    public static bool PropertyExists<T>(string propertyName)
    {
        return typeof(T).GetProperty(propertyName, BindingFlags.IgnoreCase | 
            BindingFlags.Public | BindingFlags.Instance) != null;
    }

    public static IQueryable<T> OrderByProperty<T>(
       this IQueryable<T> source, string propertyName)
    {
        if (typeof (T).GetProperty(propertyName, BindingFlags.IgnoreCase | 
            BindingFlags.Public | BindingFlags.Instance) == null)
        {
            return null;
        }
        ParameterExpression paramterExpression = Expression.Parameter(typeof (T));
        Expression orderByProperty = Expression.Property(paramterExpression, propertyName);
        LambdaExpression lambda = Expression.Lambda(orderByProperty, paramterExpression);
        MethodInfo genericMethod = 
          OrderByMethod.MakeGenericMethod(typeof (T), orderByProperty.Type);
        object ret = genericMethod.Invoke(null, new object[] {source, lambda});
        return (IQueryable<T>) ret;
    }

    public static IQueryable<T> OrderByPropertyDescending<T>(
        this IQueryable<T> source, string propertyName)
    {
        if (typeof (T).GetProperty(propertyName, BindingFlags.IgnoreCase | 
            BindingFlags.Public | BindingFlags.Instance) == null)
        {
            return null;
        }
        ParameterExpression paramterExpression = Expression.Parameter(typeof (T));
        Expression orderByProperty = Expression.Property(paramterExpression, propertyName);
        LambdaExpression lambda = Expression.Lambda(orderByProperty, paramterExpression);
        MethodInfo genericMethod = 
          OrderByDescendingMethod.MakeGenericMethod(typeof (T), orderByProperty.Type);
        object ret = genericMethod.Invoke(null, new object[] {source, lambda});
        return (IQueryable<T>) ret;
    }
}