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

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

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

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

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

Для демонстрации работы сервиса в зависимости от установленного для него жизненного цикла, создадим новое приложение Blazor Hybrid, добавим в него новую папку Services и разместим в папке следующий сервис:

public interface ITimeService
{
    public DateTime GetDateTime(); 
}

также добавим реализацию этого сервиса

public class TimeService : ITimeService
{
    private readonly DateTime _datetime;

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

    public DateTime GetDateTime()
    {
        return _datetime;
    }
}

Смысл нашего сервиса — показать дату/время, когда создавался очередной экземпляр. Теперь посмотрим как будет работать этот сервис. Для этого перейдем к файлу компонента Home.razor и изменим его код следующим образом:

@page "/"

@using BlazorDiLifiteme.Services

@inject ITimeService TimeService

<h1>Hello, world!</h1>

<p>@TimeService.GetDateTime()</p>

Теперь нам осталось только зарегистрировать наш сервис различными способами и посмотреть на результат.

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

Мы уже знаем, что для регистрации сервисов в приложении Blazor Hybrid необходимо перейти в MauiProgram.cs и, используя свойство MauiApBuilder.Services добавить сервис в контейнер DI. Для регистрации мы используем методы расширения для IServiceCollection имена которых соответствуют следующему шаблону:

Add{Keyed}[Lifecycle]()

здесь Keyed — часть названия метода, которая может отсутствовать. Сервисы, которые регистрируются с использованием методов AddKeyed... также называются сервисами с ключами (о таких сервисах мы поговорим позднее).  [Lifecycle] — один из жизненных циклов сервиса (Transient, Scoped или Singleton).

Таким образом, имена методов для регистрации сервисов могут быть такими:

  1. AddTransient()
  2. AddScoped()
  3. AddSingleton()
  4. AddKeyedTransient()
  5. AddKeyedScoped()
  6. AddKeyedSingleton()

При этом, каждый такой метод может иметь несколько перегруженных версий с различными параметрами. Например, метод AddTransient() на момент выхода .NET 9 имел девять перегруженных версий. Мы рассмотрим только наиболее распространенные варианты методов регистрации сервисов.

Со всеми версиями регистрации сервисов в .NET на примере метода AddTransient() вы можете ознакомиться в этой статье блога

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

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

    public static class MauiProgram
    {
        public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();
            builder
                .UseMauiApp<App>()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                });

            builder.Services.AddTransient<ITimeService, TimeService>(); //регистрируем сервис

            builder.Services.AddMauiBlazorWebView();

#if DEBUG
    		builder.Services.AddBlazorWebViewDeveloperTools();
    		builder.Logging.AddDebug();
#endif

            return builder.Build();
        }
    }

Здесь мы использовали одну из версий метода, указав тип сервиса и тип, реализующий сервис:

builder.Services.AddTransient<ITimeService, TimeService>(); //регистрируем сервис

Теперь запустим приложение и убедимся, что сервис вернул нам время создания своего экземпляра:

 

Теперь попробуем перейти, например, на страницу Counter, а затем снова вернуться на главную — вы увидите, что время изменяется каждый раз как только вы возвращаетесь к компоненту Home:

То есть, как и ожидалось, сервис создается каждый раз при его запросе из контейнера.

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

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

Изменим в MauiProgram.cs способ регистрации сервиса:

builder.Services.AddSingleton<ITimeService, TimeService>(); //регистрируем сервис

Запустим приложение и повторим те же операции, что и в предыдущем случае. Сколько бы раз вы не возвращались к Home — сервис будет возвращать всегда одно и то же значение времени так как объект сервиса не уничтожается.

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

Действие по умолчанию в Blazor Hybrid

Как было сказано выше, такие сервисы «живут» в какой-то ограниченной области. Что это за область? Зависит от типа приложения .NET с которым мы работаем. Например, если речь идет о приложениях ASP.NET Core Web API, MVC и так далее, то для таких приложений «областью» действия сервиса выступает http-запрос — пока обрабатывается запрос пользователя сервис «живет». Что касается приложений Blazor, то здесь, по умолчанию, scoped-сервисы ведут себя как обычные singleton-сервисы. Например, зарегистрируем на сервис следующим образом:

builder.Services.AddScoped<ITimeService, TimeService>(); //регистрируем сервис

Теперь запустим приложение и снова попытаемся несколько раз перейти на страницу Home. Опять же, как и в случае с Singleton — мы будем получать всё время одно и то же значение времени. Однако, это не означает, что scoped-сервисы бесполезны в Blazor Hybrid.  Мы можем создать область, соответствующую времени жизни компонента Razor. Для этого нам потребуется изменить предка для компонента.

Использование классов OwningComponentBase и OwningComponentBase<TService>

Класс OwningComponentBase — это абстрактный класс производный от ComponentBase (базового класса всех компонентов Razor) который предоставляет нам защищенное свойство

protected IServiceProvider ScopedServices { get; }

благодаря которому мы можем запрашивать сервисы с областью действия, соответствующей времени жизни компонента. Чтобы продемонстрировать работу с этим классом перепишем компонент Home следующим образом:

@page "/"

@using BlazorDiLifiteme.Services

@inherits OwningComponentBase

@inject ITimeService TimeService

<h1>Hello, world!</h1>

<p>Scoped-сервис по умолчанию: @(TimeService.GetDateTime())</p>
<p>Scoped-сервис @(ScopedTimeService.GetDateTime())</p>

@code{
    private ITimeService ScopedTimeService = default!;

    protected override void OnInitialized()
    {
        ScopedTimeService = ScopedServices.GetRequiredService<ITimeService>();
    }
}

Рассмотрим этот код немного подробнее. Первое, на сто необходимо обратить внимание — это на новую для нас директиву Razor:

@inherits OwningComponentBase

Эта директива означает, что компонент наследуется от класса OwningComponentBase. Второй момент — теперь мы дважды запрашиваем сервис: первый раз — как обычно, через свойство:

@inject ITimeService TimeService

а второй раз — используя свойство ScopedServices родительского класса компонента:

protected override void OnInitialized()
{
    ScopedTimeService = ScopedServices.GetRequiredService<ITimeService>();
}

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

Теперь запустим приложение. При запуске мы увидим, что время, возвращаемое обоими экземплярами сервиса полностью совпадают:

Однако, как только вы перейдете на другую страницу и снова вернетесь к Home, то время будет различаться:

так как во втором случае (при запросе с помощью свойства ScopedServices) мы получаем сервис ограниченный временем жизни компонента.

Что касается класса OwningComponentBase<TService>, то этот класс наследуется от OwningComponentBase и предоставляет нам новое свойство:

protected TService Service { get; }

То есть можно переписать код компонента следующим образом

@page "/"

@using BlazorDiLifiteme.Services

@inherits OwningComponentBase<ITimeService>

@inject ITimeService TimeService

<h1>Hello, world!</h1>

<p>Scoped-сервис по умолчанию: @(TimeService.GetDateTime())</p>
<p>Scoped-сервис @(Service.GetDateTime())</p>

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

Итого

Сервисы в .NET могут иметь три типа жизненных циклов: Transient (временные) — реализация такого сервиса создается при каждом его запросе у провайдера; Scoped (ограниченный) — сервисы с таким жизненным циклом существуют в рамках определенной области (scope) и Singleton (одноэлементные) — объект сервиса создается один раз (при первом запросе) и существует до окончания работы приложения. В зависимости от того, какой жизненный цикл предполагается использовать для зависимости (сервиса) выбирается их метод регистрации в ServiceCollection — AddTransient(), AddScoped() или AddSingleton(). Для того, чтобы сервис в приложении Blazor Hybrid был ограничен областью, соответствующей времени жизни компонента Razor, такой компонент должен наследоваться от OwningComponentBase или его наследника — OwningComponentBase<TService>.

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