ASP.NET Core: Внедрение зависимостей

ASP.NET Core logoПоддержка шаблона "Внедрение зависимостей" (Dependency Injection или сокращенно DI) присутствует в ASP.NET уже достаточно давно. Рассмотрим какие возможности использования DI существуют в ASP.NET Core и ASP.NET Core MVC, включая новые, по сравнению с MVC 5, способы внедрения зависимостей через параметр метода и свойство.

Шаблон "Внедрение зависимостей"

Внедрение зависимостей это шаблон, согласно которому компонент (класс) отдает заботу о создании необходимых для его работы других компонентов во вне. Это позволяет самому компоненту избавиться от необходимости знать:

  • как создаются другие компоненты и управлять их времени жизни;
  • конкретный тип, реализующий интерфейс в случае зависимости от интерфейса.

Последнее также позволяет изменять реализацию интерфейсов не модифицируя сам компонент.

Обязанности по обеспечению компонента экземплярами необходимых зависимостей берет на себя, как правило, фреймворк или специальная библиотека. ASP.NET Core поддерживает данный шаблон "из коробки". При этом он предоставляет следующий способ передачи зависимостей компоненту:

  • Через конструктор класса (для любых классов).

Библиотека ASP.NET Core MVC также добавляет еще 2 способа:

  • Через параметр метода класса.(для Контроллеров MVC).
  • Через свойство класса (для Представлений MVC).

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

Внедрение зависимости

Внедрение зависимости через конструктор

Самый простой и единственный, поддерживаемый непосредственно ASP.NET Core, способ передачи зависимости – через конструктор. Выглядит он следующим образом:

public class ProductsController : Controller
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        this._productService = productService;
    }

    public IActionResult Show(ProductReuest request)
    {
        IReadOnlyCollection<Product> products = this._productService.GetAll();

        return this.View(products);
    }

    …
}

Достаточно объявить необходимую зависимость как параметр конструктора, чтобы ASP.NET Core передал соответствующий ее экземпляр при создании объекта. В данном примере был использован класс MVC Контроллера, но это может быть класс любого типа.

Внедрение зависимости через параметр метода

В MVC Контроллерах часто встречается ситуация, когда зависимость необходима только в одном методе. Конечно, можно передать её через конструктор. Но в этом случае, за некоторыми исключениями, экземпляр соответствующего класса будет порождаться при каждом создании Контроллера, не зависимо от того, какой метод будет потом вызван. В такой ситуации больше подходит способ передачи зависимости через параметр конкретного метода.

Чтобы указать, что параметр метода используется для внедрения зависимости, его необходимо отметить атрибутом [FromService].

Перепишем пример выше с использованием этого подхода:

public class ProductsController : Controller
{
    public IActionResult Show(ProductRequest request, [FromServices] IProductService productService)
    {
        IReadOnlyCollection<Product> products = productService.GetAll();

        return this.View(products);
    }

    …
}

При обращении к Действию Show() в параметр productService будет передан экземпляр класса, реализующий IProductService. Параметр request будет заполнен данными из запроса. При обращении к другим Действиям Контроллера IProductService запрошен не будет.

Внедрение зависимости в представление (view)

В ASP.NET Core также существует еще один специфичный для MVC способ внедрения зависимости. Он позволяет передать зависимость в Представление не используя контроллер. Для этого используется директива @inject.

Пример:

@using DemoApp.Services
@inject IInfoService InfoService
<!DOCTYPE html>
<html>
<body>
    <div>
        <p>Info: @InfoService.GetInfo()</p>
    </div>
</body>
</html>

Кроме того @inject позволяет переопределять уже существующие зависимости. Например, можно создать собственную реализацию IHtmlHelper и передать ее в свойство Html вместо стандартной:

@inject MyHtmlHelper Html

Регистрация зависимостей

Теперь разберемся откуда ASP.NET Сore берет все необходимые зависимости. Для этого существует специальный контейнер, в котором все они должны быть зарегистрированы..

Добавление новых классов в контейнер происходит в методе ConfigureServices() класса Startup. Для этого у IServiceCollection существует метод Add(ServiceDescriptor serviceDescriptor).

Класс ServiceDescriptor описывает параметры зависимости и содержит следующие свойства:

  • ServiceLifetime Lifetime – Время жизни экземпляра класса:
    • Transient – Новый экземпляр будет создан при каждом обращении к контейнеру.
    • Scoped – Новый экземпляр будет создан для каждого запроса к серверу. При этом в рамках самого запроса всегда будет передаваться один и тоже экземпляр.
    • Singleton – Всегда будет использоваться один и тоже экземпляр данного типа (шаблон Одиночка).
  • Type ServiceType – Тип регистрируемого сервиса, как правило интерфейса. Для примера выше это будет IProductService.
  • Один из вариантов, указывающих на способ создания реализации ServiceType:
    • Type ImplementationType – Тип реализации. Будет создан в контейнере при обращении.
    • object ImplementationInstance – Созданный заранее объект типа ServiceType (только для Singleton);
    • public Func<IServiceProvider, object> ImplementationFactoryФабричный метод для создания экземпляра типа ServiceType.

Для более компактной записи существуют методы-расширения с различными перегрузками для разных вариантов сочетания описанных выше значений:

  • AddSingleton(…)
  • AddScoped(…)
  • AddTransient(…)

В качестве примера, зарегистрируем сервис IProductService и его реализацию ProductService как Singleton.

public class Startup
{
    …

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<IProductService, ProductService>();

        …
    }

    …
}

Зависимости не обязаны иметь одинаковый вариант времени жизни. Например, сервис, заданный как Transient, может зависеть от другого сервиса, созданного как Singleton.

Замена стандартной реализации

Могут быть различные причины, почему разработчика не устраивает стандартная ASP.NET Core реализация внедрения зависимостей. И это не проблема, потому что заменить ее на другую достаточно просто:

  1. Метод ConfigureServices(IServiceCollection services) должен вернуть IServiceProvider вместо void.
  2. Реализация IServiceProvider по сути является контейнером, который и отвечает за предоставление необходимых экземпляров по указанному типу.

Интерфейс IServiceProvider очень простой:

public interface IServiceProvider
{
    object GetService(Type serviceType);
}

Метод GetService(…) должен вернуть или объект указанного типа или же null если тип не был зарегистрирован.

Важно отменить, что в таком контейнере должны быть зарегистрированы:

  1. Все типы зависимостей, созданные разработчиком приложения.
  2. Все типы необходимые для ASP.NET Core и подключенных сторонних библиотек. Их список находится в переменной services, передаваемой в метод ConfigureServices(…). Изначально в списке есть только типы, необходимые для ASP.NET Core. Однако, как правило, в самом методе ConfigureServices(…) в него добавляются типы для различных библиотек (например вызов AddMvc() в примере ниже) .

Сторонние библиотеки внедрения зависимостей обычно уже содержат необходимую для интеграции функциональность. Вот пример кода, заменяющий стандартную реализацию на  библиотеку Autofac:

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    // Добавляем в список сервисы библиотек
    services.AddMvc();
    ...

    // Добавляем сервисы приложения стандартным путем
    services.AddSingleton<IProductService, ProductService>();
    ...

    // Создаем контейнер Autofac
    var containerBuilder = new ContainerBuilder();

    // Регистрируем зависимости приложения
    // используя возможности Autofac
    containerBuilder.RegisterModule<DalModule>();

    // Регистрируем зависимости из IServiceCollection
    containerBuilder.Populate(services);
  
    // Используем контейнер Autofac для DI
    var container = containerBuilder.Build();
    return new AutofacServiceProvider(container);
}

Добавить комментарий