Храните строки NULL последним в Dynamic Linq Order By

Я использую этот фрагмент ниже для упорядочивания моих запросов 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
        }

Ответ 1

Это относительно просто. Для каждого переданного сортировщика сортировки метод выполняет одно из следующих действий:

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

Когда тип x.Member является ссылочным типом или типом значения NULL, желаемое поведение может быть достигнуто путем предварительного упорядочения в том же направлении следующим выражением

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)

то есть. исходный метод для выражения pre order, за которым следует 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
    }
}

Ответ 2

Один из подходов состоит в том, чтобы передать дополнительное выражение для тестирования для 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 для полей с недействительными значениями должна работать. Вы можете построить их один раз и передать по мере необходимости:

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;

Ответ 3

Мой подход заключается в создании универсального класса, реализующего IComparer<TClass>. Таким образом, вы можете использовать свой класс во всех операторах LINQ с нестандартным компаратором. Преимущество заключается в том, что во время компиляции у вас будет полная проверка типов. Вы не можете назвать свойства, которые не могут быть сопоставлены или которые не могут быть null

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

Этот общий класс имеет два параметра типа: класс, который вы хотите сравнить, и тип свойства, с которым вы хотите сравнить. Предложения where утверждают, что TClass является ссылочным типом, поэтому вы можете получить доступ к Свойствам, а TKey - это то, что реализует нормальное сравнение.

Чтобы создать объекты для класса, у нас есть две функции Factory. Обеим функциям нужен KeySelector, аналогичный множеству селекторов ключей, которые вы можете найти в LINQ. Функция KeySelector - это функция, которая сообщит вам, какое свойство должно использоваться в ваших сравнениях. Он похож на KeySelector в функции Enumerable.Where.

Вторая функция 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,
        };
    }

Я использую частный конструктор. Только статические классы create могут создавать нулевое значение последнего компаратора

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

Частная функция, которая сравнивает Ключи таким образом, что нулевые значения будут последними

    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(...)

Ответ 4

Для динамически построенного порядка. По выражению, подобному этому list.OrderBy("NAME DESC").ToList(), вы можете использовать следующий метод расширения вспомогательного запроса.

Использование

Прежде всего, мы проверяем, чтобы имя свойства существовало в данном классе. Если мы не будем проверять, это приведет к исключению времени выполнения.

Затем мы используем либо OrderByProperty, либо OrderByPropertyDescending.

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

Вот реальное использование в моего проекта в GitHub.

Помощник запроса

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