Перейти до вмісту

Хостинг і конвеєр

Шлях запиту в ASP.NET Core: який сервер його приймає, як він проходить конвеєр middleware, як маршрутизація обирає обробник і як хост запускає та зупиняє застосунок. З цих питань видно, чи розуміє кандидат, що відбувається між мережею та його кодом.

Перед цією темою: Асинхронність.

Junior+

Що таке Kestrel і навіщо перед ним IIS або reverse proxy?

Section titled “Що таке Kestrel і навіщо перед ним IIS або reverse proxy?”
Відповідь

Що це

  • Kestrel — вебсервер, вбудований в ASP.NET Core. Він слухає порт і перетворює HTTP-запит на HttpContext для конвеєра middleware.
  • Reverse proxy — сервер перед застосунком, який пересилає запити з мережі в Kestrel.

Навіщо проксі

  • Один порт 443 на кілька сайтів: Kestrel не ділить порт з іншими процесами.
  • Сертифікати й HTTPS налаштовуються в одному місці.
  • Балансування між екземплярами застосунку.
  • Ще один рівень захисту.

Як користуватися

WebApplication.CreateBuilder вмикає Kestrel сам. Без налаштувань він слухає http://localhost:5000. За проксі першим у конвеєрі викликають app.UseForwardedHeaders().

Типова помилка

Забути про forwarded headers. Застосунок бачить IP проксі замість IP клієнта і http замість https, а UseHttpsRedirection зациклює редиректи.

Як влаштовано

  1. Транспорт приймає з’єднання: TCP-сокет, Unix-сокет за Nginx або, з .NET 8, іменований канал Windows.
  2. Якщо ввімкнено HTTPS, з’єднання проходить TLS через SslStream.
  3. Парсер HTTP/1.1, HTTP/2 або HTTP/3 читає байти з System.IO.Pipelines і створює HttpContext.
  4. Запит іде в конвеєр middleware на потоці пулу.

Коли що обирати

Середовище Варіант
Linux, віртуальна машина Kestrel за Nginx, YARP або Apache
Kubernetes Kestrel за ingress або балансувальником
Windows Server з IIS IIS і ASP.NET Core Module
Windows без IIS, потрібен спільний порт HTTP.sys

Пастки

  • Forwarded headers за замовчуванням приймаються лише від loopback-адрес. Адресу свого проксі додають у KnownProxies, інакше заголовки мовчки ігноруються.
  • ForwardLimit за замовчуванням дорівнює 1: з ланцюжка проксі читається лише останнє значення X-Forwarded-For.
  • Ліміти Kestrel (MaxRequestBodySize — 30 000 000 байтів, таймаути заголовків) і ліміти проксі діють одночасно. Спрацьовує менший.

Правило

Один застосунок і TLS більше ніде розшифрувати — Kestrel може дивитися в мережу сам. Кілька сайтів на одному IP або централізований TLS — ставте проксі й налаштовуйте forwarded headers.

Під капотом

  • Kestrel читає й пише через System.IO.Pipelines з буферами з власного пулу пам’яті, тому заголовки розбираються майже без алокацій. З .NET 10 пули Kestrel, IIS і HTTP.sys самі повертають пам’ять під час простою.
  • Сокети обслуговують IOCP або epoll, а код застосунку працює в пулі потоків. Блокувальний виклик в обробнику забирає потік у всіх з’єднань.
  • В IIS in-process Kestrel немає: запит іде з HTTP.sys через IIS просто в IISHttpServer всередині w3wp.exe.

Ціна

  • Проксі — додатковий перехід і повторний розбір HTTP. Зазвичай це частки мілісекунди, але під навантаженням це ще й з’єднання, буфери та CPU другого сервера.
  • Буферизація відповідей у Nginx ламає стримінг і Server-Sent Events, а таймаути проксі рвуть довгі WebSocket, якщо їх не налаштувати.
  • TLS на Kestrel витрачає CPU застосунку. TLS на проксі розвантажує застосунок, але всередині мережі трафік іде відкритим.

