Разница между AddTransient, AddScoped и AddSingleton в ASP.NET Core: когда и что использовать?
Краткий обзор: что такое время жизни сервиса?
В ASP.NET Core время жизни сервиса определяет, как часто будет создаваться новый экземпляр класса при запросе его через DI-контейнер. Это критически важно для корректной работы приложения, производительности и избежания утечек памяти.
1. AddTransient — каждый раз новый экземпляр
Сервисы, зарегистрированные как Transient, создаются каждый раз, когда они запрашиваются из контейнера. Даже в рамках одного HTTP-запроса, если вы запросите transient-сервис три раза, вы получите три разных объекта.
// Пример: сервис для генерации уникальных ID
public class IdGenerator : IIdGenerator
{
private readonly Guid _id = Guid.NewGuid();
public Guid GetId() => _id;
}
// Регистрация
services.AddTransient<IIdGenerator, IdGenerator>();
// В контроллере (запрос 1)
public class TestController : Controller
{
public IActionResult Index([FromServices] IIdGenerator gen1, [FromServices] IIdGenerator gen2)
{
// gen1.Id != gen2.Id — разные GUID!
return Ok();
}
}
Когда использовать:
- Легковесные, не имеющие состояния (stateless) сервисы.
- Сервисы, которые хранят временные данные (например, DTO-мапперы).
- Когда важна потокобезопасность — каждый поток получает свой экземпляр.
Подводные камни:
- Создание тяжелых объектов (например, с подключением к БД) как Transient приведет к снижению производительности.
- Если transient-сервис внедрен в singleton, он станет "захваченным" (captive dependency) и будет жить вечно, как singleton. Это частая ошибка!
2. AddScoped — один экземпляр на HTTP-запрос
Сервисы с временем жизни Scoped создаются один раз для каждого HTTP-запроса (или для каждой области — scope). В рамках одного запроса все обращения к scoped-сервису вернут один и тот же экземпляр.
// Сервис для работы с корзиной
public class CartService : ICartService
{
private readonly List<Item> _items = new();
public void AddItem(Item item) => _items.Add(item);
public IEnumerable<Item> GetItems() => _items;
}
// Регистрация
services.AddScoped<ICartService, CartService>();
// В контроллере
public class CartController : Controller
{
private readonly ICartService _cart;
public CartController(ICartService cart) => _cart = cart;
public IActionResult Add(Item item)
{
_cart.AddItem(item); // Работаем с той же корзиной в рамках запроса
return Ok();
}
}
Когда использовать:
- Сервисы, которые должны сохранять состояние в рамках одного запроса (например, корзина, Unit of Work, транзакции БД).
- DbContext в Entity Framework Core — по умолчанию регистрируется как Scoped.
- Любые сервисы, где нужна согласованность данных внутри одного запроса.
Подводные камни:
- Scoped-сервис не должен быть внедрен в singleton — это приведет к тому, что singleton получит первый экземпляр и будет использовать его вечно, игнорируя новые запросы.
- Нельзя использовать в фоновых задачах (BackgroundService) без создания собственного scope.
3. AddSingleton — один экземпляр для всего приложения
Singleton-сервисы создаются один раз при первом запросе (или при запуске, если используется AddSingleton<T>(new T())) и живут до остановки приложения.
// Конфигурация, которая не меняется
public class AppConfig
{
public string ConnectionString { get; set; }
public int MaxRetryCount { get; set; }
}
// Регистрация с готовым объектом
var config = new AppConfig { ConnectionString = "...", MaxRetryCount = 3 };
services.AddSingleton<AppConfig>(config);
// Или через фабрику
services.AddSingleton<ICacheService, MemoryCacheService>();
Когда использовать:
- Сервисы, которые не хранят состояние, зависящее от запроса (например, кэш, конфигурация, логгеры).
- Сервисы, которые должны быть общими для всего приложения (например, пул соединений с базой данных).
- Реализация паттерна "Одиночка" (Singleton) в DI-контейнере.
Подводные камни:
- Потокобезопасность! Singleton-сервис должен быть либо неизменяемым (immutable), либо правильно синхронизировать доступ к общим данным. Иначе — race conditions.
- Утечка памяти: если singleton хранит ссылки на объекты, которые больше не нужны, они не будут собраны сборщиком мусора до конца работы приложения.
- Нельзя внедрять Scoped или Transient сервисы в Singleton — это создаст "захваченные зависимости" (captive dependencies).
Какой вариант выбрать? Практические советы
- По умолчанию используйте
AddTransientдля stateless сервисов. - Для работы с БД (EF Core DbContext) —
AddScoped. - Для кэша, конфигурации, логгера —
AddSingleton. - Избегайте внедрения scoped/transient сервисов в singleton.
- Если сомневаетесь — начните с Transient. Производительность можно профилировать и оптимизировать позже.
Дополнение: как избежать ошибки "Captive Dependency"
Одна из самых частых проблем при работе с DI в ASP.NET Core — это захваченные зависимости (captive dependencies). Она возникает, когда сервис с более долгим временем жизни (например, Singleton) внедряет сервис с более коротким временем жизни (Scoped или Transient).
Пример ошибки:
public class MySingletonService
{
private readonly MyScopedService _scoped;
public MySingletonService(MyScopedService scoped)
{
_scoped = scoped; // Ошибка! Scoped захвачен Singleton
}
}
// Регистрация
services.AddSingleton<MySingletonService>();
services.AddScoped<MyScopedService>();
В этом случае MyScopedService будет создан только один раз (при создании Singleton) и будет использоваться для всех запросов, что нарушает его логику (например, DbContext будет работать с устаревшими данными).
Как исправить:
- Используйте
IServiceScopeFactoryдля создания временного scope внутри Singleton:
public class MySingletonService
{
private readonly IServiceScopeFactory _scopeFactory;
public MySingletonService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public void DoWork()
{
using (var scope = _scopeFactory.CreateScope())
{
var scopedService = scope.ServiceProvider.GetRequiredService<MyScopedService>();
scopedService.DoWork();
}
}
}
Дополнительный совет: Включите проверку в режиме разработки. В ASP.NET Core 6+
Ответов пока нет.
Войдите чтобы оставить ответ