Получение данных запроса
Binding связывает данные HTTP-запроса с параметрами обработчика. Query-строка содержит текст, однако C#-метод может ожидать целое число, необязательную строку или сервис. ASP.NET Core должен определить источник каждого параметра и выполнить необходимое преобразование. Рассмотрим этот механизм на фильтре каталога, отделяя ошибку преобразования от недопустимого предметного значения.
Продолжаем проект с двумя GET endpoints: список и отдельный курс. В lesson-07/after меняется операция списка. Она получает необязательные minLessons и q, чтобы ограничить число уроков снизу и найти слово в названии. Четыре исходные записи и точный адрес /api/courses сохраняются. Все результаты вычислены редакционно по исходным данным, без запуска приложения.
Источник и тип параметра
Фрагмент, который заменяет handler списка внутри существующей группы, выглядит так:
group.MapGet("", (int? minLessons, string? q, ICatalogRepository repository) =>
{
if (minLessons is < 1 or > 500)
return Results.ValidationProblem(new Dictionary<string, string[]>
{ ["minLessons"] = ["Число должно быть от 1 до 500."] });
var items = repository.GetAll();
var result = items.Where(c => c.Lessons >= (minLessons ?? 1)
&& (string.IsNullOrWhiteSpace(q) || c.Title.Contains(q.Trim(), StringComparison.OrdinalIgnoreCase)));
return Results.Ok(result.ToArray());
});
minLessons не присутствует в шаблоне пути, поэтому простое числовое значение читается из query. Nullable-тип int? допускает отсутствие параметра: в таком случае значение будет null. q тоже необязателен. ICatalogRepository уже зарегистрирован как сервис и получается из DI. Три параметра одного метода поступают из двух разных источников.
Для GET /api/courses?minLessons=16 ожидаем число 16 в первом параметре и null во втором. Для GET без query обе пользовательские величины отсутствуют, но repository всё равно разрешается. Не нужно создавать пустой объект запроса или передавать список курсов от клиента: список принадлежит серверному источнику данных.
Официальное описание parameter binding Microsoft перечисляет источники и правила вывода. Для этих GET endpoints простые необязательные значения берутся из query, имя параметра определяет имя ключа. При более сложной сигнатуре источник можно обозначить атрибутом, но сначала важно понимать применяемое стандартное правило.
Преобразование и предметная проверка
GET с minLessons=abc содержит строку, которую нельзя преобразовать в int. Binding завершается ошибкой до вызова тела handler. Ожидаемый статус — 400. Наш if и поиск данных в таком случае не являются причиной отказа, потому что числового параметра ещё не получилось.
GET с minLessons=0 преобразуется успешно: ноль является корректным целым числом. Затем handler отклоняет его своим правилом, поскольку минимальное число уроков в учебном фильтре ограничено диапазоном 1–500. Статус также 400, но теперь ошибка относится к предметному ограничению и возвращается через ValidationProblem с ключом minLessons.
Эти различия полезны при разработке клиента. Если он отправляет число в неправильном формате, исправлять нужно представление запроса. Если формат верен, но значение не разрешено, нужно показать человеку правило поля. Framework не знает, почему для нашего каталога выбран предел 500, и не должен придумывать его самостоятельно.
Отсутствие значения не является ошибкой. Выражение minLessons ?? 1 выбирает нижнюю границу по умолчанию. Не следует использовать здесь ноль только потому, что это значение по умолчанию для int: предметная модель отдельно задаёт допустимый диапазон. Nullable-форма делает отсутствие видимым и позволяет решить его осознанно.
Предсказуемое сочетание условий
Два условия соединяются оператором &&. Курс должен иметь достаточно уроков и одновременно соответствовать поиску по названию. При отсутствующей или состоящей из пробелов строке q второе условие пропускает все записи. При непустой строке сначала убираются крайние пробелы, затем проверяется вхождение без учёта регистра по выбранному OrdinalIgnoreCase.
Для фильтра minLessons=16 ожидаются JavaScript и HTML/CSS. Первому соответствует 20, второму 16; две записи по 12 исключаются. Для minLessons=16&q=javascript остаётся только JavaScript. Общая сумма исходного seed не изменяется: фильтр строит представление ответа, а не удаляет записи из repository.
Пример формы ответа для второго запроса:
[{"id":"javascript","title":"Современный JavaScript","topic":"frontend","lessons":20}]
Поиск неизвестного слова ожидаемо возвращает пустой массив со статусом 200. Запрос понятен, фильтр допустим, но совпадений нет. Это отличается от GET отдельного отсутствующего курса, где 404 сообщает об отсутствии конкретного ресурса. Список остаётся существующим ресурсом даже при нуле выбранных элементов.
ToArray материализует результат до сериализации. После этого ответ содержит определённый набор записей, а не ленивое описание обхода repository. Для четырёх объектов это небольшой шаг, но он делает границу данных очевидной. В будущей базе фильтрацию следует переносить ближе к запросу, а не сначала читать огромную таблицу ради одной страницы.
Граница возможностей примера
Сейчас у фильтра нет пагинации, сортировки по пользовательскому полю или полнотекстового поиска. Contains в памяти не обещает морфологию русского языка и не использует индексы PostgreSQL. Он выбран для объяснения получения параметров. Новые функции потребуют своих правил и не возникают из переименования query-ключа.
Клиент должен кодировать query-значения как часть URL. Пробел и другие специальные символы нельзя механически вставлять в адрес без кодирования. Смысл C#-параметра определяется уже разобранным запросом, а не тем, как человек визуально записал ссылку. При многократном повторении одного ключа также требуется явно выбрать контракт, вместо надежды на универсальную обработку всех возможных форм.
В Development framework способен превращать неверное преобразование в BadHttpRequestException. Чтобы последующая общая граница исключений не изменила смысл такой ошибки, с этого снимка явно зарегистрирован RouteHandlerOptions.ThrowOnBadRequest = false. Неверный binding тогда остаётся прямым отказом 400. Это настройка нашего проекта; нельзя приписывать её всякому приложению по одному похожему handler. Подробнее связь с общим обработчиком будет разобрана в уроке об ошибках.
Фильтрация здесь не является скрытой изменяющей командой. Если последовательность GET с разными значениями приводит к исчезновению курсов из постоянного состояния, значит реализация нарушила выбранную границу чтения. Объект ответа может содержать меньше элементов, но repository продолжает хранить весь исходный набор. Этот принцип пригодится и после переноса фильтра в SQL: WHERE определяет строки результата, а DELETE — изменение данных.
В итоге handler получает данные, которые уже соответствуют его базовым типам, и применяет собственное предметное правило. Это разделение позволяет объяснить одинаковый статус 400 разными причинами, не смешивая механизмы. В следующем уроке перейдём от фильтра GET к JSON-команде: проверим поля будущего курса и вернём клиенту ошибки конкретных значений.