Компроміси

  • Довіра до X-Forwarded-For — межа безпеки. Широкий KnownIPNetworks (до .NET 10 — KnownNetworks) або ForwardLimit = null дають клієнтові змогу підробити свій IP і обійти ліміти за IP.
  • ASPNETCORE_FORWARDEDHEADERS_ENABLED=true вмикає forwarded headers без обмеження за адресами проксі. Це зручно за хмарним балансувальником і небезпечно, якщо застосунок доступний напряму.

Як перевірити в продакшені

  • dotnet-counters monitor з метриками .NET 8+ з Microsoft.AspNetCore.Server.Kestrel: kestrel.active_connections, kestrel.queued_connections, kestrel.rejected_connections, kestrel.tls_handshake.duration.
  • У логах замість IP клієнтів IP проксі — увімкніть рівень Debug для forwarded headers: middleware пише «Unknown proxy» з адресою, якої немає в списку довірених.

Підсумок: проксі переносить TLS, ліміти й довіру на край мережі, а платять за це затримкою, буферизацією та акуратним налаштуванням forwarded headers.

Приклад
using System.Net;
using Microsoft.AspNetCore.HttpOverrides;
var builder = WebApplication.CreateBuilder(args);
// Ліміти самого Kestrel
builder.WebHost.ConfigureKestrel(options =>
options.Limits.MaxRequestBodySize = 10 * 1024 * 1024); // 10 МБ
// Проксі (Nginx, YARP) передає IP клієнта і схему в X-Forwarded-*
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownProxies.Add(IPAddress.Parse("10.0.0.10")); // довіряємо лише своєму проксі
});
var app = builder.Build();
app.UseForwardedHeaders(); // першим: решта побачать справжні схему та IP
app.UseHttpsRedirection();
app.MapGet("/", (HttpContext ctx) =>
$"{ctx.Request.Scheme} {ctx.Connection.RemoteIpAddress}"); // https 203.0.113.5
app.Run();
Junior+

Що таке middleware і чому важливий їхній порядок?

Section titled “Що таке middleware і чому важливий їхній порядок?”
Відповідь

Що це

Middleware — ланка конвеєра. Кожна ланка отримує HttpContext і вирішує:

  • обробити запит і передати його далі через next;
  • відповісти самій і не викликати next.

Навіщо

Наскрізні задачі — помилки, HTTPS, статика, автентифікація, стиснення — живуть в окремих ланках і не змішуються з бізнес-кодом.

Як користуватися

Метод Що робить
app.Use(...) ланка з викликом next
app.Run(...) кінцева ланка, next немає
app.Map("/path", ...) гілка конвеєра за префіксом шляху
app.UseXxx() вбудований middleware: UseExceptionHandler, UseStaticFiles

Типова помилка

Поставити UseExceptionHandler після інших middleware. Винятків з ланок, які стоять вище за нього, він не перехопить.

Як влаштовано

  1. Код ланки до await next(context) виконується на шляху запиту «всередину».
  2. Код після next виконується на зворотному шляху, коли нижні ланки вже сформували відповідь.
  3. Ланка, яка не викликає next, замикає конвеєр: нижні ланки не виконуються.
  4. Map переносить префікс, що збігся, з Path у PathBase. MapWhen розгалужує за умовою, а UseWhen після гілки повертається в основний конвеєр.

Рекомендований порядок

  1. UseForwardedHeaders, якщо застосунок за проксі.
  2. UseExceptionHandler і UseHsts; у Development — сторінка помилок розробника.
  3. UseHttpsRedirection.
  4. UseStaticFiles — віддає файл і замикає конвеєр.
  5. UseRouting.
  6. UseRateLimiter, UseCors.
  7. UseAuthentication, UseAuthorization.
  8. Власні middleware, потім endpoint’и: MapControllers, MapGet.

Пастки

  • Після початку відповіді заголовки й статус змінювати не можна — буде виняток. Запис у тіло після next може порушити Content-Length.
  • UseRateLimiter з політиками на endpoint’ах ([EnableRateLimiting]) працює лише після UseRouting.
  • Автентифікація не відхиляє анонімні запити, вона лише визначає користувача. Відмову дає авторизація.

Правило

