Конвейер middleware
Middleware — компонент конвейера, через который проходит HTTP-запрос. Он может выполнить действие перед следующим компонентом, передать управление дальше и продолжить работу после возвращения. Благодаря этому журналирование, обработка ошибок и другие общие операции не нужно повторять в каждом endpoint. В нашем каталоге рассмотрим порядок такой обработки на двух простых компонентах.
Продолжаем проект с зарегистрированным ICatalogRepository. GET /api/courses по-прежнему возвращает четыре курса и 60 уроков. В lesson-05/after изменяется только Program.cs: между построением приложения и регистрацией endpoints добавляются два middleware. Они не изменяют каталог и не создают отдельные запросы.
Движение внутрь и обратно
Полный файл запуска текущего урока включён в снимок. Его две новые части выглядят так:
app.Use(async (context, next) =>
{
app.Logger.LogInformation("A: before");
await next(context);
app.Logger.LogInformation("A: after");
});
app.Use(async (context, next) =>
{
app.Logger.LogInformation("B: before");
await next(context);
app.Logger.LogInformation("B: after");
});
app.MapCatalog();
context описывает текущее обращение: запрос, ответ, пользователя и связанные данные. next передаёт его оставшейся части конвейера. await означает, что продолжение после этой строки произойдёт после завершения следующего компонента. Поэтому регистрация A, затем B не приводит к последовательности «A целиком, B целиком». A начинает работу, входит в B, тот входит дальше, а завершение происходит в обратном направлении.
Для обычного успешного GET ожидаемые сообщения наших компонентов имеют такой относительный порядок:
A: before
B: before
B: after
A: after
Это схема сообщений конкретного запроса, а не обещание полного журнала процесса. Между ними framework может писать другие события. При одновременных запросах строки разных обращений могут перемешаться. В уроке о структурированном журнале добавим идентификатор, чтобы связать события одного обращения без предположения о соседстве строк.
Данные для ответа получает handler каталога внутри этого движения. Он не обязан знать, что A и B записывают сообщения. После его выполнения управление возвращается в B, затем в A. Подобная форма позволяет измерять время или ставить область журналирования вокруг всей нижележащей операции. В текущей партии измерений нет: мы объясняем порядок, не публикуем вымышленные миллисекунды.
Передача управления и короткий ответ
Если компонент не вызывает next, нижняя часть конвейера не выполняется. Это называют коротким завершением запроса. Оно полезно, когда ответ уже определён, например для отказа по известному условию. Однако случайно забытый next способен сделать все endpoints недоступными, хотя их регистрация остаётся в исходниках.
Рассмотрим отдельный учебный фрагмент, который можно поставить вместо A. Он не входит в полный конечный снимок:
app.Use(async (context, next) =>
{
if (context.Request.Path == "/maintenance")
{
context.Response.StatusCode = 503;
await context.Response.WriteAsync("Учебный ответ обслуживания");
return;
}
await next(context);
});
Для /maintenance вызывается запись ответа и затем return. B и catalog handler не получают этот запрос. Для других путей управление передаётся дальше. Проверка показывает именно устройство конвейера; она не является готовым режимом обслуживания production. В частности, формат ошибки, заголовки кеширования и условия включения такого режима требуют отдельного контракта.
У middleware есть ещё одно ограничение: после начала записи тела ответа нельзя надёжно менять уже отправленные заголовки и статус. Поэтому нельзя сначала вывести часть успешного JSON, затем обнаружить ошибку и обещать клиенту полноценный 500 с другим телом. Обработчик должен по возможности определить результат до начала выдачи, а внешний компонент — учитывать, был ли ответ уже начат.
Порядок общих обязанностей
Обработчик исключений должен окружать ту часть конвейера, ошибки которой он собирается преобразовать. Если он расположен после компонента, выбрасывающего исключение до next, он его не увидит. Это следует из того же движения управления: компонент может поймать только вызванную им нижележащую работу, а не уже завершившуюся верхнюю.
Порядок маршрутизации, CORS, аутентификации и авторизации также имеет значение, потому что каждый следующий шаг использует результаты предыдущего. Но эти компоненты не добавляются только ради длинного списка. В первых 16 уроках нет входа пользователей и cookie-сессии. Мы не обозначаем открытый учебный CRUD как защищённый и не вводим фиктивную проверку заголовка вместо настоящей модели доступа.
Документация Microsoft по middleware описывает этот конвейер и короткое завершение. В маленьком WebApplication некоторые инфраструктурные компоненты вставляются автоматически по зарегистрированным возможностям. Поэтому строки MapGet не следует мысленно читать как обычный вызов обработки HTTP: они объявляют endpoints для последующего выбора и выполнения.
Наш A и B зарегистрированы до MapCatalog, но существенная связь состоит в окружении выполнения endpoint. Они не фильтруют конкретную тему каталога и не зависят от repository. Общая обязанность находится в общем конвейере, предметная — в handler. Смешивание этих уровней затрудняет понимание причин: middleware, который сам ищет курс и решает предметную ошибку, начинает повторять маршруты и правила данных.
Что следует наблюдать после запуска
При последующем выполнении нужно отдельно читать ответ и события. GET /api/courses ожидаемо сохраняет статус 200 и четыре записи. GET к неизвестному пути проходит через A и B, но маршрута для него нет, поэтому ожидается 404. Сам факт наличия сообщений A и B не доказывает, что был вызван catalog handler: общий конвейер может обработать и обращение без подходящего endpoint.
Если нижележащая работа завершится исключением, обычные строки после await next не исполнятся. Для действия, необходимого при любом завершении, нужен try/finally; при этом нельзя обещать успешно отправленный статус, пока исключение ещё будет обрабатывать внешний компонент. Такие нюансы станут важными для журнала запросов в следующей группе тем.
В наших компонентах logger доступен через app, а данные конкретного запроса — через context. Это разные времена жизни. Сохранять context в статическом поле для позднего использования нельзя: следующий запрос имеет свой контекст, а исходное обращение уже может завершиться. Если после ответа нужна самостоятельная фоновая работа, ей следует передать только необходимые значения и определить отдельную область ресурсов, а не удерживать всё HTTP-обращение.
Для текущего урока достаточно одного запроса и относительного порядка четырёх собственных сообщений. При последующем ручном наблюдении не пытайтесь восстановить конвейер по абсолютно всем соседним строкам консоли: фоновые события host и другие обращения имеют собственную последовательность. Контекст событий требуется именно потому, что серверное время не складывается в один общий учебный сценарий.
Таким образом, порядок регистрации задаёт вложенность обязанностей, а вызов next управляет продолжением. Пример не меняет данные и адреса каталога, но позволяет объяснить, где находится общая работа вокруг обращения. В следующем уроке рассмотрим, как внутри этого обращения выбирается endpoint по методу и пути, и добавим маршрут отдельного курса.