Содержание
Внедрение зависимостей (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); } }
Здесь мы сделали следующее:
- Создали интерфейс
IMessageWriter
у которого определили метод WriteLine - Класс
ConsoleMessageWriter
реализует этот интерфейс - Класс
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
. В целом у этого класса определены следующие свойства:
Implementation |
Возвращает фабрику, используемую для создания экземпляров сервиса. |
Implementation |
Возвращает экземпляр, реализующий сервис. |
Implementation |
Возвращает объект Type , реализующий сервис. |
Lifetime |
Возвращает объект ServiceLifetime сервиса — жизненный цикл сервиса. |
Service |
Возвращает объект 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.