Спочатку те, що має бачити всі запити й перехоплювати всі помилки, потім те, що дешево відсікає зайве, потім безпека, потім бізнес-логіка.

Під капотом

  • Use додає в ApplicationBuilder фабрику Func<RequestDelegate, RequestDelegate>. Під час старту хоста Build() проходить список з кінця й обгортає кожну ланку навколо наступної.
  • Результат — один RequestDelegate, який сервер викликає на кожен запит. Порядок «заморожений» до перезапуску.
  • Map і MapWhen будують вкладений конвеєр теж під час старту, а не на кожен запит.

Ціна

  • Ланка — це виклик делегата, а async-ланка — ще й власна state machine. Десяток легких middleware майже безкоштовні.
  • Повільна ланка на початку конвеєра сповільнює всі запити, зокрема статику й health checks. Дороге логування до UseStaticFiles оплачується за кожну картинку.

Компроміси

  • Middleware проти фільтрів MVC: middleware не знає про модель і action, зате працює для будь-яких endpoint’ів і статики. Логіку, прив’язану до контролерів, зручніше тримати у фільтрах.
  • Ланку, якій треба знати вибраний endpoint, ставлять після UseRouting. Тоді рішення ухвалюється за метаданими endpoint’а, а не за рядком шляху.

Як перевірити в продакшені

  • Повільну ланку шукають, вставляючи між middleware замір часу через Stopwatch — так радить документація з маршрутизації.
  • Метрика http.server.request.duration (.NET 8+) проти часу action у логах показує, скільки з’їдає конвеєр навколо endpoint’а.
  • Порядок закріплюють інтеграційними тестами на WebApplicationFactory: запит без токена отримує 401, статика віддається без автентифікації.

Підсумок: конвеєр — ланцюжок делегатів, побудований один раз під час старту, і порядок ланок — частина архітектури, а не стиль коду.

Приклад
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
Console.WriteLine("A: before");
await next(context); // далі конвеєром
Console.WriteLine("A: after"); // на зворотному шляху
});
app.Map("/health", health => // гілка: /health відповідає тут
health.Run(ctx => ctx.Response.WriteAsync("OK")));
app.Use(async (context, next) =>
{
if (!context.Request.Headers.ContainsKey("X-Api-Key"))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
return; // коротке замикання: next не викликаємо
}
await next(context);
});
app.Run(ctx => ctx.Response.WriteAsync("Hello")); // термінальна ланка
app.Run();
// GET /health -> 200 OK
// GET / -> 401
// GET / з X-Api-Key -> 200 Hello; у консолі завжди A: before, A: after
Джерела
Middle+

Як запускається і зупиняється застосунок: Generic Host і WebApplicationBuilder?

Section titled “Як запускається і зупиняється застосунок: Generic Host і WebApplicationBuilder?”
Відповідь

Що це

Хост (Generic Host) володіє всім, що потрібно застосунку:

  • конфігурацією та логуванням;
  • контейнером залежностей;
  • фоновими сервісами (IHostedService) і вебсервером.

Як користуватися

  1. WebApplication.CreateBuilder(args) — builder з налаштуваннями за замовчуванням.
  2. builder.Services.Add... — реєстрація сервісів.
  3. builder.Build() — побудова застосунку.
  4. app.Use... і app.Map... — опис конвеєра.
  5. app.Run() — запуск і очікування зупинки.

Життєвий цикл

IHostApplicationLifetime повідомляє про три події: ApplicationStarted, ApplicationStopping, ApplicationStopped. Метод StopApplication() зупиняє застосунок з коду.

Типова помилка

Реєструвати сервіси після builder.Build(). Контейнер уже побудовано, і виклик кине InvalidOperationException: колекція сервісів стала лише для читання.

Як влаштовано

  1. CreateBuilder задає content root і читає appsettings.json, appsettings.{Environment}.json, user secrets (у Development), змінні середовища й аргументи командного рядка. Він же додає логування та сервер: Kestrel та інтеграцію з IIS.
  2. Build() створює контейнер. У Development увімкнено перевірки scope і залежностей.
  3. Run() викликає StartAsync: hosted services стартують по черзі в порядку реєстрації, потім будується конвеєр, стартує сервер і спрацьовує ApplicationStarted.
  4. Зупинка: ApplicationStopping → сервер перестає приймати з’єднання й чекає на поточні запити → StopAsync решти hosted services у зворотному порядку → ApplicationStopped.

