Содержание
- Жизненный цикл зависимостей в .NET
- Пример, демонстрирующий работу сервисов с различными жизненными циклами
- Singleton (одноэлементный) сервис
- Scoped (ограниченный) сервис
- Методы регистрации сервисов
- AddTransient(IServiceCollection, Type)
- AddTransient<TService>(IServiceCollection)
- AddTransient(IServiceCollection, Type, Func<IServiceProvider,Object>)
- AddTransient<TService>(IServiceCollection, Func<IServiceProvider,TService>)
- AddTransient(IServiceCollection, Type, Type)
- AddTransient<TService,TImplementation>(IServiceCollection)
- AddTransient<TService,TImplementation>(IServiceCollection, Func<IServiceProvider,TImplementation>)
- Итого
Получив первоначальные знания о работе с 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-сервис. Итак, результат работы нашего тестового примера с таким сервисом будет выглядеть следующим образом:
Запрос 1: 12.12.2024 8:58:29
Запрос 2: 12.12.2024 8:58:30
Как видите, каждый раз сервис возвращает новое значение времени, так как объект сервиса создается каждый раз при очередном запросе.
Singleton (одноэлементный) сервис
Такой сервис создается один раз при первом запросе, а при повторных запросах возвращается уже созданный ранее объект. Такие виды сервисов нужны в том случае, если необходимо хранить состояние системы (или её части) или, когда создание/уничтожение transient-сервиса может быть дороже, чем постоянно держать в памяти один объект singleton-сервиса.
Перерегистрируем наш сервис, как singleton
services.AddSingleton<ITimeService, TimeService>();
и посмотрим на результат работы приложения:
Запрос 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 (одноэлементные) — объект сервиса создается один раз (при первом запросе) и существует до окончания работы приложения. В зависимости от того, какой жизненный цикл предполагается использовать для зависимости (сервиса) выбирается их метод регистрации в ServiceCollection
— AddTransient()
, AddScoped()
или AddSingleton()
. Также, начиная с .NET 8 появился новый вид методов — AddKeyed[LifeTime]()
, используемый для регистрации сервисов с ключами. Об этих сервисах мы поговорим в следующей части.