ASP.NET Core
ASP.NET Core устроен проще, чем кажется по обилию атрибутов. Веб-сервер Kestrel разбирает сокет в объект HttpContext, хост открывает для запроса свой DI-scope, а дальше контекст просто протаскивается через цепочку делегатов — конвейер middleware. Каждый компонент видит запрос по пути вниз, вызывает next и видит ответ на обратном пути вверх. Endpoint (контроллер или Minimal API) — последнее звено этой цепочки, а не какая-то отдельная сущность рядом с ней.
Из этой конструкции растут все ловушки темы, и все они — про порядок и про время жизни. Порядок регистрации middleware семантичен: UseExceptionHandler ловит только то, что зарегистрировано ниже него, UseAuthentication обязан идти до UseAuthorization, а терминальный компонент не зовёт next — и всё, что после него, просто не выполняется. Время жизни сервиса определяет, сколько живёт захваченная им ссылка: Singleton, принявший в конструктор Scoped DbContext, держит его до конца процесса — это captive dependency, и в продакшене она даёт порчу состояния между запросами и одновременное использование DbContext из параллельных запросов. Конфигурация складывается стопкой провайдеров, где последний перекрывает предыдущих, — поэтому переменная окружения выигрывает у appsettings.json, а не наоборот. Разбор по слоям — ниже.
Карта темы
- Конвейер middleware — цепочка делегатов, две фазы вокруг
next, короткое замыкание и семантика порядка. - Внедрение зависимостей —
IServiceCollectionзамерзает наBuild(), конструкторное внедрение, scope запроса и его Dispose. - Времена жизни сервисов —
Transient/Scoped/Singletonи captive dependency как главный баг темы. - Приоритет конфигурации — стопка провайдеров, победа последнего, переменные окружения против
appsettings.json, секреты. - Обработка исключений —
UseExceptionHandlerнаверху конвейера,ProblemDetails,IExceptionHandler. - Health-checks — liveness против readiness и почему их нельзя путать в оркестраторе.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Регистрировать UseExceptionHandler в конце конвейера | Он оборачивает только то, что ниже него — исключения из routing и аутентификации до него не доходят, клиент получает голый 500 |
Ждать, что конвейер продолжится без вызова next | Компонент, не вызвавший next, терминальный — всё, что зарегистрировано после, молча не выполняется |
Ставить UseAuthorization до UseAuthentication или до UseRouting | В первом случае HttpContext.User ещё пуст, во втором нет метаданных endpoint — политика проверяет пустоту |
Писать в ответ после next, когда заголовки уже отправлены | Response.HasStarted уже true — попытка изменить статус или заголовок бросает исключение |
Добавлять регистрации в builder.Services после Build() | Коллекция заморожена — InvalidOperationException; сервис в контейнер не попадёт |
Внедрять Scoped DbContext в Singleton | Captive dependency — один DbContext на весь процесс: раздувшийся change tracker, состояние первого запроса во всех следующих и параллельное использование не потокобезопасного объекта |
Разрешать Scoped прямо из корневого провайдера в фоновой задаче | Экземпляр живёт до остановки приложения и никогда не диспозится по запросу — нужна IServiceScopeFactory |
Считать, что appsettings.json всегда главнее | Провайдеры складываются стопкой, побеждает последний: переменная окружения перекрывает JSON, аргументы командной строки — переменную окружения |
Класть секрет в базовый appsettings.json | Файл коммитится — ключ подписи или пароль от БД уезжает в репозиторий; секретам место в user secrets и переменных окружения |
Записывать вложенность в имени переменной окружения через : | На части платформ такое имя недопустимо — вложенность кодируется двойным подчёркиванием __ |
| Проверять базу данных в liveness-пробе | Кратковременный сбой БД превращается в перезапуск всех реплик — зависимости проверяют в readiness |
| Считать liveness и readiness одной проверкой | Медленная зависимость перезапускает здоровый процесс вместо того, чтобы просто убрать его из балансировщика |
Значение для собеседований
Эту тему спрашивают у всех, кто заявил backend на C#, и почти всегда одинаково: интервьюер даёт конкретный симптом и смотрит, есть ли у вас модель конвейера и контейнера, или только память об именах методов. «Почему UseAuthorization ничего не запрещает?», «Почему логи молчат, а клиент видит 500?», «Почему на проде утекает память, а локально нет?» — все три ответа лежат в одной плоскости: порядок регистрации и время жизни.
Что обычно проверяют:
- Что такое middleware-компонент и что именно делает вызов
next— включая фазу ответа на обратном пути. - Почему порядок регистрации семантичен, и какой порядок канонический: обработчик исключений → routing → аутентификация → авторизация → endpoint.
- Когда строится
IServiceProvider, откуда разрешаются сервисы запроса и кто их диспозит. - Разницу
Transient/Scoped/Singletonи что такое captive dependency — с механикой, а не с определением. - Какой провайдер конфигурации победит для одного ключа и где хранить секрет.
- Зачем контейнеризованному сервису health-check и чем liveness отличается от readiness по последствиям.
Типичный неверный ответ: «Времена жизни — это просто про то, сколько объектов создаётся; на поведение не влияет». Влияет: захват Scoped-сервиса синглтоном не создаёт «лишний объект», а фиксирует ссылку один раз на весь процесс. Кандидат, который сам доводит эту мысль до «а DbContext не потокобезопасен, значит параллельные запросы получат A second operation was started on this context instance», закрывает вопрос про времена жизни целиком.