Коли що обирати

Спосіб Коли
WebApplication.CreateBuilder звичайний вебзастосунок, .NET 6+
WebApplication.CreateSlimBuilder Native AOT і мінімум функцій, .NET 8+
Host.CreateApplicationBuilder worker-сервіс без HTTP
Host.CreateDefaultBuilder і Startup старий код .NET Core 3.x–5

Пастки

  • Запити, які не вклалися в ShutdownTimeout (за замовчуванням 30 секунд), обриваються.
  • Колбеки ApplicationStopping виконуються синхронно: довгий колбек затримує всю зупинку.
  • Довгий StartAsync у hosted service затримує старт сервера — порт ще не слухається.

Правило

Довгу ініціалізацію робіть через await до app.Run() або в BackgroundService, а не в StartAsync. Якщо запити бувають довшими за 30 секунд, задайте ShutdownTimeout явно.

Під капотом

  • Вебсервер — теж hosted service (GenericWebHostService). Під час старту він будує конвеєр і запускає IServer: Kestrel, IISHttpServer або HTTP.sys.
  • IHostLifetime вирішує, коли починати й закінчувати: ConsoleLifetime (Ctrl+C, SIGINT, SIGTERM, SIGQUIT), WindowsServiceLifetime, SystemdLifetime. З .NET 6 SIGTERM обробляється через POSIX-сигнали, а не через AppDomain.ProcessExit.
  • З .NET 8 IHostedLifecycleService додає StartingAsync, StartedAsync, StoppingAsync і StoppedAsync. HostOptions.ServicesStartConcurrently і ServicesStopConcurrently вмикають паралельні старт і зупинку.

Ціна

Бюджет зупинки задає середовище, і застосунок має в нього вкластися:

Середовище Чекає до примусового завершення
docker stop 10 секунд
Kubernetes terminationGracePeriodSeconds, за замовчуванням 30
Пул IIS Shutdown Time Limit, за замовчуванням 90 секунд

Якщо ShutdownTimeout більший за бюджет середовища, процес уб’ють посеред зупинки, і ApplicationStopped не виконається.

Компроміси

  • Довгий ShutdownTimeout береже запити, але сповільнює деплой і масштабування.
  • У Kubernetes под прибирається з балансування паралельно із SIGTERM. Тому додають коротку паузу перед зупинкою (preStop), доки нові запити не перестануть надходити.
  • WebHostBuilder і WebHost у .NET 10 застаріли (попередження ASPDEPR004 і ASPDEPR008): новий код — лише на Generic Host.

Як перевірити в продакшені

  • Категорія логів Microsoft.Hosting.Lifetime пише «Application started» і «Application is shutting down…». За ними видно, скільки тривала зупинка.
  • Помилки 502 і 503 у клієнтів під час деплою та OperationCanceledException у логах — ознака, що бюджет середовища менший за потрібний.

Підсумок: коректна зупинка — це узгоджені таймаути хоста, сервера й середовища, а не одне налаштування ShutdownTimeout.

Приклад
var builder = WebApplication.CreateBuilder(args);
// Поточним запитам під час зупинки даємо до 20 секунд
builder.Services.Configure<HostOptions>(o => o.ShutdownTimeout = TimeSpan.FromSeconds(20));
var app = builder.Build();
IHostApplicationLifetime lifetime = app.Lifetime;
lifetime.ApplicationStarted.Register(() => app.Logger.LogInformation("Started"));
lifetime.ApplicationStopping.Register(() => app.Logger.LogInformation("Stopping: draining requests"));
lifetime.ApplicationStopped.Register(() => app.Logger.LogInformation("Stopped"));
app.MapGet("/slow", async (CancellationToken ct) =>
{
await Task.Delay(TimeSpan.FromSeconds(10), ct); // при Ctrl+C встигне завершитися
return "done";
});
app.Run(); // блокує потік до зупинки хоста
// Ctrl+C під час /slow: Stopping → відповідь "done" → Stopped
Middle+

