Динамическая сортировка IQueryable

Пару дней назад я столкнулся с ситуацией, когда входящий запрос может потребовать сортировки его результата.

Модель была похожа на следующую:

public record Sorting(string PropertyName, string Order);

public class MyRequest : IRequest<SomeDto>
{
    // Some fields
    public Sorting? Sort { get; init; }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Несколько дополнительных правил были неявными:

Используя EF Core, условно сортировать полученный запрос было легко, но сделать это чище было не так просто.

В итоге я пришел к решению, которое, на мой взгляд, достаточно чистое, и хотел бы поделиться им с теми, кому оно может когда-нибудь помочь!

Контекст

Наш пример будет основан на очень простой модели всего с двумя полями и таким же типом сортировки DTO, который был у меня:

public record User(string FirstName, string LastName);
public record Sorting(string PropertyName, string Order);
Войти в полноэкранный режим Выход из полноэкранного режима

В нашем примере Sorting не будет нулевым, но в реальном случае не забудьте проверить это!

Что касается наших данных, то они могут быть чем-то вроде списка или DbSet с запросом, содержащим входящие строки:

var users = new List<User>
{
    new("Pierre", "Bouillon"),
    new("Thomas", "Anderson"),
    new("Tom", "Scott"),
    new("Keanu", "Reeves"),
    new("Edward", "Snowden"),
};

// Or an EF Core query:
var users = Context.Users.Where(user => /* ... */);

var sort = new Sorting("FirstName", "asc");
Войти в полноэкранный режим Выйти из полноэкранного режима

Все готово!

Наивный подход

Во-первых, мы можем легко написать очень наивный подход: проверить каждое поле, которое может потребовать сортировки, а затем порядок, прежде чем вернуть отсортированный запрос.

if (sort.PropertyName == nameof(User.FirstName))
{
    if (sort.Order == "asc")
    {
        return users.OrderBy(user => user.FirstName);
    }
    else
    {
        return users.OrderByDescending(user => user.FirstName);
    }
}

if (sort.PropertyName == nameof(User.LastName))
{
    if (sort.Order == "asc")
    {
        return users.OrderBy(user => user.LastName);
    }
    else
    {
        return users.OrderByDescending(user => user.LastName);
    }
}

throw new ArgumentException();
Войти в полноэкранный режим Выйти из полноэкранного режима

Это немного многословно, особенно проверка порядка каждый раз, когда есть только два значения: либо asc, либо что-то еще, мы можем сделать лучше.

Использование троичных операторов

Тернарный оператор (также называемый условным оператором в справочнике по C#) не всегда легко читается и должен использоваться с осторожностью, но я нахожу его довольно явным.

Давайте уточним нашу проверку порядка с его помощью:

if (sort.PropertyName == nameof(User.FirstName))
{
    return sort.Order == "asc"
        ? users.OrderBy(user => user.FirstName)
        : users.OrderByDescending(user => user.FirstName);
}

if (sort.PropertyName == nameof(User.LastName))
{
    return sort.Order == "asc"
        ? users.OrderBy(user => user.LastName)
        : users.OrderByDescending(user => user.LastName);
}

throw new ArgumentException();
Войти в полноэкранный режим Выйти из полноэкранного режима

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

if (sort.PropertyName == /* a property */)
{
    return /* users sorted with the appropriate order */;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Должен быть способ напрямую вернуть соответствующий запрос на основе PropertyName

Использование выражения switch

Введенные в C# 8.0, выражения switch могут пригодиться в таких случаях.

Что касается PropertyName, мы можем применить нашу логику и напрямую вернуть наш тернарный оператор:

return sort.PropertyName switch
{
    nameof(User.FirstName) => sort.Order == "asc"
        ? users.OrderBy(user => user.FirstName)
        : users.OrderByDescending(user => user.FirstName),

    nameof(User.LastName) => sort.Order == "asc"
        ? users.OrderBy(user => user.FirstName)
        : users.OrderByDescending(user => user.FirstName),

    _ => throw new ArgumentException(),
};
Войти в полноэкранный режим Выйти из полноэкранного режима

Это гораздо более лаконично, но теперь, когда заключающие утверждения if были отрефакторены, мы можем заметить, что здесь много повторяющегося кода:

return sort.PropertyName switch
{
    nameof(/* property */) => sort.Order == "asc"
        ? users.OrderBy(user => user./* property */)
        : users.OrderByDescending(user => user./* property */),

    nameof(/* property */) => sort.Order == "asc"
        ? users.OrderBy(user => user./* property */)
        : users.OrderByDescending(user => user./* property */),

    _ => throw new ArgumentException(),
};
Войти в полноэкранный режим Выход из полноэкранного режима

Возможно, нам стоит поискать другой способ разделения сортировки.

Использование выражений

Делая шаг назад, мы видим, что сортировка наших пользователей требует извлечения двух данных:

  • Имя свойства
  • порядок

В нашей предыдущей логике оценка порядка была сделана внутри оценки имени свойства, что привело к дублированию кода в этом месте.

Если мы внимательно посмотрим на метод Enumerable.OrderBy, то увидим, что он принимает в качестве параметра ключ как функцию.

Давайте еще раз доработаем наш код так, чтобы мы извлекали ключ перед применением порядка:

Expression<Func<User, string>> sortBy = sort.PropertyName switch
{
    nameof(User.FirstName) => user => user.FirstName,
    nameof(User.LastName) => user => user.LastName,
    _ => throw new ArgumentException(),
};

return sort.Order == "asc"
    ? users.OrderBy(sortBy)
    : users.OrderByDescending(sortBy);
Войти в полноэкранный режим Выйти из полноэкранного режима

Это намного лучше!

Кроме того, нам просто нужно добавить еще одну руку в оператор switch, если мы хотим сортировать по новому свойству, что будет довольно просто.

Кстати, мы просто сравнивали порядок с "asc" без какой-либо другой проверки. Возможно, в своем коде вы захотите подробнее рассмотреть лучший способ проверки этого значения для обработки случаев и локальных значений.

Счастливого кодирования!

Оцените статью
Procodings.ru
Добавить комментарий