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:
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 vs AddScoped — практическая разница
- Когда использовать AddTransient, AddScoped и AddSingleton
- Как lifetimes влияют на поведение приложения (память, Dispose, потокобезопасность)
- Разбор вашего кода: две регистрации IEmailSender
- Частые ошибки и anti‑patterns и как их исправлять
- Короткие примеры кода и тесты поведения
- Источники
- Заключение
Что такое 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, который создаётся в конструкторе — так видно, когда экземпляры разные, а когда одни и те же:
public interface IRandomId { Guid Id { get; } }
public class RandomId : IRandomId
{
public Guid Id { get; } = Guid.NewGuid();
}
Регистрация и контроллер:
// В 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
Вы привели:
services.AddTransient<IEmailSender, AuthMessageSender>(); services.AddScoped<IEmailSender, AuthMessageSender>();
Последняя строка определяет, какой экземпляр будет предоставлен при обычной инъекции IEmailSender — встроенный контейнер по умолчанию использует последнюю регистрацию для одиночного разрешения сервиса; если же вы разрешаете IEnumerable
Если вы хотите тестировать оба варианта — регистрируйте разные реализации или используйте фабрику/условную регистрацию, но не записывайте одну и ту же реализацию в двух lifetimes одновременно.
Частые ошибки и anti‑patterns и как их исправлять
- Регистрация DbContext как Singleton — приведёт к ошибкам и конкурентному доступу к данным. DbContext — Scoped.
- Singleton, который напрямую зависит от Scoped: это либо захватывает один scoped‑экземпляр, либо приведёт к ошибке при валидации; решение — создавать scope вручную или менять архитектуру.
- Хранение per‑request данных в Singleton (например, userId в поле) — приводит к утечкам и подслушиванию данных между запросами.
- Игнорирование IDisposable: если вы вручную new IDisposable внутри Singleton/Scoped — не забывайте освобождать.
- Для фоновых сервисов (IHostedService) всегда создавайте scope для доступа к Scoped‑сервисам:
using(var scope = _serviceProvider.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// работа с db
}
Подробные советы и расширенные сценарии — в блоге Andrew Lock.
Короткие примеры кода и тесты поведения
Пример регистрации с фабрикой (разница в момент создания):
// Экземпляр создаётся прямо при регистрации:
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 выше).
Источники
- https://habr.com/ru/companies/otus/articles/539762/
- https://www.c-sharpcorner.com/article/understanding-addtransient-vs-addscoped-vs-addsingleton-in-asp-net-core/
- https://stackoverflow.com/questions/38138100/addtransient-addscoped-and-addsingleton-services-differences
- https://metanit.com/sharp/aspnet5/6.2.php
- https://www.tutorialspoint.com/what-is-the-addsingleton-vs-addscoped-vs-add-transient-chash-asp-net-core
- https://medium.com/@developerstory/addsingleton-vs-addtransient-vs-addscoped-in-net-core-9a936147c72e
- https://codewithmukesh.com/blog/when-to-use-transient-scoped-singleton-dotnet/
- https://andrewlock.net/going-beyond-singleton-scoped-and-transient-lifetimes/
Заключение
Внедрение зависимостей в ASP.NET Core — это управление lifetime: AddTransient даёт новый экземпляр при каждом внедрении, AddScoped даёт один экземпляр на область (обычно HTTP‑запрос), AddSingleton — один экземпляр на приложение. Выберите lifetime по характеру сервиса и его зависимостей: для DbContext — Scoped, для лёгких утилит — Transient, для кэша и глобальных провайдеров — Singleton (только потокобезопасно). В вашем конкретном коде оставьте одну регистрацию для IEmailSender — та, которая соответствует реальным зависимостям и поведению сервиса.