В чем разница между временными существованиями (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 создает один экземпляр, который существует на протяжении всего времени жизни приложения. Ключевые различия заключаются в частоте создания экземпляров, использовании памяти, потокобезопасности и в том, как они обрабатывают службы с состоянием между несколькими запросами.
Содержание
- Понимание времени жизни службы
- Время жизни AddTransient
- Время жизни AddScoped
- Время жизни AddSingleton
- Выбор правильного времени жизни
- Распространенные ошибки и лучшие практики
- Вопросы производительности
Понимание времени жизни службы
Время жизни службы в ASP.NET Core в внедрении зависимостей контролирует, как создаются и управляются экземпляры зарегистрированных служб. Каждое время жизни служит разным целям и имеет определенные характеристики, которые делают его подходящим для конкретных сценариев.
Определение времени жизни службы: Время жизни службы определяет область и длительность существования экземпляра службы и то, как он совместно используется в разных частях вашего приложения.
Три основных времени жизни службы:
- Transient: Создается новый экземпляр при каждом запросе службы
- Scoped: Создается новый экземпляр на одну область (обычно на один HTTP-запрос)
- Singleton: Создается один экземпляр один раз и повторно используется на протяжении всего времени жизни приложения
Эти времена жизни регистрируются с помощью методов AddTransient(), AddScoped() и AddSingleton() соответственно при настройке служб в методе ConfigureServices вашего класса Startup или в Program.cs в более новых версиях.
Время жизни AddTransient
Временные службы (Transient services) создаются каждый раз, когда они запрашиваются из контейнера внедрения зависимостей. Это означает, что если одна и та же служба запрашивается несколько раз в одной области, будет создано несколько экземпляров.
Характеристики
- Создание: Новый экземпляр при каждом запросе
- Область: Нет осведомленности об области
- Использование памяти: Высокое (создается больше экземпляров)
- Потокобезопасность: Как правило безопасно (нет общего состояния между запросами)
Пример использования
// В методе ConfigureServices
services.AddTransient<IEmailSender, AuthMessageSender>();
// Использование в контроллере
public class HomeController : Controller
{
private readonly IEmailSender _emailSender1;
private readonly IEmailSender _emailSender2;
public HomeController(IEmailSender emailSender1, IEmailSender emailSender2)
{
_emailSender1 = emailSender1;
_emailSender2 = emailSender2;
}
public IActionResult Index()
{
// Это будут разные экземпляры
var instance1 = _emailSender1;
var instance2 = _emailSender2;
}
}
В этом примере _emailSender1 и _emailSender2 будут разными экземплярами, потому что временные службы создаются каждый раз, когда они внедряются.
Когда использовать AddTransient
- Легковесные службы с минимальными накладными расходами на инициализацию
- Бессостоянные службы, которым не нужно поддерживать состояние между вызовами
- Службы, которые никогда не должны совместно использоваться между разными частями вашего приложения
- Сценарии модульного тестирования, где вам нужны свежие экземпляры для каждого теста
Время жизни AddScoped
Службы с областью действия (Scoped services) создаются один раз на область клиента (запрос) и совместно используются в этой области. В веб-приложениях область обычно соответствует одному HTTP-запросу.
Характеристики
- Создание: Новый экземпляр на область (обычно на один HTTP-запрос)
- Область: Осведомленность об области (существует в текущей области)
- Использование памяти: Среднее (один экземпляр на запрос)
- Потокобезопасность: Безопасно в пределах одного запроса, но будьте осторожны с общим состоянием
Пример использования
// В методе ConfigureServices
services.AddScoped<IEmailSender, AuthMessageSender>();
// Использование в нескольких службах в одном запросе
public class OrderService
{
private readonly IEmailSender _emailSender;
public OrderService(IEmailSender emailSender)
{
_emailSender = emailSender;
}
public async Task ProcessOrderAsync(Order order)
{
await _emailSender.SendConfirmationEmail(order);
}
}
public class NotificationService
{
private readonly IEmailSender _emailSender;
public NotificationService(IEmailSender emailSender)
{
_emailSender = emailSender;
}
public async Task SendNotificationsAsync(Order order)
{
await _emailSender.SendNotificationEmail(order);
}
}
// В контроллере, вызывающем обе службы
public class OrderController : Controller
{
private readonly OrderService _orderService;
private readonly NotificationService _notificationService;
public OrderController(OrderService orderService, NotificationService notificationService)
{
_orderService = orderService;
_notificationService = notificationService;
}
public async IActionResult PlaceOrder(Order order)
{
await _orderService.ProcessOrderAsync(order);
await _notificationService.SendNotificationsAsync(order);
}
}
В этом примере и OrderService, и NotificationService получат один и тот же экземпляр IEmailSender, потому что они находятся в одной области HTTP-запроса.
Когда использовать AddScoped
- Контексты базы данных (Entity Framework DbContext) - один на запрос для обеспечения правильного управления транзакциями
- Шаблоны единицы работы (Unit of work) - службы, которым нужно поддерживать состояние в рамках одной операции
- Службы, специфичные для запроса, которым нужно совместно использовать данные между несколькими компонентами во время запроса
- Службы, которым нужно быть последовательными в рамках одной операции, но изолированными между разными запросами
Время жизни AddSingleton
Службы-одиночки (Singleton services) создаются только один раз при первом запросе, а затем повторно используются для всех последующих запросов на протяжении всего времени жизни приложения.
Характеристики
- Создание: Один экземпляр создается один раз и повторно используется
- Область: Приложение в целом (нет осведомленности об области)
- Использование памяти: Наименьшее (существует только один экземпляр)
- Потокобезопасность: Критический аспект - должен быть потокобезопасным, если используется в веб-приложениях
Пример использования
// В методе ConfigureServices
services.AddSingleton<IEmailSender, AuthMessageSender>();
// Служба конфигурации приложения в целом
services.AddSingleton<IConfigurationService, ConfigurationService>();
// Служба кэширования
services.AddSingleton<ICacheService, MemoryCacheService>();
Вопросы потокобезопасности
Для служб-одиночек в веб-приложениях вы должны обеспечить их потокобезопасность, так как они будут доступны из нескольких одновременных запросов:
public class ThreadSafeSingletonService : IThreadSafeService
{
private readonly object _lock = new object();
private Dictionary<string, object> _cache = new Dictionary<string, object>();
public void AddToCache(string key, object value)
{
lock (_lock)
{
_cache[key] = value;
}
}
public object GetFromCache(string key)
{
lock (_lock)
{
return _cache.TryGetValue(key, out var value) ? value : null;
}
}
}
Когда использовать AddSingleton
- Службы конфигурации приложения, которые не изменяются во время выполнения
- Службы ведения журнала, которые могут безопасно совместно использовать состояние между запросами
- Службы кэширования, предназначенные для совместного использования во всем приложении
- Службы с дорогой инициализацией, которые вы хотите инициализировать только один раз
- Чисто бессостоянные службы, которые не поддерживают никаких данных, специфичных для запроса
Выбор правильного времени жизни
Выбор подходящего времени жизни службы имеет решающее значение для производительности, использования памяти и корректности работы приложения. Вот структура принятия решений:
Схема принятия решений
Сохраняет ли ваша службу состояние, которое должно быть изолировано между запросами?
├── ДА → Используйте AddScoped
│ ├── Связано ли состояние с базой данных? → DbContext почти всегда должен быть scoped
│ └── Нужно ли совместно использовать его в рамках запроса? → Scoped подходит
└── НЕТ → Рассмотрите другие факторы
├── Очень ли дорого создавать службу? → Рассмотрите AddSingleton
├── Нужен ли один и тот же экземпляр во всем приложении? → Используйте AddSingleton
└── Нужен ли свежий экземпляр каждый раз? → Используйте AddTransient
Рекомендации по времени жизни для распространенных служб
| Тип службы | Рекомендуемое время жизни | Причина |
|---|---|---|
| Контекст Entity Framework | Scoped | Обеспечивает правильную изоляцию транзакций на запрос |
| Классы репозитория | Scoped | Работает с областью DbContext |
| Службы бизнес-логики | Scoped | Поддерживает состояние, специфичное для запроса |
| Службы конфигурации | Singleton | Настройки приложения в целом, которые не изменяются |
| Службы ведения журнала | Singleton | Безопасно совместно использовать во всем приложении |
| Службы HTTP-клиента | Singleton | Повторное использование соединений повышает производительность |
| Единица работы (Unit of Work) | Scoped | Координирует несколько репозиториев в рамках запроса |
| Службы кэширования | Singleton | Кэширование на уровне приложения |
| Службы проверки | Transient | Свежие правила проверки каждый раз |
Распространенные ошибки и лучшие практики
Распространенные ошибки, которых следует избегать
-
Scoped служба в Singleton
csharp// АНТИ-ШАБЛОН: Это вызовет проблемы services.AddSingleton<IMyService>(provider => { var scopedService = provider.GetRequiredService<IScopedService>(); return new MyService(scopedService); });Это создает захваченную зависимость (captive dependency), когда scoped служба захватывается singleton, что заставляет ее существовать дольше, чем предполагалось.
-
Состояние в службах-одиночках
csharp// АНТИ-ШАБЛОН: Не потокобезопасно public class BadSingletonService : IMyService { private int _counter = 0; // Общий для всех запросов! public void Increment() { _counter++; // Состояние гонки! } } -
Чрезмерное использование Transient служб для тяжелых объектов
csharp// ВОЗМОЖНАЯ ПРОБЛЕМА: Дорогие объекты создаются часто services.AddTransient<HeavyObject>();
Лучшие практики
-
Следуйте принципу цепочки зависимостей: Службы не должны зависеть от служб с более коротким временем жизни
- Singleton → Singleton ✓
- Singleton → Scoped ✗ (Захваченная зависимость)
- Singleton → Transient ✗ (Захваченная зависимость)
- Scoped → Scoped ✓
- Scoped → Transient ✓
- Transient → Transient ✓
-
Используйте Scoped для операций с базой данных
csharpservices.AddScoped<IApplicationDbContext, ApplicationDbContext>(); services.AddScoped<IProductRepository, ProductRepository>(); services.AddScoped<IOrderService, OrderService>();
-
Рассмотрите шаблон Factory для сложных зависимостей
csharpservices.AddSingleton<IComplexService>(provider => { // Создайте scoped зависимости здесь var scopedDependency = provider.CreateScope() .ServiceProvider.GetRequiredService<IScopedDependency>(); return new ComplexService(scopedDependency); });
Вопросы производительности
Влияние на память
- Singleton: Наименьший след памяти (один экземпляр)
- Scoped: Средний след памяти (один экземпляр на одновременный запрос)
- Transient: Наибольший след памяти (количество экземпляров = количество запросов)
Накладные расходы на создание
- Singleton: Создается только один раз (наибольшие начальные затраты, но амортизируемые)
- Scoped: Создается один раз на запрос (умеренные затраты на создание)
- Transient: Создается каждый раз (наибольшие совокупные затраты на создание)
Вопросы масштабируемости
Для приложений с высоким трафиком:
// Хорошо для сценариев с высоким трафиком
services.AddSingleton<IHttpClientFactory, DefaultHttpClientFactory>();
// Потенциально проблематично для высокого трафика
services.AddTransient<ExpensiveService>(); // Создает много экземпляров
Мониторинг и диагностика
ASP.NET Core предоставляет встроенную диагностику для внедрения зависимостей:
// В Program.cs
var builder = WebApplication.CreateBuilder(args);
// Добавление служб в контейнер.
builder.Services.AddControllers();
// Включение диагностики внедрения зависимостей
builder.Services.AddDiagnostics();
var app = builder.Build();
// Использование промежуточного ПО диагностики
app.UseDiagnostics();
app.MapControllers();
app.Run();
Это помогает выявлять потенциальные проблемы с временем жизни службы и цепочками зависимостей.
Заключение
Понимание различий между временными жизненными циклами службы AddTransient, AddScoped и AddSingleton необходимо для создания надежных и производительных приложений ASP.NET Core. Вот основные выводы:
-
Используйте AddTransient для легковесных, бессостоянных служб, которым нужны свежие экземпляры каждый раз при их запросе, таких как службы проверки или утилиты с минимальными накладными расходами на инициализацию.
-
Используйте AddScoped для служб, которым нужно поддерживать состояние в рамках одного запроса, но быть изолированными между разными запросами, особенно для контекстов базы данных (DbContext), репозиториев и служб бизнес-логики.
-
Используйте AddSingleton для служб на уровне всего приложения, которые являются потокобезопасными и не поддерживают состояние, специфичное для запроса, таких как службы конфигурации, ведение журналов и механизмы кэширования.
-
Будьте осторожны с захваченными зависимостями - никогда не внедряйте scoped или transient службы в singleton, так как это может вызвать неожиданное поведение и утечки памяти.
-
Учитывайте производственные последствия - службы-одиночки предлагают лучшую производительность для часто используемых служб, в то время как временные службы могут повлиять на производительность, если используются для дорогих объектов.
Тщательно выбирая подходящее время жизни для каждой зависимости, вы будете создавать приложения, которые более поддерживаемые, производительные и менее подверженные проблемам, связанным с параллелизмом.
Источники
- Microsoft Learn - Service lifetimes in dependency injection
- ASP.NET Core Documentation - Dependency injection
- Stack Overflow - Difference between AddTransient, AddScoped and AddSingleton
- Microsoft Learn - Registering scoped services in singleton
- GitHub - ASP.NET Core source code - ServiceLifetime enum