AddTransient, AddScoped, AddSingleton: различия в C# с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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
Количество экземпляров Каждый запрос — новый Один на запрос Один на всё приложение
Когда создаётся При каждом внедрении Один раз за запрос При первом запросе или старте
Потокобезопасность Обычно не нужна Не нужна (один поток на запрос) Обязательна!