Жизненный цикл зависимостей (работа с ServiceCollection)

Получив первоначальные знания о работе с DI с использованием встроенных в .NET объектов, можно погрузиться в темы внедрения зависимостей более детально. В этой части мы более подробно изучим работу с сервисами, а именно — рассмотрим жизненный цикл зависимостей и использование сервисов с различными жизненными циклами.

Жизненный цикл зависимостей в .NET

Всего в .NET выделяется три типа жизненных циклов зависимостей (сервисов):

  • Transient (временные) — реализация такого сервиса создается при каждом его запросе у провайдера
  • Scoped (ограниченный) — сервисы с таким жизненным циклом существуют в рамках определенной области (scope). Как только запрос сервиса происходит из другой области — создается новый объект сервиса.
  • Singleton (одноэлементные) — объект сервиса создается один раз (при первом запросе) и существует до окончания работы приложения.

Рассмотрим сервисы с различным жизненным циклом более детально. Но для начала, чтобы наглядно продемонстрировать работу сервисов с различными жизненными циклами, напишем небольшой тестовый пример.

Пример, демонстрирующий работу сервисов с различными жизненными циклами

Код примера:

using Microsoft.Extensions.DependencyInjection;

namespace Example2
{
    public interface ITimeService
    {
        public DateTime GetTime();
    }

    public class TimeService : ITimeService
    {
        private readonly DateTime _time;

        public TimeService()
        {
            _time = DateTime.Now;
        }

        public DateTime GetTime()
        {
            return _time;
        }
    }


    internal class Program
    {
        static void Main(string[] args)
        {
            ServiceCollection services = new ServiceCollection();
            
            services.AddTransient<ITimeService, TimeService>(); //эта часть будет изменяться
            
            IServiceProvider serviceProvider = services.BuildServiceProvider();

            for (int i = 0; i < 3; i++)
            {
                var service = serviceProvider.GetService<ITimeService>();
                Console.WriteLine($"Запрос {i}: {service.GetTime()}");
                Thread.Sleep( 1000 );
            }
        }
    }
}

Здесь сервис ITimeService возвращает время, которое фиксируется в момент создания реализации сервиса (объекта, реализующего интерфейс). В зависимости от того, какой жизненный цикл мы будем устанавливать для сервиса, будет изменяться способ регистрации:

services.AddTransient<ITimeService, TimeService>(); //эта часть будет изменяться

В цикле мы трижды запрашиваем сервис и выводим значение времени в консоль с интервалом в одну секунду:

for (int i = 0; i < 3; i++)
{
    var service = serviceProvider.GetService<ITimeService>();
    Console.WriteLine($"Запрос {i}: {service.GetTime()}");
    Thread.Sleep( 1000 );
}

Transient (временный) сервис

Объект такого сервиса создается каждый раз при запросе. Если нет необходимости хранить данных состоянии, сервис выполняет простые задачи, например, отправляет уведомление, электронное письмо и так далее, то можно зарегистрировать transient-сервис. Итак, результат работы нашего тестового примера с таким сервисом будет выглядеть следующим образом:

Запрос 0: 12.12.2024 8:58:28
Запрос 1: 12.12.2024 8:58:29
Запрос 2: 12.12.2024 8:58:30

Как видите, каждый раз сервис возвращает новое значение времени, так как объект сервиса создается каждый раз при очередном запросе.

Singleton (одноэлементный) сервис

Такой сервис создается один раз при первом запросе, а при повторных запросах возвращается уже созданный ранее объект. Такие виды сервисов нужны в том случае, если необходимо хранить состояние системы (или её части) или, когда создание/уничтожение transient-сервиса может быть дороже, чем постоянно держать в памяти один объект singleton-сервиса.

Перерегистрируем наш сервис, как singleton

services.AddSingleton<ITimeService, TimeService>();

и посмотрим на результат работы приложения:

