В чем разница между AddTransient, AddScoped и AddSingleton в ASP.NET Core?
Краткая суть различий
Эти три метода определяют время жизни (lifetime) создаваемых объектов. Проще всего запомнить так:
- Transient — каждый раз новый объект (одноразовый).
- Scoped — один объект на один HTTP-запрос.
- Singleton — один объект на всё приложение (один раз при запуске).
Подробное объяснение с примерами
Давайте разберем ваш пример с RandomService на практике. Представьте, что у нас есть контроллер, который использует этот сервис дважды в одном запросе:
public class TestController : Controller
{
private readonly IRandomService _service1;
private readonly IRandomService _service2;
public TestController(IRandomService service1, IRandomService service2)
{
_service1 = service1;
_service2 = service2;
}
public IActionResult Index()
{
ViewBag.Num1 = _service1.GetRandomNumber();
ViewBag.Num2 = _service2.GetRandomNumber();
return View();
}
}
1. AddTransient (каждый раз новый)
services.AddTransient<IRandomService, RandomService>();
При каждом обращении к сервису создается новый экземпляр. Даже в рамках одного HTTP-запроса, если вы запрашиваете сервис дважды — получите два разных объекта с разными числами. ViewBag.Num1 и ViewBag.Num2 будут разными.
Когда использовать: для легковесных сервисов без состояния (stateless), которые не хранят данные между вызовами. Например, сервисы для отправки email, хеширования паролей, логирования.
2. AddScoped (один на запрос)
services.AddScoped<IRandomService, RandomService>();
В рамках одного HTTP-запроса создается один экземпляр. Если вы запрашиваете сервис дважды в одном запросе — получите один и тот же объект с одним числом. ViewBag.Num1 и ViewBag.Num2 будут одинаковыми. Но при новом запросе (например, обновлении страницы) создастся новый экземпляр с другим числом.
Когда использовать: для сервисов, которые должны разделять состояние в рамках одного запроса. Например, DbContext в Entity Framework Core, работа с текущим пользователем (IHttpContextAccessor), Unit of Work.
3. AddSingleton (один на всё приложение)
services.AddSingleton<IRandomService, RandomService>();
Создается один экземпляр при первом запросе (или при запуске, если указать AddSingleton<IRandomService, RandomService>(new RandomService())). Этот экземпляр живет, пока работает приложение. Все запросы и все пользователи получают один и тот же объект. В вашем примере число будет одинаковым для всех вызовов и всех запросов.
Когда использовать: для сервисов, которые должны быть общими для всего приложения и не зависеть от запроса. Например, кэширование данных, конфигурация, логирование (если оно не должно менять контекст), сервисы для работы с внешними API (один HttpClient).
Важные предостережения
- Не используйте Singleton для Scoped-сервисов. Если Singleton-сервис зависит от Scoped-сервиса, то Scoped-сервис станет фактически Singleton (потому что будет захвачен в замыкание). Это частая ошибка — "captive dependency".
- Не используйте Transient для тяжелых объектов. Если создание объекта дорогое (например, загрузка больших данных), Transient создаст его много раз, что убьет производительность.
- Singleton не потокобезопасен по умолчанию. Если Singleton хранит изменяемое состояние, нужно синхронизировать доступ (lock, ConcurrentDictionary и т.д.).
Итоговая таблица
- Transient: Новый объект каждый раз. Для легковесных stateless-сервисов.
- Scoped: Один объект на запрос. Для сервисов с состоянием в рамках запроса.
- Singleton: Один объект на всё приложение. Для общих, потокобезопасных сервисов.
Дополнение: как проверить время жизни на практике
Чтобы наглядно убедиться в разнице, добавьте в ваш сервис вывод времени создания и уникальный ID:
public class RandomService : IRandomService
{
private readonly int _number;
private readonly Guid _id;
public RandomService()
{
_id = Guid.NewGuid();
_number = new Random().Next(1, 1000);
Console.WriteLine($"[{_id}] Создан в {DateTime.Now:HH:mm:ss.fff}");
}
public int GetRandomNumber() => _number;
public Guid GetId() => _id;
}
Теперь в контроллере выведите ID обоих экземпляров. Вы увидите:
- Transient: разные GUID даже в одном запросе.
- Scoped: одинаковые GUID в одном запросе, разные — в разных.
- Singleton: одинаковые GUID всегда.
Еще один важный нюанс: если вы регистрируете сервис как Singleton, но передаете его в контроллер через конструктор, то контроллер сам создается как Scoped (по умолчанию). Это нормально — контроллеры живут один запрос, а Singleton живет вечно. Проблемы начинаются, когда вы внедряете Scoped-сервис в Singleton — этого делать нельзя.
Для сложных сценариев используйте фабрики или делегаты при регистрации, например:
services.AddScoped<IMyService>(provider =>
{
var config = provider.GetRequiredService<IConfiguration>();
return new MyService(config["SomeKey"]);
});
Это дает полный контроль над созданием объекта.