Перейти к содержанию

Маршрутизация запросов

Маршрутизация выбирает endpoint по HTTP-методу и пути запроса. Наличие слова courses в адресе не заставляет сервер искать курс: приложение должно объявить, какой участок пути обозначает идентификатор и какой handler использует его. В этом уроке объединим endpoints каталога общей группой и добавим чтение одного курса.

В исходном состоянии GET /api/courses возвращает весь список через ICatalogRepository. Четыре курса не меняются. Снимок lesson-06/after сохраняет конфигурацию и DI, но заменяет файл Endpoints/CatalogEndpoints.cs. Учебный сервер остаётся отдельным Development-приложением на loopback; показанные обращения не выполнялись при написании.

Общий префикс и параметр пути

Полный файл регистрации endpoints этого шага выглядит так:

namespace CatalogApi;
public static class CatalogEndpoints
{
    public static void MapCatalog(this WebApplication app)
    {
        var group = app.MapGroup("/api/courses");
        group.MapGet("", (ICatalogRepository repository) => repository.GetAll());
        group.MapGet("/{id}", (string id, ICatalogRepository repository) =>
        {
            var course = repository.Find(id);
            return course is null ? Results.NotFound() : Results.Ok(course);
        });
    }
}

MapGroup("/api/courses") создаёт общую группу маршрутов. Вызов MapGet("") внутри неё означает GET по самому префиксу. Вызов MapGet("/{id}") добавляет один сегмент после префикса. Итоговые пути — /api/courses и /api/courses/{id}. Группа помогает не повторять строку префикса и позднее применять общие метаданные.

Имя id в фигурных скобках связано с параметром string id handler. Для запроса /api/courses/javascript значение равно javascript. Framework получает его из пути, а repository выполняет предметный поиск. Эти два действия различаются: маршрутизация нашла подходящее описание endpoint, но это ещё не означает существование курса с таким идентификатором.

Если Find возвращает запись, handler формирует успешный результат. Если он возвращает null, выбран ответ 404. На этом шаге тело ошибки пустое; единый формат Problem Details появится позже. Nullable-результат интерфейса вынуждает явно рассмотреть оба исхода, вместо обращения к свойствам отсутствующего объекта.

Для GET /api/courses/javascript ожидается объект:

{"id":"javascript","title":"Современный JavaScript","topic":"frontend","lessons":20}

Для GET /api/courses/not-present ожидается 404. Оба обращения выбирают один и тот же endpoint, но repository получает разные значения. В первом случае данные существуют, во втором отсутствуют. Это хорошее различие для чтения журнала: одинаковое имя endpoint способно давать разные предметные результаты.

Метод является частью выбора

GET и POST к одному пути имеют разные назначения. Пока в проекте зарегистрированы только GET endpoints, POST /api/courses/javascript не запускает функцию чтения. При существующем пути с неподдержанным методом ожидается 405. Если путь вообще не соответствует объявленному шаблону, ожидается 404. Не следует превращать любую неудачу клиента в «маршрут не найден»: метод тоже участвует в выборе.

Query-строка не добавляет новый сегмент. В запросе /api/courses/javascript?view=short параметр id по-прежнему равен javascript. Наш текущий handler не использует view, поэтому ответ от него не меняется. На следующем шаге query-параметры будут объявлены отдельно и получат смысл. Простое присутствие значения в URL ещё не является реализацией фильтра.

Идентификаторы каталога — строки, включая html-css. Мы не добавляем к ним ограничение :int, потому что оно отвергло бы все реальные записи нашего seed. Тип и формат параметра выбираются из предметной модели, а не по привычке использовать числовой ID в учебных примерах.

Рассмотрим отдельную иллюстрацию, не включённую в конечный проект:

app.MapGet("/numbers/{id:int}", (int id) => new { id });

Здесь ограничение относится к совпадению маршрута. /numbers/12 подходит, /numbers/twelve не подходит и при отсутствии другого endpoint даёт 404. Такой результат отличается от ошибки преобразования query-параметра выбранного handler, которую рассмотрим в следующем уроке. Ограничение маршрута удобно для различения форм URL; его не следует использовать как замену всей валидации предметных значений.

Статические и параметрические пути

В продолжении появится POST /api/courses/validate. Сегмент validate будет статическим, а {id} — параметрическим. Routing имеет правила выбора подходящих шаблонов; порядок строк в исходнике не стоит воспринимать как простой цикл «возьмём первый подходящий обработчик». Если объявления становятся неоднозначными, лучше изменить модель маршрутов, чем надеяться на случайное соседство строк.

Статический служебный путь также требует учитывать реальные идентификаторы. Нельзя бездумно создавать служебные GET endpoints под именами, которые допустимы для курса, и затем удивляться пересечению. В нашей операции проверки команды используется другой HTTP-метод и явное служебное значение. Принцип остаётся тем же: публичное пространство путей проектируется целиком.

Описание route handlers и route groups Microsoft служит источником для способов объявления. Учебные пути и случаи отсутствующего курса — наши собственные договорённости. Framework выбирает endpoint, но не определяет, какие идентификаторы действительны и какое объяснение должна получить ошибка каталога.

Адрес ресурса сохраняет смысл

Перенос всех endpoints в группу не создаёт новую версию API. Общий GET всё ещё находится по /api/courses, а добавленный адрес обозначает один конкретный курс. Если клиент сохранил этот адрес, изменение папки C# или названия метода не должно его ломать. URL следует рассматривать как внешний контракт отдельно от организации исходников.

В дальнейшем ID не будет зависеть от текущего названия курса. Иначе переименование «HTML и CSS» потребовало бы менять адрес и все ссылки на него. Отдельный идентификатор позволяет менять отображаемые данные без переселения ресурса. Для новых записей сервер позднее будет генерировать свои ID; сейчас используем фиксированные учебные строки.

Имя C#-параметра также нельзя менять без внимания к источнику. В шаблоне объявлен {id}, поэтому переименование параметра handler в несвязанное имя может изменить правило binding или потребовать явного указания источника. Рефакторинг, который выглядит внутренним, становится частью связи маршрута и данных запроса. Читайте шаблон и сигнатуру вместе: отдельная корректная строка пути ещё не подтверждает корректность всей операции.

Сам адрес javascript является идентификатором ресурса, а не командой выполнить JavaScript. Сервер использует его как строковый ключ поиска. Такое различие помогает избегать лишней магии при проектировании: участок URL имеет только то значение, которое ему назначает конкретный контракт приложения.

После настоящего запуска сравните успешный список, успешную отдельную запись, неизвестный ID и неподдержанный метод. Эти случаи проверяют разные стороны выбора и поиска. В текущей подготовке они остаются ожидаемыми, поэтому статья не утверждает, что сервер уже выдал конкретный ответ. В следующем уроке добавим фильтрацию списка и разберём, как строки запроса превращаются в параметры C#.