НейроАгент

AddTransient vs AddScoped vs AddSingleton: Полное руководство

Узнайте ключевые различия между временами жизни сервисов AddTransient, AddScoped и AddSingleton в ASP.NET Core dependency injection. Узнайте, когда использовать каждый тип времени жизни и как они влияют на производительность и поведение приложения.

Вопрос

В чем разница между временными существованиями (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 создает один экземпляр, который существует на протяжении всего времени жизни приложения. Ключевые различия заключаются в частоте создания экземпляров, использовании памяти, потокобезопасности и в том, как они обрабатывают службы с состоянием между несколькими запросами.

Содержание

Понимание времени жизни службы

Время жизни службы в ASP.NET Core в внедрении зависимостей контролирует, как создаются и управляются экземпляры зарегистрированных служб. Каждое время жизни служит разным целям и имеет определенные характеристики, которые делают его подходящим для конкретных сценариев.

Определение времени жизни службы: Время жизни службы определяет область и длительность существования экземпляра службы и то, как он совместно используется в разных частях вашего приложения.

Три основных времени жизни службы:

  1. Transient: Создается новый экземпляр при каждом запросе службы
  2. Scoped: Создается новый экземпляр на одну область (обычно на один HTTP-запрос)
  3. Singleton: Создается один экземпляр один раз и повторно используется на протяжении всего времени жизни приложения

Эти времена жизни регистрируются с помощью методов AddTransient(), AddScoped() и AddSingleton() соответственно при настройке служб в методе ConfigureServices вашего класса Startup или в Program.cs в более новых версиях.


Время жизни AddTransient

Временные службы (Transient services) создаются каждый раз, когда они запрашиваются из контейнера внедрения зависимостей. Это означает, что если одна и та же служба запрашивается несколько раз в одной области, будет создано несколько экземпляров.

Характеристики

  • Создание: Новый экземпляр при каждом запросе
  • Область: Нет осведомленности об области
  • Использование памяти: Высокое (создается больше экземпляров)
  • Потокобезопасность: Как правило безопасно (нет общего состояния между запросами)

Пример использования

csharp
// В методе 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-запрос)
  • Область: Осведомленность об области (существует в текущей области)
  • Использование памяти: Среднее (один экземпляр на запрос)
  • Потокобезопасность: Безопасно в пределах одного запроса, но будьте осторожны с общим состоянием

Пример использования

csharp
// В методе 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) создаются только один раз при первом запросе, а затем повторно используются для всех последующих запросов на протяжении всего времени жизни приложения.

Характеристики

  • Создание: Один экземпляр создается один раз и повторно используется
  • Область: Приложение в целом (нет осведомленности об области)
  • Использование памяти: Наименьшее (существует только один экземпляр)
  • Потокобезопасность: Критический аспект - должен быть потокобезопасным, если используется в веб-приложениях

Пример использования

csharp
// В методе ConfigureServices
services.AddSingleton<IEmailSender, AuthMessageSender>();

// Служба конфигурации приложения в целом
services.AddSingleton<IConfigurationService, ConfigurationService>();

// Служба кэширования
services.AddSingleton<ICacheService, MemoryCacheService>();

Вопросы потокобезопасности

Для служб-одиночек в веб-приложениях вы должны обеспечить их потокобезопасность, так как они будут доступны из нескольких одновременных запросов:

csharp
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 Свежие правила проверки каждый раз

Распространенные ошибки и лучшие практики

Распространенные ошибки, которых следует избегать

  1. Scoped служба в Singleton

    csharp
    // АНТИ-ШАБЛОН: Это вызовет проблемы
    services.AddSingleton<IMyService>(provider => 
    {
        var scopedService = provider.GetRequiredService<IScopedService>();
        return new MyService(scopedService);
    });
    

    Это создает захваченную зависимость (captive dependency), когда scoped служба захватывается singleton, что заставляет ее существовать дольше, чем предполагалось.

  2. Состояние в службах-одиночках

    csharp
    // АНТИ-ШАБЛОН: Не потокобезопасно
    public class BadSingletonService : IMyService
    {
        private int _counter = 0;  // Общий для всех запросов!
        
        public void Increment()
        {
            _counter++;  // Состояние гонки!
        }
    }
    
  3. Чрезмерное использование Transient служб для тяжелых объектов

    csharp
    // ВОЗМОЖНАЯ ПРОБЛЕМА: Дорогие объекты создаются часто
    services.AddTransient<HeavyObject>();
    

Лучшие практики

  1. Следуйте принципу цепочки зависимостей: Службы не должны зависеть от служб с более коротким временем жизни

    • Singleton → Singleton ✓
    • Singleton → Scoped ✗ (Захваченная зависимость)
    • Singleton → Transient ✗ (Захваченная зависимость)
    • Scoped → Scoped ✓
    • Scoped → Transient ✓
    • Transient → Transient ✓
  2. Используйте Scoped для операций с базой данных

    csharp
    services.AddScoped<IApplicationDbContext, ApplicationDbContext>();
    services.AddScoped<IProductRepository, ProductRepository>();
    services.AddScoped<IOrderService, OrderService>();
    
  3. Рассмотрите шаблон Factory для сложных зависимостей

    csharp
    services.AddSingleton<IComplexService>(provider => 
    {
        // Создайте scoped зависимости здесь
        var scopedDependency = provider.CreateScope()
            .ServiceProvider.GetRequiredService<IScopedDependency>();
        
        return new ComplexService(scopedDependency);
    });
    

Вопросы производительности

Влияние на память

  • Singleton: Наименьший след памяти (один экземпляр)
  • Scoped: Средний след памяти (один экземпляр на одновременный запрос)
  • Transient: Наибольший след памяти (количество экземпляров = количество запросов)

Накладные расходы на создание

  • Singleton: Создается только один раз (наибольшие начальные затраты, но амортизируемые)
  • Scoped: Создается один раз на запрос (умеренные затраты на создание)
  • Transient: Создается каждый раз (наибольшие совокупные затраты на создание)

Вопросы масштабируемости

Для приложений с высоким трафиком:

csharp
// Хорошо для сценариев с высоким трафиком
services.AddSingleton<IHttpClientFactory, DefaultHttpClientFactory>();

// Потенциально проблематично для высокого трафика
services.AddTransient<ExpensiveService>();  // Создает много экземпляров

Мониторинг и диагностика

ASP.NET Core предоставляет встроенную диагностику для внедрения зависимостей:

csharp
// В 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. Вот основные выводы:

  1. Используйте AddTransient для легковесных, бессостоянных служб, которым нужны свежие экземпляры каждый раз при их запросе, таких как службы проверки или утилиты с минимальными накладными расходами на инициализацию.

  2. Используйте AddScoped для служб, которым нужно поддерживать состояние в рамках одного запроса, но быть изолированными между разными запросами, особенно для контекстов базы данных (DbContext), репозиториев и служб бизнес-логики.

  3. Используйте AddSingleton для служб на уровне всего приложения, которые являются потокобезопасными и не поддерживают состояние, специфичное для запроса, таких как службы конфигурации, ведение журналов и механизмы кэширования.

  4. Будьте осторожны с захваченными зависимостями - никогда не внедряйте scoped или transient службы в singleton, так как это может вызвать неожиданное поведение и утечки памяти.

  5. Учитывайте производственные последствия - службы-одиночки предлагают лучшую производительность для часто используемых служб, в то время как временные службы могут повлиять на производительность, если используются для дорогих объектов.

Тщательно выбирая подходящее время жизни для каждой зависимости, вы будете создавать приложения, которые более поддерживаемые, производительные и менее подверженные проблемам, связанным с параллелизмом.

Источники

  1. Microsoft Learn - Service lifetimes in dependency injection
  2. ASP.NET Core Documentation - Dependency injection
  3. Stack Overflow - Difference between AddTransient, AddScoped and AddSingleton
  4. Microsoft Learn - Registering scoped services in singleton
  5. GitHub - ASP.NET Core source code - ServiceLifetime enum