AddTransient, AddScoped и AddSingleton: Как не запутаться в жизненных циклах сервисов C#
Вы когда-нибудь ловили себя на мысли: «Почему мой сервис сохраняет данные между запросами, хотя я этого не просил?» Или наоборот — «Почему каждый раз создаётся новый объект, и я теряю состояние?»
Добро пожаловать в мир внедрения зависимостей (Dependency Injection, DI) в ASP.NET Core! Это одна из самых мощных и одновременно запутанных тем для новичков. Сегодня мы разберём три кита, на которых держится управление объектами в C#: AddTransient, AddScoped и AddSingleton.
К концу этой статьи вы не только поймёте разницу, но и сможете уверенно отвечать на этот вопрос на собеседовании (да, его обожают задавать!). Поехали!
Что такое жизненный цикл сервиса и зачем он нужен?
Представьте, что ваше приложение — это ресторан. Сервисы — это повара, официанты и посуда. Жизненный цикл определяет, как долго они «живут»:
- Transient (Транзитный) — одноразовый сотрудник. Пришёл, сделал одну тарелку супа и ушёл. Новый заказ — новый сотрудник.
- Scoped (В области видимости) — сотрудник на одну смену. Обслуживает всех клиентов, пока ресторан открыт (пока длится HTTP-запрос).
- Singleton (Одиночка) — шеф-повар, который работает всю жизнь ресторана. Один на всё приложение.
В C# эта логика реализуется через встроенный контейнер DI. Давайте посмотрим на код.
1. AddTransient — каждый раз новый экземпляр
Когда использовать: Лёгкие сервисы без состояния (stateless), которые не хранят данные между вызовами. Идеально для репозиториев, хелперов, утилит.
Как работает: Каждый раз, когда вы запрашиваете сервис (через конструктор или напрямую из контейнера), создаётся новый объект.
// Регистрация в Program.cs
builder.Services.AddTransient<IMyService, MyService>();
// Пример сервиса
public interface IMyService
{
Guid GetInstanceId();
}
public class MyService : IMyService
{
private readonly Guid _id = Guid.NewGuid();
public Guid GetInstanceId() => _id;
}
// Контроллер, который использует сервис
public class HomeController : Controller
{
private readonly IMyService _service1;
private readonly IMyService _service2;
public HomeController(IMyService service1, IMyService service2)
{
_service1 = service1;
_service2 = service2;
}
public IActionResult Index()
{
ViewBag.Id1 = _service1.GetInstanceId();
ViewBag.Id2 = _service2.GetInstanceId();
// Id1 и Id2 — РАЗНЫЕ GUID!
return View();
}
}
Важно: Даже в рамках одного HTTP-запроса, если вы внедряете IMyService в два разных места, вы получите два разных объекта. Память расходуется, но зато нет риска «загрязнения» данных.
2. AddScoped — один экземпляр на запрос
Когда использовать: Сервисы, которые должны быть согласованы в рамках одного HTTP-запроса. Например, DbContext в Entity Framework Core, сервисы работы с корзиной покупок, логгеры запросов.
Как работает: В пределах одного HTTP-запроса (или scope) создаётся один экземпляр. Если вы запрашиваете его несколько раз — получаете один и тот же объект. Но каждый новый запрос от другого пользователя получит свой экземпляр.
// Регистрация
builder.Services.AddScoped<IScopedService, ScopedService>();
public interface IScopedService
{
Guid GetInstanceId();
}
public class ScopedService : IScopedService
{
private readonly Guid _id = Guid.NewGuid();
public Guid GetInstanceId() => _id;
}
// В том же контроллере
public IActionResult Index()
{
var scoped1 = HttpContext.RequestServices.GetService<IScopedService>();
var scoped2 = HttpContext.RequestServices.GetService<IScopedService>();
// scoped1.GetInstanceId() == scoped2.GetInstanceId() — ОДИНАКОВЫЕ!
return View();
}
Ловушка: Никогда не внедряйте Scoped-сервис в Singleton! Это приведёт к утечке памяти и непредсказуемому поведению. Контейнер DI в ASP.NET Core выбросит исключение во время выполнения (или создаст «капкан» — singleton будет жить дольше, чем scoped).
3. AddSingleton — один на всё приложение
Когда использовать: Сервисы, которые должны быть глобальными и общими для всех пользователей. Кеши, конфигурации, логгеры (хотя логгеры обычно тоже singleton), сервисы подсчёта статистики.
Как работает: Объект создаётся один раз при первом запросе (или при старте приложения, если зарегистрировать как AddSingleton<T>(new T())) и живёт, пока работает приложение.
// Регистрация
builder.Services.AddSingleton<ISingletonService, SingletonService>();
public interface ISingletonService
{
int IncrementAndGet();
}
public class SingletonService : ISingletonService
{
private int _counter = 0;
public int IncrementAndGet() => Interlocked.Increment(ref _counter);
}
// В любом месте приложения
public IActionResult Index()
{
var singleton = HttpContext.RequestServices.GetService<ISingletonService>();
ViewBag.Count = singleton.IncrementAndGet(); // 1, 2, 3... при каждом обновлении
return View();
}
Внимание: Singleton-сервисы должны быть потокобезопасными! Если несколько потоков одновременно вызывают методы — используйте блокировки (lock) или атомарные операции (Interlocked).
Сравнительная таблица (шпаргалка)
| Характеристика | Transient | Scoped | Singleton |
|---|---|---|---|
| Количество экземпляров | Каждый запрос — новый | Один на запрос | Один на всё приложение |
| Когда создаётся | При каждом внедрении | Один раз за запрос | При первом запросе или старте |
| Потокобезопасность | Обычно не нужна | Не нужна (один поток на запрос) | Обязательна! |