Какой подход создания Singleton выбрать при реализации DI контейнера: при старте приложения или при первом обращении?
Что проверяют
Отличаете ли вы синглтон как паттерн от времени жизни в контейнере и видите ли цену каждого варианта: время старта, момент появления ошибки, потокобезопасность. Следом обычно спрашивают, как поступает Microsoft.Extensions.DependencyInjection и как прогреть сервис заранее.
Как отвечать
Для своего контейнера разумный ответ: ленивое создание по умолчанию и явная опция для жадного. Так работает и стандартный контейнер .NET: AddSingleton<IFoo, Foo>() создаёт экземпляр при первом разрешении, а не при старте.
Сравнение:
- при старте: ошибки конструктора и конфигурации всплывают сразу, до первого запроса, и первый пользователь не платит за инициализацию. Цена: дольше старт и создание сервисов, которые могут не понадобиться;
- при первом обращении: быстрый старт, платите только за используемое. Цена: медленный первый запрос и исключение из конструктора уже в рантайме, посреди чужого запроса;
- потокобезопасность: два потока могут одновременно запросить ещё не созданный сервис. Контейнер обязан гарантировать единственный экземпляр:
Lazy<T>с режимомExecutionAndPublication(он и так по умолчанию) или блокировка с двойной проверкой.
Как это делают в .NET на практике:
- прогрев: разрешить сервис в
Program.csпослеBuild()или вIHostedService.StartAsync, там же удобно делать асинхронную инициализацию; AddSingleton(instance): экземпляр создаёте вы, значит он жадный, и контейнер не вызовет у негоDisposeпри остановке;- проверки:
ValidateOnBuildиValidateScopesв среде Development включены по умолчанию и ловят ошибки регистрации и захват scoped-сервиса синглтоном.
В Autofac жадное создание включается через AutoActivate() или IStartable.
Где спотыкаются
Путают со статическим свойством Instance. В DI класс не знает, что он синглтон, это решает регистрация.
Делают асинхронную инициализацию в конструкторе через .Result или .Wait(). Это риск дедлока и заблокированный поток пула.
Внедряют scoped-сервис, например DbContext, в синглтон. Он живёт до остановки приложения и становится общим для всех запросов.