Запрос 0: 12.12.2024 9:04:22
Запрос 1: 12.12.2024 9:04:22
Запрос 2: 12.12.2024 9:04:22

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

Scoped (ограниченный) сервис

Как было сказано выше, такие сервисы «живут» в какой-то ограниченной области. Что это за область? Например, если мы говорим про web-приложения, то областью действия сервиса для обработки запроса является http-запрос. Что касается других типов приложений, то такую область мы можем определить самостоятельно, например, перепишем наше приложение следующим образом:

services.AddScoped<ITimeService, TimeService>(); //эта часть будет изменяться

IServiceProvider serviceProvider = services.BuildServiceProvider();

using (IServiceScope scope = serviceProvider.CreateScope()) //создали область
{
    for (int i = 0; i < 3; i++)
    {
        var service = scope.ServiceProvider.GetService<ITimeService>();
        Console.WriteLine($"Запрос {i}: {service.GetTime()}");
        Thread.Sleep(1000);
    }
}

здесь, используя метод CreateScope() у serviceProvider мы создаем область в которой будет использоваться наш сервис. Можно запустить приложение и убедиться, что действовать оно будет точно также, как и в предыдущем случае, то есть выдаст за три запроса три одинаковых значения времени. Обратите внимание на то как запрашивается сервис:

var service = scope.ServiceProvider.GetService<ITimeService>();

здесь мы запрашиваем сервис, используя переменную scope, а не напрямую через serviceProvider. Теперь изменим область видимости, например, так:

for (int i = 0; i < 3; i++)
{
    using (IServiceScope scope = serviceProvider.CreateScope()) //создали область
    {
        var service = scope.ServiceProvider.GetService<ITimeService>();
        Console.WriteLine($"Запрос {i}: {service.GetTime()}");
        Thread.Sleep(1000);
    }
}

Теперь мы будем при каждой итерации цикла создавать новую область и, соответственно, объект сервиса у нас будет каждый раз новый, то есть мы здесь получим поведение такое же, как и при использование transient-сервиса. Можете поэкспериментировать с этим примером и запросить сервис на каждой итерации цикла не один раз, а например, дважды — вы увидите, что пока мы находимся в области, заданной scope — сервис будет использоваться один и тот же.

Единственный момент, который может сбить с толку при использовании scoped-сервисов заключается в том, что, если запросить scoped-сервис вне области действия, то он становится обычным singleton-сервисом. Вы не видите никаких ошибок при компиляции приложения, не будет ошибок и при работе приложения, но такой вызов scoped-сервисов не рекомендуется самими разработчиками .NET и считается антишаблоном — так делать не надо:

for (int i = 0; i < 3; i++)
{
  //  using (IServiceScope scope = serviceProvider.CreateScope()) //создали область
  //  {
        var service = serviceProvider.GetService<ITimeService>();
        Console.WriteLine($"Запрос {i}: {service.GetTime()}");
        Thread.Sleep(1000);
  //  }
}

Методы регистрации сервисов

Как вы уже наверняка обратили внимание, при регистрации сервиса мы используем методы ServiceCollection (а, если быть точнее, то методы расширения для ServiceCollection), имена которых соответствуют шаблону:

Add[Жизненный_цикл]()

а начиная с .NET 8, появились также методы вида:

AddKeyed[Жизненный_цикл]()

и, соответственно, таких методов в .NET 7 и более ранних версиях три: AddTransient(), AddScoped() и AddSingleton() и шесть для .NET 8 и более поздних версий. Каждый из этих методов имеет ряд перегруженных версий. Рассмотрим эти версии на примере регистрации сервиса с жизненным циклом transient:

AddTransient(IServiceCollection, Type)

AddTransient(IServiceCollection, Type)

При использовании этого метода необходимо указывать тип реализующий сервис. Например,

services.AddTransient(typeof(TimeService)); //регистрируем сервис
var service = serviceProvider.GetService<TimeService>(); //запрашиваем

