Dependency Injection (внедрение зависимостей) в ASP.NET Core

Внедрение зависимостей (Dependency Injection, DI) в .NET является встроенной частью платформы. Это механизм, позволяющий сделать взаимодействующие в приложении объекты слабосвязанными — такие объекты связываются через абстракции (чаще всего — интерфейсы). Чтобы разобраться с этим механизмом и понять в чем его преимущества, рассмотрим небольшой пример.

Пример Dependency Injection

В основе механизма DI стоит понятие «зависимость». Зависимость — это любой объект, от которого зависит другой объект. Рассмотрим следующий пример:

public class MessageWriter
{
    public void WriteLine(string message)
    { 
        Console.WriteLine(message); 
    }
}

public class Messenger
{
    MessageWriter _writer = new();
    public void SendMessage(string message) 
    {
        _writer.WriteLine(message);
    }
}

Здесь класс Messanger производит отправку какого-либо сообщению и зависит от другого класса — MessageWriter у которого определен метод WriteLine, отправляющий строку текста в консоль. В этом примере прослеживается прямая зависимость: Messanger тесно связан с MessageWriter и создает экземпляр этого класса. С точки зрения работоспособности — код рабочий. Например, я могу спокойно использовать класс Messanger в приложении ASP.NET Core, хотя бы так:

public class Program
 {
     public static void Main(string[] args)
     {
         var builder = WebApplication.CreateBuilder(args);
         var app = builder.Build();

         app.Run(async context =>
         {
             Messenger messenger = new Messenger();  
             messenger.SendMessage("Hello");
             await context.Response.WriteAsync("Hello, ASP.NET Core"); 
         });

         app.Run();
     }
 }

Приложение будет работать и даже выводить в консоль строку. Но с точки зрения поддержки такого проекта возникает сразу ряд проблем: если мы захотим, чтобы Messanger выводил сообщение не в консоль, а на html-страницу или вообще писать текст в файл — нам придётся либо переписывать класс Messanger и создавать новый класс MessageWriter . А если у MessageWriter имеются какие-то свои зависимости, то в итоге, получим массу проблем по поддержек такого проекта. В приложении может быть масса классов, использующих MessageWriter, что тоже не облегчает задачу по изменению способа доставки сообщения.

Следовательно, нам необходимо каким-либо образом «отвязать» класс Messanger от класса MessageWriter. Сделать это можно, используя интерфейсы. Перепишем наш код для отправки сообщений следующим образом:

public interface IMessageWriter
{
    void WriteLine(string message);
}

public class ConsoleMessageWriter: IMessageWriter
{
    public void WriteLine(string message)
    { 
        Console.WriteLine(message); 
    }
}

public class Messenger
{
    IMessageWriter _writer;

    public Messenger(IMessageWriter writer)
    {
        _writer = writer;
    }

    public void SendMessage(string message) 
    {
        _writer.WriteLine(message);
    }
}

Здесь мы сделали следующее:

  1. Создали интерфейс IMessageWriter у которого определили метод WriteLine
  2. Класс ConsoleMessageWriter реализует этот интерфейс
  3. Класс Messenger теперь ничего не знает про реализацию ConsoleMessageWriter, но знает, что у этого IMessageWriter имеется метод WriteLine 

Остается вопрос — как управлять такими зависимостями? В ASP.NET Core по умолчанию имеет встроенный контейнер DI, который представлен интерфейсом IServiceProvider. Сами же зависимости часто называются сервисами — отсюда, видимо, и название интерфейса.

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

Штатные сервисы ASP.NET Core

Даже пустое приложение ASP.NET Core уже имеет в своем составе ряд сервисов, каждый из которых отвечает за какие-либо операции. В самом начале, когда мы изучали класс WebApplicationBuilder, мы упоминали про его свойство Services, в котором и представлены все сервисы приложения. Для начала посмотрим, какие сведения содержит это свойство в пустом приложении ASP.NET Core.

Сервисы ASP.NET Core по умолчанию

Создадим новое пустое приложение ASP.NET Core и напишем такой код:

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        
        var app = builder.Build();

        IServiceCollection services = builder.Services;//получили список сервисов

        app.Run(async context =>
        {
           context.Response.ContentType = "text/html; charset=utf-8";

           await context.Response.WriteAsync($"<h1>Перечень сервисов ({services.Count})</h1>"); 

            StringBuilder sb = new StringBuilder();
            sb.Append("<table >");
            sb.Append("<thead><tr><td>Тип</td><td>Жизненный цикл</td><td>Реализация</td></tr><thead>");
            foreach (ServiceDescriptor svc in services)
            {
                sb.Append("<tr>");
                sb.Append($"<td>{svc.ServiceType.Name}</td>");
                sb.Append($"<td>{svc.Lifetime}</td>");
                sb.Append($"<td>{svc.ImplementationType?.Name}</td>");
                sb.Append("</tr>");
            }
            sb.Append("</table>");
            await context.Response.WriteAsync(sb.ToString());

        });

        app.Run();
    }
}

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

