Программирование

AddTransient vs AddScoped vs AddSingleton в ASP.NET Core

Разница между AddTransient, AddScoped и AddSingleton в ASP.NET Core — когда использовать каждый lifetime, влияние на DbContext, потокобезопасность и Dispose.

В чем разница между временными существованиями (lifetimes) сервисов AddTransient, AddScoped и AddSingleton в внедрении зависимостей ASP.NET Core?

Я реализую внедрение зависимостей в ASP.NET Core и добавил следующий код в метод ConfigureServices:

csharp
public void ConfigureServices(IServiceCollection services)
{
 // Add framework services.

 // Add application services.
 services.AddTransient<IEmailSender, AuthMessageSender>();
 services.AddScoped<IEmailSender, AuthMessageSender>();
}

Оба подхода работают, но мне нужно понять:

  • В чем разница между временными существованиями сервисов AddTransient и AddScoped?
  • Когда следует использовать каждое из временных существований сервисов?
  • Как эти временные существования влияют на поведение моего приложения?
  • А что насчет AddSingleton (упомянутого в заголовке, но не в коде)?

В ASP.NET Core при внедрении зависимостей AddTransient, AddScoped и AddSingleton задают разные lifetimes: AddTransient создаёт новый экземпляр при каждом запросе на создание, AddScoped — один экземпляр на одну область (в веб‑приложении это обычно HTTP‑запрос), а AddSingleton — один экземпляр на весь жизненный цикл приложения. В вашем фрагменте кода последняя регистрация для IEmailSender (AddScoped) будет определять поведение при обычной инъекции — первая строка фактически лишняя; выберите одно lifetime по назначению сервиса.


Содержание


Что такое AddTransient, AddScoped и AddSingleton в ASP.NET Core

  • AddTransient — контейнер создаёт новый экземпляр сервиса при каждом запросе на разрешение (при каждом внедрении). Подходит для лёгких, немутирующих (stateless) утилит и помощников. Подробнее — пример и объяснение на Habr и C# Corner.
  • AddScoped — контейнер создаёт один экземпляр в пределах одной области (IServiceScope). В типичном веб‑приложении область создаётся для каждого HTTP‑запроса, значит Scoped — «один экземпляр на запрос». Это стандартный выбор для EF Core DbContext и unit‑of‑work. См. пример на Metanit.
  • AddSingleton — контейнер хранит единственный экземпляр на весь процесс приложения (создаётся либо при регистрации, либо при первом запросе, в зависимости от способа регистрации). Singleton хранит состояние дольше всех — потому он должен быть потокобезопасным. Подробнее на C# Corner и в практических заметках CodeWithMukesh.

Коротко: transient = new на каждый вызов, scoped = один на scope/запрос, singleton = один на приложение.


AddTransient vs AddScoped — практическая разница

Как это выглядит в коде и на практике? Представьте сервис с Guid, который создаётся в конструкторе — так видно, когда экземпляры разные, а когда одни и те же:

csharp
public interface IRandomId { Guid Id { get; } }

public class RandomId : IRandomId
{
 public Guid Id { get; } = Guid.NewGuid();
}

Регистрация и контроллер:

csharp
// В ConfigureServices:
services.AddTransient<IRandomId, RandomId>();
// или
services.AddScoped<IRandomId, RandomId>();

// Контроллер:
public class TestController : Controller
{
 private readonly IRandomId _a;
 private readonly IRandomId _b;
 public TestController(IRandomId a, IRandomId b) { _a = a; _b = b; }

 [HttpGet("/ids")]
 public IActionResult Get() => Ok(new { a = _a.Id, b = _b.Id });
}

Поведение:

  • При AddTransient — _a.Id и _b.Id будут разными (каждое внедрение = новый экземпляр).
  • При AddScoped — в рамках одного HTTP‑запроса оба будут одинаковыми (один экземпляр на scope), но в другом запросе Guid поменяется.
  • При AddSingleton — Guid останется одинаковым для всех запросов до перезапуска приложения.

Transient даёт изоляцию на каждое внедрение, Scoped — консистентность в пределах запроса. Это влияет на состояние, тестируемость и расход памяти (много transient‑экземпляров = больше аллокаций).


Когда использовать AddTransient, AddScoped и AddSingleton

Рекомендации по выбору (обобщённо — ориентируйтесь на зависимости и характер состояния):

  • AddTransient
  • Лёгкие, stateless‑сервисы: форматтеры, валидаторы, небольшие помощники.
  • Когда каждый вызов должен иметь «чистый» экземпляр.
  • AddScoped
  • EF Core DbContext, репозитории, unit‑of‑work: объект, который должен жить ровно в рамках одного запроса.
  • Сервисы, которые собирают или кэшируют данные в течение одного HTTP‑запроса.
  • AddSingleton
  • Кэш в памяти (MemoryCache), провайдеры конфигурации, тяжёлые ресурсы, доступные всем потокам.
  • Только если сервис потокобезопасен и не хранит per‑request state.