Як написати власний middleware і чим відрізняються способи за часом життя?

Section titled “Як написати власний middleware і чим відрізняються способи за часом життя?”
Відповідь

Що це

Власний middleware — ланка конвеєра з вашою логікою: замір часу, ідентифікатор кореляції, перевірка заголовка.

Як користуватися

  1. Лямбда: app.Use(async (context, next) => { ...; await next(context); }). Підходить для пари рядків.
  2. Клас за угодою:
    • конструктор приймає RequestDelegate next;
    • публічний метод InvokeAsync(HttpContext context) повертає Task;
    • підключення — app.UseMiddleware<MyMiddleware>() або власний метод розширення app.UseMy().
  3. IMiddleware: клас реалізує метод InvokeAsync(HttpContext, RequestDelegate) і реєструється в DI.

Типова помилка

Забути await next(context). Запит далі не піде, і клієнт отримає порожню відповідь 200.

Як влаштовано

Спосіб Коли створюється Scoped-залежності
Лямбда в app.Use один раз під час побудови конвеєра через context.RequestServices
Клас за угодою один раз під час побудови конвеєра параметрами InvokeAsync
IMiddleware на кожен запит, з DI у конструкторі

Коли що обирати

  • Клас за угодою — за замовчуванням. Немає зайвої роботи на запит, налаштування передаються через UseMiddleware<T>(args) або IOptions<T>.
  • IMiddleware — коли потрібен конструктор зі scoped-залежностями й сигнатура, яку перевіряє компілятор.

Пастки

  • Поля класу за угодою спільні для всіх запитів. Стан запиту в полі — гонка між потоками.
  • Scoped-сервіс у конструкторі класу за угодою: у Development — виняток «Cannot resolve scoped service … from root provider» під час старту, у продакшені — один екземпляр на весь застосунок.
  • Незареєстрований IMiddleware не заважає старту, але перший же запит отримає 500.
  • Аргументи в UseMiddleware<T>(...) для IMiddleware передати не можна — NotSupportedException.

Правило

Конструктор — для синглтонів і налаштувань, InvokeAsync — для всього, що живе в межах запиту.

Під капотом

  • UseMiddleware<T> працює під час побудови конвеєра. Для класу за угодою він знаходить Invoke або InvokeAsync і створює екземпляр через ActivatorUtilities з кореневого контейнера.
  • Якщо в InvokeAsync лише параметр HttpContext, виклик іде прямим делегатом. Додаткові параметри розв’язуються на кожен запит із context.RequestServices.
  • Для IMiddleware у конвеєр стає обгортка. На кожен запит вона бере IMiddlewareFactory зі scope запиту, отримує екземпляр, викликає його й повертає фабриці через Release. Звільняє екземпляр контейнер разом зі scope.

Ціна

  • IMiddleware з реєстрацією Transient — алокація й розв’язання залежностей на кожен запит.
  • Перевантаження app.Use з Func<Task> замість RequestDelegate створює на кожен запит два зайві об’єкти. З .NET 6 є перевантаження app.Use((context, next) => next(context)), де next — RequestDelegate, і цих алокацій немає.

Компроміси

  • Клас за угодою швидший, але його контракт не перевіряє компілятор: помилка в імені InvokeAsync спливе не під час збирання проєкту, а винятком під час старту.
  • IMiddleware простіше тестувати й декорувати. Власний IMiddlewareFactory дає змогу брати middleware зі стороннього контейнера.

Як перевірити в продакшені

  • Захоплений scoped-сервіс ловить перевірка scope: тримайте її ввімкненою в Development та в інтеграційних тестах.
  • Алокації на запит видно в dotnet-counters (швидкість алокацій GC) і в навантажувальному тесті до та після зміни.
  • Незареєстрований IMiddleware ловить smoke-тест на WebApplicationFactory, який робить хоча б один запит.

Підсумок: вибір способу — це вибір часу життя: синглтон із залежностями запиту в параметрах або об’єкт на кожен запит.

