Как реализовано создание экземпляров контроллеров в ASP.NET Core и какие правила жизни объектов применяются?
Что проверяют
Знаете ли вы, кто и когда создаёт контроллер, и понимаете ли разницу между временами жизни сервисов на практике. Следом почти всегда спрашивают про singleton, который зависит от scoped-сервиса, и что при этом случится.
Как отвечать
Контроллер создаётся заново на каждый HTTP-запрос. Этим занимается IControllerActivator: реализация по умолчанию DefaultControllerActivator строит объект через ActivatorUtilities и берёт зависимости конструктора из HttpContext.RequestServices, то есть из скоупа текущего запроса. Сами контроллеры в контейнере по умолчанию не зарегистрированы; AddControllersAsServices() регистрирует их и переключает активацию на контейнер, это нужно, например, для перехватчиков стороннего DI вроде Autofac. В конце запроса активатор вызывает у контроллера Dispose, если тот его реализует.
Времена жизни:
- Transient: новый экземпляр при каждом разрешении. Два класса в одном запросе получат два разных объекта;
- Scoped: один экземпляр на скоуп, в веб-приложении это HTTP-запрос.
AddDbContextрегистрируетDbContextименно так; - Singleton: один экземпляр на всё приложение, обязан быть потокобезопасным.
Правило совместимости: сервис не должен зависеть от сервиса с более коротким временем жизни. Singleton, захвативший scoped DbContext, будет держать его вечно и делить между потоками. В окружении Development хост включает ValidateScopes и ValidateOnBuild, и такая конфигурация падает на старте. Если singleton или BackgroundService нужен scoped-сервис, скоуп создают явно через IServiceScopeFactory.CreateScope().
Зависимость, нужная одному экшену, приходит параметром с [FromServices], а с .NET 8 есть ещё keyed-сервисы и [FromKeyedServices].
Где спотыкаются
Говорят, что Transient это «новый объект на каждый запрос». Так ведёт себя Scoped, а Transient создаётся на каждое разрешение зависимости.
Хранят состояние в полях контроллера, рассчитывая увидеть его в следующем запросе. Экземпляр живёт ровно один запрос.
Разрешают scoped-сервис из корневого провайдера, например из app.Services при старте, и получают исключение, а без валидации фактически singleton с утечкой.