Пару дней назад я столкнулся с ситуацией, когда входящий запрос может потребовать сортировки его результата.
Модель была похожа на следующую:
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"
без какой-либо другой проверки. Возможно, в своем коде вы захотите подробнее рассмотреть лучший способ проверки этого значения для обработки случаев и локальных значений.
Счастливого кодирования!