Содержание
Внедрение зависимостей (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.