Приклад
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<RequestInfo>();
builder.Services.AddScoped<TimingMiddleware>(); // IMiddleware має бути в DI
var app = builder.Build();
app.UseMiddleware<CorrelationMiddleware>("X-Correlation-Id"); // аргумент конструктора
app.UseMiddleware<TimingMiddleware>();
app.MapGet("/", (RequestInfo info) => info.Id); // той самий scoped-об'єкт, що й у middleware
app.Run();
// За угодою: один екземпляр на весь застосунок
public class CorrelationMiddleware(RequestDelegate next, string header)
{
public Task InvokeAsync(HttpContext context, RequestInfo info) // scoped — параметром
{
info.Id = context.Request.Headers[header].FirstOrDefault() ?? Guid.NewGuid().ToString();
return next(context);
}
}
// IMiddleware: свій екземпляр на кожен запит, scoped — у конструкторі
public class TimingMiddleware(RequestInfo info, ILogger<TimingMiddleware> logger) : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
long start = Environment.TickCount64;
await next(context);
logger.LogInformation("{Id}: {Ms} ms", info.Id, Environment.TickCount64 - start);
}
}
public class RequestInfo { public string Id { get; set; } = ""; }
Middle+

Як працює endpoint routing і як обирається маршрут?

Section titled “Як працює endpoint routing і як обирається маршрут?”
Відповідь

Що це

  • Endpoint — обробник запиту: делегат MapGet, action контролера, Razor Page, хаб SignalR.
  • Маршрут — шаблон URL, за яким обирається endpoint.

Як користуватися

Шаблон Що означає
/orders/{id} параметр id
/orders/{id:int} лише ціле число
/orders/{id?} необов’язковий параметр
/{page=Home} значення за замовчуванням
/files/{**path} catch-all: увесь залишок шляху

Значення параметрів приходять в аргументи обробника: (int id) => ....

Типова помилка

Перевіряти введення обмеженнями маршруту. Неправильне значення дасть 404, хоча клієнтові потрібен 400 із зрозумілим повідомленням.

Як влаштовано

  1. UseRouting зіставляє запит з усіма endpoint’ами й зберігає переможця: context.GetEndpoint(), значення параметрів — у Request.RouteValues.
  2. Middleware між UseRouting і UseEndpoints читають метадані endpoint’а: [Authorize], політику CORS, ліміти.
  3. UseEndpoints викликає делегат endpoint’а.

WebApplication (.NET 6+) додає UseRouting на початок, якщо ви не викликали його самі, і UseEndpoints у кінець. Хоч би де стояв app.MapGet у коді, endpoint виконується в кінці конвеєра.

Як обирається маршрут

Порядок реєстрації не важливий, перемагає конкретніший шаблон:

  • літерал сильніший за параметр: /orders/new перемагає /orders/{id};
  • параметр з обмеженням сильніший за параметр без нього;
  • catch-all найслабший.

Якщо два endpoint’и рівні за пріоритетом, запит падає з AmbiguousMatchException. Явний Order — крайній захід.

Пастки

  • Middleware перед явним UseRouting не бачать endpoint: GetEndpoint() повертає null.
  • UseCors і UseRateLimiter з політиками endpoint’ів ставлять після UseRouting.
  • Неоднозначність не видно під час старту: /{x:alpha} і /{x:int} рівні за пріоритетом, але не перетинаються, тому маршрутизація не може перевірити це заздалегідь.

Правило

Спільний префікс і спільні метадані виносьте в MapGroup (.NET 7+). Рішення «пускати чи ні» ухвалюйте за метаданими endpoint’а, а не за рядком шляху.

Під капотом

  • Під час першого запиту маршрутизація будує з усіх шаблонів автомат (DFA) і перебудовує його, коли змінюються джерела endpoint’ів. Час зіставлення залежить від довжини шляху, а не від кількості маршрутів.
  • Кандидатів з автомата відсіюють обмеження та MatcherPolicy: HTTP-метод, хост, тип вмісту. EndpointSelector обирає найкращого за Order і пріоритетом шаблону.
  • Endpoint — RequestDelegate плюс EndpointMetadataCollection. Метадані — будь-які об’єкти: атрибути, RequireAuthorization(), WithMetadata(...).

