Содержание
Внедрение зависимостей (Dependency Injection, DI) — это набор принципов и паттернов проектирования программных продуктов, позволяющий разрабатывать слабосвязанный код. Dependency Injection в ASP.NET Core и Blazor, в частности — это основной способ расширения возможностей приложения. Вместе с тем, так как приложения Blazor Hybrid направлены на разработку кроссплатформенных приложений, то здесь использование DI имеет свои особенности, которые мы и рассмотрим в этой части. Но начнем всё же с наиболее общих моментов использования DI в приложении Blazor.
Зависимости и сервисы
Пример без зависимостей
Зависимость — это любой объект, от которого зависит другой объект. Например, в процессе разработки приложения вы решите, что было бы неплохо записывать в файл лог работы какого-либо компонента приложения, например, счётчика (Counter.razor). Создадим новый класс с именем FileLogger и разместим его в папке Services приложения Blazor Hybrid:
namespace BlazorDI.Services
{
public class FileLogger
{
private const string FileName = "Log.log";
private readonly string _newLine = Environment.NewLine;
public void WriteLine(string message)
{
File.AppendAllText(FileName, $"{message}{_newLine}");
}
}
}
Теперь используем этот класс в компоненте Counter следующим образом:
@page "/counter"
@using BlazorDI.Services
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private FileLogger logger = new();
private void IncrementCount()
{
currentCount++;
logger.WriteLine("Значение счётчика увеличено на 1");
}
}
Здесь с помощью директивы Razor @using мы подключили новое пространство имен в котором содержится наш новый класс, а в коде C# создали объект типа и вызвали метод:
private FileLogger logger = new();
private void IncrementCount()
{
currentCount++;
logger.WriteLine("Значение счётчика увеличено на 1");
}
Теперь можно запустить приложение и убедиться, что файл действительно создается и в него пишется строка «Значение счётчика увеличено на 1», каждый раз как только мы кликаем по кнопке счётчика. Теперь класс компонента Razor зависит от класса FileLogger. То есть FileLogger — это зависимость. С течением времени мы можем использовать этот класс во многих местах нашего приложения, развивать его, добавляя новые методы, свойства и так далее. Приложение будет работать и даже, возможно, хорошо поддерживаться до тех пор, пока вы не решите кардинально изменить способ ведения лога приложения. Например, вы решите, что неплохо бы писать лог не в файл, а вообще отправлять по сети или вести лог в базе данных SQLite. Вариантов масса, но все они приводят нас сейчас к тому, что придется пересматривать весь код приложения в поисках использования FileLogger и менять его на новый класс, а, если учесть, что FileLogger, в свою очередь, может к этому времени зависеть от других классов, то поддержка такого кода становится весьма нетривиальной задачей. И чтобы решить эту задачу с минимальными потерями, мы и можем задействовать внедрение зависимостей.
Использование абстракций
Чтобы можно было без проблем заменять одну зависимость на другую в приложении используются абстракции, например, интерфейсы. Перепишем код нашего приложения следующим образом. Во-первых, создадим в папке Services интерфейс IBlazorLogger:
namespace BlazorDI.Services
{
public interface IBlazorLogger
{
public void WriteLine(string message);
}
}
Этот интерфейс пока содержит всего один метод, но, в дальнейшем мы можем добавить в него и другие методы работы с логом приложения. В терминах DI наш интерфейс называется служба или сервис.
Во-вторых, реализуем этот интерфейс в классе FileLogger:
public class FileLogger: IBlazorLogger
{
private const string FileName = "Log.log";
private readonly string _newLine = Environment.NewLine;
public void WriteLine(string message)
{
File.AppendAllText(FileName, $"{message}{_newLine}");
}
}
Реализуя интерфейс IBlazorLogger в различных классах мы можем создавать различные реализации нашего сервиса, например:
public class DbLogger : IBlazorLogger
{
public void WriteLine(string message)
{
//тут реализация записи лога в БД
}
}
public class HttpLogger : IBlazorLogger
{
public void WriteLine(string message)
{
//тут реализация отправки лога по сети
}
}
Теперь нам необходимо каким-либо образом указать компоненту Counter, что мы будем использовать реализацию IBlazorLogger. Самый простой вариант — сделать вот так:
private IBlazorLogger logger = new FileLogger();
private void IncrementCount()
{
currentCount++;
logger.WriteLine("Значение счётчика увеличено на 1");
}
Но так мы теряем смысл внедрения зависимостей — мы всё также указываем, что за интерфейсом скрывается FileLogger. Воспользуемся уже имеющимися возможностями .NET.
Пространство имен Microsoft.Extensions.DependencyInjection
Основные типы данных по работе с зависимостями в .NET сосредоточены в пространстве имен Microsoft.Extensions.DependencyInjection. Здесь содержатся три основных интерфейса с которыми мы будем дальше работать:
IServiceCollection— определяет контракт для коллекции дескрипторов сервисов.IServiceProvider— определяет механизм получения объекта сервиса.ServiceDescriptor— описывает сервис.
Смысл нашей работы заключается в следующем. Мы добавляем сервис в коллекцию IServiceCollection перед запуском приложения. Когда приложение запущено, мы используем IServiceProvider для получения сервиса из коллекции (контейнера DI). При этом, контейнер DI самостоятельно определяет все связи зависимостей, создает и возвращает нам уже готовый к использованию экземпляр сервиса. В зависимости от типа проекта .NET, необходимые классы для работы с DI могут уже присутствовать в проекте. Например, в приложении Blazor Hybrid у нас уже имеется готовый к использованию контейнер DI. Посмотрим на код файла MauiProgram.cs:
После того, как с использованием метода CreateBuilder() класса MauiApp создается экземпляр MauiAppBuilder мы получаем в свое распоряжение объект, реализующий IServiceCollection и можем, используя его, зарегистрировать свой сервис в приложении. Ниже мы рассмотрим один из вариантов регистрации сервиса, а другие варианты — рассмотрим в следующей части.
Регистрация и использование собственного сервиса в приложении
Итак, вернемся к нашему приложению и зарегистрируем сервис FileLogger. Допишем код файла MauiProgram.cs следующим образом:
using BlazorDI.Services;
using Microsoft.Extensions.Logging;
namespace BlazorDI
{
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.AddSingleton<IBlazorLogger, FileLogger>();//регистрируем свой сервис
builder.Services.AddMauiBlazorWebView();
#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools();
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}
}
Здесь мы, используя метод расширения AddSingleton() добавили в контейнер DI свой сервис IBlazorLogger, указав также и его реализацию — FileLogger. Теперь при запросе сервиса мы будем получать в свое распоряжение экземпляр именно этого класса.
Осталось запросить сервис и воспользоваться им. Опять же, ниже показан один из вариантов того, как мы можем это сделать. Откроем файл Counter.razor и перепишем его следующим образом:
@page "/counter"
@using BlazorDI.Services
@*запрашиваем сервис*@
@inject IBlazorLogger Logger
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
// private IBlazorLogger logger = new FileLogger();
private void IncrementCount()
{
currentCount++;
Logger.WriteLine("Значение счётчика увеличено на 1"); //используем сервис
}
}
Здесь мы используем директиву @inject для запроса сервиса
@inject IBlazorLogger Logger
IBlazorLogger — сам сервис, а Logger свойство, которое будет содержать готовый к использованию экземпляр сервиса. Обратите внимание, что мы больше нигде (кроме файла MauiProgram.cs) не указываем конкретный тип, реализующий сервис — нам не важно, что скрывается за интерфейсом — FileLogger или другой класс. Главное, что мы можем воспользоваться методом WriteLine() о котором мы знаем. Более того, теперь все сервисы регистрируются в одном месте приложения — в файле MauiProgram.cs и мы можем с минимальными затратами ресурсов заменить одну реализацию сервиса на другую, не меняя при этом код всего приложения. Теперь можно запустить приложение и убедиться, что оно работает как и ранее.
Итого
В этой части мы познакомились с механизмом внедрения зависимостей в приложениях Blazor Hybrid, а также зарегистрировали свой собственный сервис и воспользовались им в компоненте Razor. Далее мы рассмотрим другие возможности Dependency Injection в Blazor Hybrid.