IServiceCollection services = builder.Services;//получили список сервисов

IServiceCollection — это коллекция объектов типа ServiceDescriptor. В middleware мы создаем html-таблицу, состоящую из трех столбцов. Каждый столбец содержит описание наиболее часто используемых свойств объекта ServiceDescriptor. В целом у этого класса определены следующие свойства:

ImplementationFactory Возвращает фабрику, используемую для создания экземпляров сервиса.
ImplementationInstance Возвращает экземпляр, реализующий сервис.
ImplementationType Возвращает объект Type , реализующий сервис.
Lifetime Возвращает объект ServiceLifetime сервиса — жизненный цикл сервиса.
ServiceType Возвращает объект Type сервиса.

 

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

Впечатляющий список на 92 сервиса в пустом приложении ASP.NET Core — здесь и логгеры и сервисы, отвечающие за настройку приложения, работу с конфигурациями и ещё много всяких сервисов. Так что пустое приложение оказывается не такое уж и пустое. Более того, мы можем воспользоваться и подключить в приложение другие штатные сервисы ASP.NET Core, которые пока отключены.

Регистрация штатных сервисов ASP.NET Core

Все сервисы, которые предоставляются в ASP.NET Core по умолчанию, регистрируются в приложении с помощью методов расширений IServiceCollection, имеющих общую форму Add[название_сервиса]. Вот как может выглядеть список всех штатных сервисов, которые мы можем зарегистрировать:

Список методов Add[xxxx] достаточно обширный и содержит, например, такие методы как AddDirectoryBrowser(), AddAuthentication() и т.д. Впервые увидев этот список, возможно, возникнет вопрос: в чем отличие этих методов от аналогичных, которые мы использовали ранее, например, UseDirectoryBrowser()? Здесь стоит немного разобраться в том, что подразумевается под сервисами (Services) и компонентами middleware.

Отличие сервиса от компонента middleware

Middleware Сервис
Встраиваются в конвейер обработки запросов через методы расширения IApplicationBuilder  Регистрируется в контейнере DI через методы расширения IServiceCollection
обязательно должен знать контекст запроса (HttpContext) может ничего не знать про контекст запроса (HttpContext)
добавляет какие-либо возможности по обработке запроса (добавить заголовок, отправить текст, файл пользователю и т.д.), т.е. находятся как бы между нашим приложением и сервером, например, Kestrel Добавляют новые возможности приложению в целом, например, логирование операций, поддержку MVC и т.д.
Выполняются в порядке добавления в конвейер, при этом каждый компонент обрабатывает запрос/ответ и передает его следующему компоненту Могут использоваться в других сервисах и вызываться компонентами middleware

Регистрация своего сервиса в контейнере DI

Закончим эту часть про DI регистрацией выше разработанного сервиса для отправки сообщений. В качестве сервиса будет выступать IMessageWriter:

public static void Main(string[] args)
{
    var builder = WebApplication.CreateBuilder(args);
    
    //добавляем сервис
    builder.Services.AddTransient<IMessageWriter, ConsoleMessageWriter>();
    
    var app = builder.Build();

    
    app.Run(async context =>
    {
       context.Response.ContentType = "text/html; charset=utf-8";

       string message = "Hello, service!";
       
        //получаем сервис
       var writer = app.Services.GetService<IMessageWriter>();
       //работаем с сервисом
       writer?.WriteLine(message);

       await context.Response.WriteAsync($"<h1>{message}</h1>");

    });

    app.Run();
}

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

builder.Services.AddTransient<IMessageWriter, ConsoleMessageWriter>();

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

Во-вторых, мы получаем объект сервиса в компоненте middleware, используя метод GetService:

var writer = app.Services.GetService<IMessageWriter>();

и пишем в консоль строку:

writer?.WriteLine(message);

сам же компонент middleware отправляет пользователю эту же строку, только в формате HTML:

context.Response.ContentType = "text/html; charset=utf-8"; 
string message = "Hello, service!"; 
await context.Response.WriteAsync($"<h1>{message}</h1>");

Итого

Сегодня мы познакомились с таким новым для нас механизмом как Dependency Injection (внедрение зависимостей) с помощью которой мы можем регистрировать в приложениях ASP.NET Core свои сервисы или использовать штатные для добавления нашему приложению новых возможностей. Конечно, тема DI в ASP.NET Core на этом не исчерпывается и в следующих частях мы подробнее рассмотрим работу с DI и сервисами ASP.NET Core.

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