Ціна

  • Regex-обмеження та складені сегменти на кшталт {x}-{y} помітно дорожчі за звичайні й створюють більше рядків.
  • Тисячі маршрутів з параметром у перших сегментах ({tenant}/...) роздувають автомат: довгий перший запит і безліч об’єктів DfaNode у дампі. Допомагають обмеження на кшталт {tenant:int} і перенесення параметрів ближче до кінця шаблону.

Компроміси

  • ShortCircuit() і MapShortCircuit (.NET 8+) виконують endpoint одразу після маршрутизації й пропускають решту middleware. Це заощаджує роботу на health checks і robots.txt, але несумісне з [Authorize] і CORS.
  • Власний термінальний middleware замість endpoint’а позбавляє авторизацію та CORS метаданих: інтеграцію доведеться писати вручну.

Як перевірити в продакшені

  • Логи категорії Microsoft.AspNetCore.Routing на рівні Debug показують кандидатів і вибраний endpoint.
  • Метрики .NET 8+: aspnetcore.routing.match_attempts з ознакою успіху й тег http.route у http.server.request.duration — затримка за шаблоном маршруту, а не за сирим URL.
  • Повний список маршрутів віддає EndpointDataSource з DI.

Підсумок: маршрутизація — єдина точка рішень: швидкий автомат, передбачуваний пріоритет і метадані, на які спираються політики.

Приклад
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.Use(async (context, next) =>
{
// Неявний UseRouting стоїть на початку: endpoint уже вибрано
var endpoint = context.GetEndpoint();
if (endpoint?.Metadata.GetMetadata<AuditAttribute>() is not null)
app.Logger.LogInformation("Audit: {Endpoint}", endpoint.DisplayName);
await next(context);
});
var orders = app.MapGroup("/orders");
orders.MapGet("/{id:int}", (int id) => $"by id {id}"); // /orders/5
orders.MapGet("/{name}", (string name) => $"by name {name}"); // /orders/abc
orders.MapGet("/new", () => "new order form") // літерал сильніший за параметри
.WithMetadata(new AuditAttribute());
app.MapGet("/robots.txt", () => "User-agent: *").ShortCircuit(); // middleware вище не виконається
app.Run();
sealed class AuditAttribute : Attribute { }
Джерела
Senior+

In-process і out-of-process в IIS: як працює ASP.NET Core Module і що обрати?

Section titled “In-process і out-of-process в IIS: як працює ASP.NET Core Module і що обрати?”
Відповідь

Що це

ASP.NET Core Module — модуль IIS з .NET Hosting Bundle. Він пов’язує IIS і застосунок одним із двох способів:

  • in-process — застосунок завантажується просто в w3wp.exe, замість Kestrel працює IISHttpServer;
  • out-of-process — ANCM запускає dotnet.exe або MyApp.exe з Kestrel і пересилає туди запити.

Як користуватися

  1. Установіть на сервер .NET Hosting Bundle: рантайм і ANCM.
  2. Створіть пул з .NET CLR version = No Managed Code.
  3. Задайте модель у .csproj: <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel> (за замовчуванням) або OutOfProcess. Публікація перенесе її в web.config.

Типова помилка

Покласти два in-process-застосунки в один пул. Другий не запуститься з помилкою 500.35: кожному застосунку потрібен свій пул.

Як влаштовано

In-process Out-of-process
Процес w3wp.exe dotnet.exe або MyApp.exe
Сервер IISHttpServer Kestrel на випадковому порту
Шлях запиту HTTP.sys → IIS → застосунок HTTP.sys → IIS → loopback → Kestrel
Падіння застосунку падає w3wp, IIS запускає новий ANCM перезапускає процес
Помилка старту 500.30 502.5

Коли що обирати

  • In-process — за замовчуванням: швидше, немає зайвого процесу й порту.
  • Out-of-process — публікація в один файл (in-process її не підтримує, помилка 500.38) або однакова поведінка Kestrel в IIS та інших середовищах.

