Dependency injection (DI) или внедрение зависимостей представляет механизм, который позволяет сделать взаимодействующие в приложении объекты слабосвязанными. Такие объекты связаны между собой через абстракции, например, через интерфейсы, что делает всю систему более гибкой, более адаптируемой и расширяемой.
В центре подобного механизма находится понятие зависимость - некоторая сущность, от которой зависит другая сущность. Например:
class Logger { public void Log(string message) => Console.WriteLine(message); } class Message { Logger logger = new Logger(); public string Text { get; set; } = ""; public void Print() => logger.Log(Text); }
Здесь сущность Message, которая представляет некоторое сообщение, зависит от другой сущности - Logger, которая представляет логгер. В методе Print()
класса Message имитируется логгирование текста сообщения путем вызова у объекта Logger метода Log, который выводит сообщение на консоль.
Однако здесь класс Message тесно связан с классом Loger. Класс Message
отвечает за создание объекта Logger. Это имеет ряд недостатков. Прежде всего, если мы захотим вместо класса Logger использовать другой тип тип логгера, например, логгировать в файл, а не на консоль,
то нам придется менять класс Message. Один класс не составит труда поменять, но если в проекте таких классов много, то поменять во всех класс Logger на
другой будет труднее. Кроме того, класс Logger может иметь свои зависимости, которые тоже может потребоваться поменять. В итоге такими системами сложнее управлять и сложнее
тестировать.
Чтобы отвязать объект Logger от класса Message, мы можем создать абстракцию, которая будет представлять логгер, и передавать ее извне в объект Message:
interface ILogger { void Log(string message); } class Logger : ILogger { public void Log(string message) => Console.WriteLine(message); } class Message { ILogger logger; public string Text { get; set; } = ""; public Message(ILogger logger) { this.logger = logger; } public void Print() => logger.Log(Text); }
Теперь класс Message не зависит от конкретной реализации класса Logger - это может быть любая реализация интерфейса ILogger. Кроме того, создание объекта логгера выносится во внешний код. Класс Message больше ничего не знает о логгере кроме того, что у него есть метод Log, который позволяет логгировать его текст.
Тем не менее остается проблема управления подобными зависимостями, особенно если это касается больших приложений. Нередко для установки зависимостей в подобных системах используются специальные контейнеры - IoC-контейнеры (Inversion of Control). Такие контейнеры служат своего рода фабриками, которые устанавливают зависимости между абстракциями и конкретными объектами и, как правило, управляют созданием этих объектов.
Преимуществом ASP.NET Core в этом оношении является то, что фреймворк уже по умолчанию имеет встроенный контейнер внедрения зависимостей, который представлен интерфейсом IServiceProvider. А сами зависимости еще называются сервисами, собственно поэтому контейнер можно назвать провайдером сервисов. Этот контейнер отвечает за сопоставление зависимостей с конкретными типами и за внедрение зависимостей в различные объекты.
За управление сервисами в приложении в классе WebApplicationBuilder определено свойство Services, которое представляет объект IServiceCollection - коллекцию сервисов:
WebApplicationBuilder builder = WebApplication.CreateBuilder(); IServiceCollection allServices = builder.Services; // коллекция сервисов
И даже если мы не добавляем в эту коллекцию никаких сервисов, IServiceCollection уже содержит ряд сервисов по умолчанию
Как видно на скриншоте, в коллекции IServiceCollection 81 сервис, который мы можем использовать в приложении. Это такие сервисы, как ILogger<T>, ILoggerFactory, IWebHostEnvironment и ряд других. Они добавляются по умолчанию инфраструктурой ASP.NET Core. И мы их можем использовать в различных частях приложения.
Каждый сервис в коллекции IServiceCollection представляет объект ServiceDescriptor, который несет некоторую информацию. В частности, наиболее важные свойства этого объекта:
ServiceType: тип сервиса
ImplementationType: тип реализации сервиса
Lifetime: жизненный цикл сервиса
Например, получим все сервисы, которые добавлены в приложение:
using System.Text; var builder = WebApplication.CreateBuilder(); var services = builder.Services; var app = builder.Build(); app.Run(async context => { var sb = new StringBuilder(); sb.Append("<h1>Все сервисы</h1>"); sb.Append("<table>"); sb.Append("<tr><th>Тип</th><th>Lifetime</th><th>Реализация</th></tr>"); foreach (var svc in services) { sb.Append("<tr>"); sb.Append($"<td>{svc.ServiceType.FullName}</td>"); sb.Append($"<td>{svc.Lifetime}</td>"); sb.Append($"<td>{svc.ImplementationType?.FullName}</td>"); sb.Append("</tr>"); } sb.Append("</table>"); context.Response.ContentType = "text/html;charset=utf-8"; await context.Response.WriteAsync(sb.ToString()); }); app.Run();
Кроме ряда подключаемых по умолчанию сервисов ASP.NET Core имеет еще ряд встроенных сервисов, которые мы можем подключать в приложение при необходимости.
Все сервисы и компоненты middleware, которые предоставляются ASP.NET по умолчанию, регистрируются в приложение с помощью методов расширений IServiceCollection,
имеющих общую форму Add[название_сервиса]
.
Например:
var builder = WebApplication.CreateBuilder(); builder.Services.AddMvc();
Для объекта IServiceCollection определено ряд методов расширений, которые начинаются на Add, как, например, AddMvc()
. Эти методы добавляют
в объект IServiceCollection соответствующие сервисы. Например, AddMvc()
добавляет в приложение сервисы MVC, благодаря чему мы сможем их использовать
в приложении.