Если ваш IEmailSender не удерживает состояние между вызовами и не зависит от DbContext, его можно зарегистрировать Transient. Если он зависит от Scoped‑сервисов (например, DbContext) — сделайте его Scoped (или Transient, но не Singleton). Читайте практические рекомендации на CodeWithMukesh и примеры на C# Corner.


Как lifetimes влияют на поведение приложения (память, Dispose, потокобезопасность)

  • Память и GC: transient создаёт больше кратковременных объектов — при высоких нагрузках это заметно. Scoped сокращает число аллокаций внутри запроса; Singleton минимизирует аллокации, но держит объекты в памяти дольше.
  • Dispose и IDisposable: контейнер автоматически вызывает Dispose для объектов, которые он создал:
  • Scoped: Dispose при завершении области (в веб‑приложении — при конце HTTP‑запроса).
  • Singleton: Dispose при завершении приложения (когда провайдер сервисов уничтожается).
  • Transient: если transient создаётся контейнером в рамках scope, он будет освобождён при завершении scope; если вы создаёте экземпляр вручную — сами отвечаете за Dispose.
    Подробности по поведению — в наглядных примерах на Metanit.
  • Потокобезопасность: Singleton используется многими потоками одновременно — внутренние поля должны быть защищены (locks, concurrent collections и т.п.). Scoped безопасен в контексте одного запроса, transient — изолирован по внедрению.
  • Lifetime‑mismatch: если Singleton зависит от Scoped, получится «пойманный» scoped‑экземпляр или неожиданные ошибки (при включённой валидации scope контейнер может выбросить исключение). Для фоновых задач используйте явное создание scope через CreateScope() — пример ниже. См. обсуждение расширенных сценариев на Andrew Lock.

Разбор вашего кода: две регистрации IEmailSender

Вы привели:

csharp
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddScoped<IEmailSender, AuthMessageSender>();

Последняя строка определяет, какой экземпляр будет предоставлен при обычной инъекции IEmailSender — встроенный контейнер по умолчанию использует последнюю регистрацию для одиночного разрешения сервиса; если же вы разрешаете IEnumerable, вы получите все зарегистрированные реализации. Про это можно прочитать в дискуссиях и практических заметках по DI, например на StackOverflow и в обучающих статьях (см. Habr). Практический вывод: держите одну, соответствующую назначению сервиса; две строки обычно лишние и вводят в заблуждение.

Если вы хотите тестировать оба варианта — регистрируйте разные реализации или используйте фабрику/условную регистрацию, но не записывайте одну и ту же реализацию в двух lifetimes одновременно.


Частые ошибки и anti‑patterns и как их исправлять

  • Регистрация DbContext как Singleton — приведёт к ошибкам и конкурентному доступу к данным. DbContext — Scoped.
  • Singleton, который напрямую зависит от Scoped: это либо захватывает один scoped‑экземпляр, либо приведёт к ошибке при валидации; решение — создавать scope вручную или менять архитектуру.
  • Хранение per‑request данных в Singleton (например, userId в поле) — приводит к утечкам и подслушиванию данных между запросами.
  • Игнорирование IDisposable: если вы вручную new IDisposable внутри Singleton/Scoped — не забывайте освобождать.
  • Для фоновых сервисов (IHostedService) всегда создавайте scope для доступа к Scoped‑сервисам:
csharp
using(var scope = _serviceProvider.CreateScope())
{
 var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
 // работа с db
}

Подробные советы и расширенные сценарии — в блоге Andrew Lock.


Короткие примеры кода и тесты поведения

Пример регистрации с фабрикой (разница в момент создания):

csharp
// Экземпляр создаётся прямо при регистрации:
var instance = new HeavyService();
services.AddSingleton<IHeavy, HeavyService>(sp => instance);

// Экземпляр создаётся при первом запросе:
services.AddSingleton<ICache>(sp =>
{
 var cfg = sp.GetRequiredService<IConfiguration>();
 return new Cache(cfg["Size"]);
});

Пример правильного выбора для IEmailSender:

  • Если AuthMessageSender не хранит состояние и не зависит от Scoped‑сервисов — можно Transient.
  • Если он использует DbContext или другие Scoped‑сервисы — сделайте Scoped.
  • Если он обязан быть единственным и потокобезопасным (например, держит соединение), можно Singleton, но проверяйте потокобезопасность и не храните per‑request данные.

Тест: залогируйте Guid в конструкторе и смотрите, как меняется значение между запросами и внутри запроса (см. пример RandomId выше).


Источники


Заключение

Внедрение зависимостей в ASP.NET Core — это управление lifetime: AddTransient даёт новый экземпляр при каждом внедрении, AddScoped даёт один экземпляр на область (обычно HTTP‑запрос), AddSingleton — один экземпляр на приложение. Выберите lifetime по характеру сервиса и его зависимостей: для DbContext — Scoped, для лёгких утилит — Transient, для кэша и глобальных провайдеров — Singleton (только потокобезопасно). В вашем конкретном коде оставьте одну регистрацию для IEmailSender — та, которая соответствует реальным зависимостям и поведению сервиса.

Авторы
Проверено модерацией
Модерация
AddTransient vs AddScoped vs AddSingleton в ASP.NET Core