AddTransient<TService>(IServiceCollection)

Этот метод действует аналогично предыдущему — регистрирует в качестве сервиса конкретную его реализацию.

AddTransient(IServiceCollection, Type, Func<IServiceProvider,Object>)

public static IServiceCollection AddTransient (this IServiceCollection services, Type serviceType, Func<IServiceProvider,object> implementationFactory);

Этот метод регистрирует сервис с указанием фабрики, используемой для создания экземпляра сервиса. Например,

services.AddTransient(typeof(ITimeService), 
    _ => { 
        return new TimeService(); 
    }); //регистрируем сервис

var service = serviceProvider.GetService<ITimeService>(); //запрашиваем сервис

В отличие от предыдущих методов, в этом методе мы указываем тип сервиса (интерфейс) и лямбда-выражение (фабрику) для создания конкретной реализации сервиса, что позволяет запросить сервис из провайдера, а не его реализацию.

AddTransient<TService>(IServiceCollection, Func<IServiceProvider,TService>)

public static IServiceCollection AddTransient<TService> (this IServiceCollection services, Func<IServiceProvider,TService> implementationFactory) where TService : class;

Действует аналогично предыдущему методу

AddTransient(IServiceCollection, Type, Type)

AddTransient(IServiceCollection, Type serviceType, Type implementationType)

В этом случае мы должны указать тип регистрируемого сервиса и тип, реализующий сервис. Например:

services.AddTransient(typeof(ITimeService), typeof(TimeService)); //регистрируем сервис
var service = serviceProvider.GetService<ITimeService>(); //запрашиваем сервис

в отличие от предыдущего метода, здесь мы запрашиваем именно сервис, а не его реализацию.

AddTransient<TService,TImplementation>(IServiceCollection)

public static IServiceCollection AddTransient<TService,TImplementation>(this IServiceCollection services) where TService : class where TImplementation : class, TService;

Этот метод действует аналогично предыдущему и именно этот метод мы чаще всего используем в работе:

services.AddTransient<ITimeService, TimeService>(); //регистрируем сервис
var service = serviceProvider.GetService<ITimeService>(); //запрашиваем сервис

AddTransient<TService,TImplementation>(IServiceCollection, Func<IServiceProvider,TImplementation>)

public static IServiceCollection AddTransient<TService,TImplementation> (this IServiceCollection services, Func<IServiceProvider,TImplementation> implementationFactory) where TService : class where TImplementation : class, TService;

Позволяет зарегистрировать сервис, указав тип его реализации и фабрику создания экземпляра типа. Например:

services.AddTransient<ITimeService, TimeService>( 
    _ => { 
        return new TimeService(); 
    }); //регистрируем сервис
var service = serviceProvider.GetService<ITimeService>(); //запрашиваем сервис

Аналогичные версии методов вы можете использовать и для регистрации сервисов с другими жизненными циклами, например, AddSingleton<TService, TImplementation>() и другие. Думаю, что повторять их описание в рамках этой части не стоит, а лучше перейти к следующему вопросу использования DI в наших приложениях .NET/C#.

Итого

Сервисы в .NET могут иметь три типа жизненных циклов: Transient (временные) — реализация такого сервиса создается при каждом его запросе у провайдера; Scoped (ограниченный) — сервисы с таким жизненным циклом существуют в рамках определенной области (scope) и Singleton (одноэлементные) — объект сервиса создается один раз (при первом запросе) и существует до окончания работы приложения. В зависимости от того, какой жизненный цикл предполагается использовать для зависимости (сервиса) выбирается их метод регистрации в ServiceCollectionAddTransient(), AddScoped() или AddSingleton(). Также, начиная с .NET 8 появился новый вид методов — AddKeyed[LifeTime](), используемый для регистрации сервисов с ключами. Об этих сервисах мы поговорим в следующей части.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии