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

Жизненный цикл зависимостей в .NET MAUI — последовательность всех этапов работы сервиса от момента его создания до момента удаления из памяти. В предыдущей части мы разработали простой сервис для записи лога работы приложения в файл и применили этот сервис в своем приложении .NET MAUI. В этой части мы продолжим работу над нашим сервисов и разберемся с различными вариантами регистрации сервисов в приложении.

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

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

  • Transient (временные) — реализация такого сервиса создается при каждом его запросе у провайдера. Сервисы с таким жизненным циклом используются, обычно, в том случае, если нет необходимости сохранять состояние. Например, ели сервис предназначен только для того, чтобы получить текущие координаты устройства и нет необходимости сравнивать эти координаты с предыдущими, то можно определить для сервиса жизненный цикл как Transient. Другой пример — сервисы отправки уведомлений.
  • Scoped (ограниченный) — сервисы с таким жизненным циклом существуют в рамках определенной области (scope). Как только запрос сервиса происходит из другой области — создается новый объект сервиса. Например, Scoped-сервисы используются при обработке HTTP-запросов. Обработка таких запросов в приложении может производится в несколько этапов и сервис может включаться в работу на любом из этих этапов. Таким образом в рамках одного HTTP-запроса создается область (scope) внутри которой «живет» свой экземпляр сервиса. Как только запрос обработан, сервис уничтожается.
  • Singleton (одноэлементные) — объект сервиса создается один раз (при первом запросе) и существует до окончания работы приложения. Такие сервисы используются в том случае, если требуется хранить состояние на протяжении всего времени работы приложения или в течение длительного времени. Также Singleton-сервисы можно применять в том случае, если затраты на создание и настройку такого сервиса превышают затраты на хранение сервиса в памяти.

Ниже мы рассмотрим различные варианты применения сервисов с различными жизненными циклами. Чтобы для сервиса использовался определенный жизненный цикл, его необходимо соответствующим образом зарегистрировать в контейнере внедрения зависимостей. В .NET все названия методов регистрации зависимостей выглядят однотипно и соответствуют следующим шаблонам:

  1. Add[Lificycle]() — регистрирует сервис с жизненным циклом [Lifecycle] в контейнере DI
  2. AddKeyed[Lificycle] — регистрирует сервис с жизненным циклом [Lifecycle] и ключом в контейнере DI

Например, вернемся к предыдущей части и посмотрим на то, как мы регистрировали свой сервис:

//зарегистрировали свой сервис в приложении
builder.Services.AddSingleton<ILogger, FileLogger>();

То есть мы зарегистрировали Singleton-сервис (хотя, могли бы обойтись с Transient). Рассмотрим как влияет жизненный цикл сервиса на работу нашего приложения.

Transient

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

public class FileLogger: ILogger
{
    const string FILE_NAME = "Log.txt";
    private readonly DateTime _currentTime;
   
    private readonly string _newLine = Environment.NewLine;

    public FileLogger()
    {
        _currentTime = DateTime.Now;
    }

    public void WriteLine(string message)
    {
        File.AppendAllText(FILE_NAME, $"[{_currentTime}] - {message}{_newLine}");
    }
}

то есть , при создании экземпляра сервиса, в _currentTime будет запоминаться текущее время и это время будет записываться в лог. Зарегистрируем наш сервис с жизненным циклом transient. Перепишем код MauiProgram.cs следующим образом:

using Microsoft.Extensions.Logging;

using System.Diagnostics;

namespace MauiApp23;

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

        //зарегистрировали свой сервис в приложении
        builder.Services.AddTransient<ILogger, FileLogger>();

        foreach (var app in builder.Services)
        {
            var s= app.ServiceType.ToString();
            Debug.WriteLine(s);
        }

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

        return builder.Build();
    }
}
Далее в тексте, для сокращения, я буду показывать только вызов метод регистрации сервиса в контейнере, так как весь оставшийся код в MauiProgram.cs меняться не будет

Также, изменим код файла MainPage.xaml.cs:

public partial class MainPage : ContentPage
{
    int count = 0;

    private ILogger _logger; //сервис

    public MainPage() 
    {
        InitializeComponent();
    }

    private void OnCounterClicked(object sender, EventArgs e)
    {
        count++;

        if (count == 1)
            CounterBtn.Text = $"Clicked {count} time";
        else
            CounterBtn.Text = $"Clicked {count} times";

        SemanticScreenReader.Announce(CounterBtn.Text);

        if (Handler != null)
        {
            //запрашиваем сервис
            _logger = Handler.GetRequiredService<ILogger>();
            //добавляем запись в лог
            _logger.WriteLine(CounterBtn.Text);
        }
        else
            throw new Exception("Ошибка получения сервисов");
    }
}

Здесь мы поменяли способ получения сервиса. Об этих способах мы поговорим подробнее в следующей части. Теперь запустим приложение и нажмем кнопку несколько раз с небольшим интервалом после чего зайдем в папку с exe-файлом (у меня это папка C:\Users\%user_name%\source\repos\%project_name%\bin\Debug\net9.0-windows10.0.19041.0\win10-x64) и посмотрим на содержимое файла Log.txt:

[22.03.2025 15:44:03] - Clicked 1 time
[22.03.2025 15:44:08] - Clicked 2 times

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

Singleton

Сервисы с таким циклом создаются один раз, после чего контейнер сохраняет ссылку на объект сервиса и при очередном запросе сервиса возвращает одну и ту же копию. Изменим способ регистрации сервиса:

//зарегистрировали свой сервис в приложении
builder.Services.AddSingleton<ILogger, FileLogger>();

Снова запустим приложение, несколько раз нажмем на кнопку и проверим результат записи лога:

[22.03.2025 15:57:26] - Clicked 1 time
[22.03.2025 15:57:26] - Clicked 2 times

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

Scoped

Сервисы с таким жизненным циклом «живут» в некоторой, определенной нами, области. Следовательно, чтобы продемонстрировать работу с таким сервисом, внесем в приложение следующие изменения.

Во-первых, изменим способ регистрации сервиса:

//зарегистрировали свой сервис в приложении
builder.Services.AddScoped<ILogger, FileLogger>();

Во-вторых, изменим обработчик Clicked кнопки следующим образом:

private async void OnCounterClicked(object sender, EventArgs e)
{
    count++;

    if (count == 1)
        CounterBtn.Text = $"Clicked {count} time";
    else
        CounterBtn.Text = $"Clicked {count} times";

    SemanticScreenReader.Announce(CounterBtn.Text);

    if (Handler != null)
    {
        //создаем область
        using (IServiceScope scope = Handler.MauiContext.Services.CreateScope())
        {
            //запрашиваем сервис
            _logger = scope.ServiceProvider.GetRequiredService<ILogger>();
            //добавляем запись в лог
            _logger.WriteLine(CounterBtn.Text);
            await Task.Delay(1000); //ждем 1 секунду
            //снова запрашиваем сервис
            _logger = scope.ServiceProvider.GetRequiredService<ILogger>();
            _logger.WriteLine("Это всё ещё одна и та же копия сервиса");
        }
        //создаем область
        using (IServiceScope scope2 = Handler.MauiContext.Services.CreateScope())
        {
            await Task.Delay(1000); //ждем 1 секунду
            _logger = scope2.ServiceProvider.GetRequiredService<ILogger>();
            _logger.WriteLine("А это уже другая область и другой сервис");
        }
    }
    else
        throw new Exception("Ошибка получения сервисов");
}

Здесь мы создаем две области:

//создаем область
using (IServiceScope scope = Handler.MauiContext.Services.CreateScope())
{
   ....
}

//создаем область
using (IServiceScope scope2 = Handler.MauiContext.Services.CreateScope())
{
   ....
}

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

//запрашиваем сервис
_logger = scope.ServiceProvider.GetRequiredService<ILogger>();

Так как scoped-сервис «живет» внутри своей области, то в области scope мы запрашиваем сервис дважды с интервалом в 1 секунду, чтобы показать, что наш сервис всё ещё тот же. И далее, во второй области мы снова запрашиваем сервис, чтобы продемонстрировать, что в новой области будет новый объект сервиса.

Вот как теперь будет выглядеть лог нашего приложения:

[22.03.2025 16:15:15] - Clicked 1 time
[22.03.2025 16:15:15] - Это всё ещё одна и та же копия сервиса
[22.03.2025 16:15:17] - А это уже другая область и другой сервис

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

Итого

Сервисы в .NET MAUI можно зарегистрировать с использованием трех жизненных циклов — transient, singleton и scoped. Выбор того или иного жизненного цикла зависит от задач, которые возлагаются на него, а также от сложности самого сервиса.

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