Пастки

  • Розрядність пулу (Enable 32-Bit Applications) має збігатися з розрядністю застосунку й рантайму.
  • Ліміт IIS maxAllowedContentLength (30 000 000 байтів за замовчуванням) перевіряється раніше, ніж MaxRequestBodySize застосунку.
  • requestTimeout (за замовчуванням 2 хвилини) діє лише в out-of-process: довше ANCM на відповідь не чекає.
  • DLL застосунку, що працює, заблоковані. Для деплою кладуть app_offline.htm або вмикають shadow copy (.NET 7+).

Правило

In-process за замовчуванням і один пул на застосунок. Out-of-process — лише з конкретною причиною.

Під капотом

  • ANCM — це прошарок aspnetcorev2.dll у конвеєрі IIS та обробник для вибраної моделі.
  • In-process: обробник завантажує CoreCLR у w3wp.exe і викликає Main застосунку. UseIIS реєструє IISHttpServer, який отримує запити з нативного конвеєра IIS без сокетів.
  • Out-of-process: aspnetcorev2_outofprocess.dll запускає процес з ASPNETCORE_PORT і ASPNETCORE_TOKEN. Кожен запит несе заголовок MS-ASPNETCORE-TOKEN, а middleware IIS-інтеграції відхиляє запити без нього й вмикає forwarded headers.

Ціна

  • Out-of-process: другий розбір HTTP, з’єднання через loopback і окремий процес із власною пам’яттю.
  • In-process: застосунок ділить процес з IIS, і його падіння — це падіння w3wp.
  • Рециклінг пулу (за замовчуванням раз на 1740 хвилин) і зупинка після 20 хвилин простою означають холодний старт: JIT, прогрівання кешів, нові з’єднання.

Компроміси

  • Overlapped recycle піднімає новий w3wp до зупинки старого. Дві копії застосунку працюють одночасно, і фонові задачі, блокування файлів та in-memory-кеші мають це переживати.
  • Під час рециклінгу ANCM чекає shutdownDelay (за замовчуванням 1 секунда), потім хост витрачає до ShutdownTimeout на поточні запити. Нульовий простій не гарантують ні overlapped recycle, ні shadow copy — для цього потрібен blue-green.
  • Холодний старт прибирають Application Initialization з preloadEnabled, startMode="AlwaysRunning" і idle timeout, що дорівнює 0.

Як перевірити в продакшені

  • Журнал подій Windows, джерело «IIS AspNetCore Module V2».
  • Тимчасово stdoutLogEnabled="true" або налагоджувальний лог ANCM: debugLevel і debugFile у handlerSettings.
  • Дамп знімають з w3wp.exe для in-process і з dotnet.exe для out-of-process; до того самого процесу підключають dotnet-counters.

Підсумок: in-process дає максимум продуктивності ціною спільної з IIS долі процесу, тому рециклінг і деплой планують під це.

Налаштування IIS з боку застосунку
var builder = WebApplication.CreateBuilder(args);
// In-process: ліміти IISHttpServer (ліміт IIS maxAllowedContentLength перевіряється раніше)
builder.Services.Configure<IISServerOptions>(o => o.MaxRequestBodySize = 50_000_000);
var app = builder.Build();
// w3wp — in-process, dotnet або ім'я застосунку — out-of-process
app.MapGet("/host", () => System.Diagnostics.Process.GetCurrentProcess().ProcessName);
app.Run();
Модель хостингу та web.config
<!-- .csproj: модель хостингу (InProcess — за замовчуванням) -->
<PropertyGroup>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
<!-- web.config, який створює публікація -->
<aspNetCore processPath="dotnet" arguments=".\MyApp.dll" hostingModel="inprocess"
stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout">
<handlerSettings>
<!-- пауза перед зупинкою старого екземпляра під час рециклінгу, мс -->
<handlerSetting name="shutdownDelay" value="5000" />
<!-- деплой без блокування DLL, .NET 7+ -->
<handlerSetting name="enableShadowCopy" value="true" />
<handlerSetting name="shadowCopyDirectory" value="../ShadowCopy/" />
</handlerSettings>
</aspNetCore>