Жизненный цикл HTTP-запроса в ASP.NET Core: middleware pipeline?
Путь запроса от сокета до контроллера
- Kestrel читает байты из сокета (через
System.IO.Pipelines), парсит HTTP/1.1, HTTP/2 или HTTP/3. - Создаётся
HttpContext:Request,Response,User,Items,RequestServices(скоуп DI на запрос),Features. HttpContextпередаётся в первый делегат конвейера.- Каждый middleware решает: обработать, передать дальше, или ответить сам.
- В конце — эндпоинт (контроллер, Minimal API, Razor Page, gRPC-сервис, SignalR-хаб).
- Ответ проходит middleware в обратном порядке.
- Скоуп DI уничтожается — вызываются
Disposescoped-сервисов.
Модель «луковицы»
app.Use(async (context, next) =>
{
var sw = Stopwatch.StartNew();
// код ДО — на входе
await next(context); // передать управление следующему
// код ПОСЛЕ — на выходе (ответ уже сформирован)
logger.LogInformation("{Path} → {Status} за {Ms} мс",
context.Request.Path, context.Response.StatusCode, sw.ElapsedMilliseconds);
});public sealed class CorrelationIdMiddleware(RequestDelegate next)
{
private const string Header = "X-Correlation-ID";
public async Task InvokeAsync(HttpContext context, ILogger<CorrelationIdMiddleware> logger) // scoped-зависимости — в InvokeAsync
{
var id = context.Request.Headers.TryGetValue(Header, out var v) ? v.ToString() : Guid.NewGuid().ToString("N");
context.Response.OnStarting(() => { context.Response.Headers[Header] = id; return Task.CompletedTask; });
using (logger.BeginScope(new Dictionary<string, object> { ["CorrelationId"] = id }))
await next(context);
}
}
app.UseMiddleware<CorrelationIdMiddleware>();Правильный порядок
var app = builder.Build();
if (app.Environment.IsDevelopment())
app.UseDeveloperExceptionPage();
else
{
app.UseExceptionHandler("/error"); // 1. самый внешний — ловит всё, что ниже
app.UseHsts();
}
app.UseHttpsRedirection(); // 2. редирект на HTTPS
app.UseStaticFiles(); // 3. статика — без аутентификации, быстро замыкает
app.UseRouting(); // 4. выбор эндпоинта (метаданные для следующих)
app.UseCors(); // 5. после Routing, до Auth
app.UseAuthentication(); // 6. кто ты? → HttpContext.User
app.UseAuthorization(); // 7. можно ли? (читает [Authorize] эндпоинта)
app.UseRateLimiter(); // 8. лимиты (могут зависеть от пользователя)
app.UseOutputCache(); // 9. кэш ответов
app.MapControllers(); // 10. эндпоинты
app.MapHealthChecks("/health");
app.Run();Ветвление конвейера
// Map: отдельный конвейер для префикса пути (путь «отрезается»)
app.Map("/admin", admin =>
{
admin.UseMiddleware<AdminIpFilterMiddleware>();
admin.Run(ctx => ctx.Response.WriteAsync("Admin area"));
});
// MapWhen: ветка по условию, в основной конвейер не возвращается
app.MapWhen(ctx => ctx.Request.Headers.ContainsKey("X-Legacy"), legacy => legacy.UseMiddleware<LegacyMiddleware>());
// UseWhen: ветка по условию, ПОТОМ возвращается в основной конвейер
app.UseWhen(ctx => ctx.Request.Path.StartsWithSegments("/api"), api => api.UseMiddleware<ApiKeyMiddleware>());
// Run: терминальный middleware — next нет
app.Run(async ctx => await ctx.Response.WriteAsync("Fallback"));Endpoint routing: зачем UseRouting отдельно
С ASP.NET Core 3.0 маршрутизация разделена на два шага:
UseRouting— выбирает эндпоинт и кладёт его вHttpContext(context.GetEndpoint()), вместе с метаданными ([Authorize], политика CORS, rate limit).- Middleware между
UseRoutingи эндпоинтами видят метаданные выбранного эндпоинта и действуют по ним. MapControllers()/MapGet()— выполняют эндпоинт (с .NET 6WebApplicationдобавляетUseEndpointsавтоматически).
Типичные ошибки
- Писать в
Responseпослеawait next(), когда ответ уже начал отправляться →InvalidOperationException: Headers are read-only, response has already started. ИспользуйтеResponse.OnStarting. - Читать
Request.Bodyв middleware и не включитьRequest.EnableBuffering()— тело можно прочитать только один раз, контроллер получит пустой поток. - Хранить состояние запроса в полях middleware-класса — он общий для всех запросов → гонки. Для данных запроса —
HttpContext.ItemsилиHttpContext.Features. - Блокирующий I/O в middleware — тормозит весь сервис.
Вопросы на засыпку
Чем middleware отличается от фильтра MVC?
Middleware работает для всех запросов на уровне HTTP и не знает про контроллеры и модели. Фильтры работают только внутри MVC/эндпоинтов, имеют доступ к ActionArguments, результату действия, ModelState. Кросс-срезы HTTP (логирование, заголовки) — middleware; логика вокруг действия — фильтры.
Что такое HttpContext.Features?
Низкоуровневый набор интерфейсов, которые предоставляет сервер: IHttpRequestFeature, IHttpConnectionFeature, IHttpResponseBodyFeature и т.п. HttpContext.Request/Response — удобные обёртки над ними. Middleware может подменять фичи (так работает, например, буферизация ответа).
Можно ли использовать HttpContext в фоновой задаче после завершения запроса?
Нет. HttpContext переиспользуется (пулинг) после завершения запроса; обращение к нему из фоновой задачи — неопределённое поведение. Скопируйте нужные данные (UserId, заголовки) до запуска фоновой работы.
Как short-circuit работает с эндпоинтами в .NET 8?
Метод .ShortCircuit() у эндпоинта (app.MapGet("/ping", ...).ShortCircuit()) выполняет его сразу после UseRouting, пропуская остальные middleware (аутентификацию, CORS) — полезно для health-check и robots.txt.
Итог
Запрос в ASP.NET Core — это HttpContext, проходящий через цепочку делегатов-middleware туда и обратно. Порядок регистрации определяет поведение: обработка ошибок — снаружи, статика — пораньше, аутентификация — перед авторизацией, эндпоинты — в центре.