Внедрение зависимостей и контроллеры

Steve Smith

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

Просмотрите или скачайте пример с GitHub.

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

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

Внедрение конструктора

Встроенная поддержка внедрения конструктора в ASP.NET Core распространяется на MVC контроллеры. Если вы просто добавите тип сервиса в ваш контроллер в качестве параметра конструктора, ASP.NET попытается разрешить этот тип, используя свой встроенный контейнер сервисов. Сервисы обычно, но не всегда, определяются с помощью интерфейсов. Например, если в вашем приложении есть бизнес логика, которая зависит от текущего времени, вы можете внедрить сервис, который извлекает время (вместо того, чтобы все это жестко кодировать), и тогда реализации, в которых используется время, будут хорошо работать.

using System;

namespace ControllerDI.Interfaces
{
    public interface IDateTime
    {
        DateTime Now { get; }
    }
}

Реализация интерфейса, который использует системные часы при рантайме, довольно тривиальна:

using System;
using ControllerDI.Interfaces;

namespace ControllerDI.Services
{
    public class SystemDateTime : IDateTime
    {
        public DateTime Now
        {
            get { return DateTime.Now; }
        }
    }
}

Теперь мы можем использовать сервис в нашем контроллере. В данном случае мы добавили некоторую логику в метод Index в HomeController для отображения приветствия пользователя, основанного на времени.

using ControllerDI.Interfaces;
using Microsoft.AspNetCore.Mvc;

namespace ControllerDI.Controllers
{
    public class HomeController : Controller
    {
        private readonly IDateTime _dateTime;

        public HomeController(IDateTime dateTime)
        {
            _dateTime = dateTime;
        }

        public IActionResult Index()
        {
            var serverTime = _dateTime.Now;
            if (serverTime.Hour < 12)
            {
                ViewData["Message"] = "It's morning here - Good Morning!";
            }
            else if (serverTime.Hour < 17)
            {
                ViewData["Message"] = "It's afternoon here - Good Afternoon!";
            }
            else
            {
                ViewData["Message"] = "It's evening here - Good Evening!";
            }
            return View();
        }
    }
}

Если мы теперь запустим приложение, то получим ошибку:

При обработке запроса появится необработанное исключение.

InvalidOperationException: Unable to resolve service for type 'ControllerDI.Interfaces.IDateTime' while attempting to activate 'ControllerDI.Controllers.HomeController'.
Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)

Эта ошибка появится, если мы не настроили сервис в методе ConfigureServices класса Startup. Чтобы указать это, запросы к IDateTime должны быть разрешены с помощью экземпляра SystemDateTime. Добавьте следующие сроки из листинга внизу в ваш метод ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    // Add application services.
    services.AddTransient<IDateTime, SystemDateTime>();
}

Примечание

Этот конкретный сервис должен быть реализован с помощью любой из данных опций: Transient, Scoped, или Singleton. См. Внедрение зависимостей (Dependency Injection), чтобы понять, как каждая из этих опций повлияет на поведение вашего сервиса.

После настройки сервиса при запуске приложения и переходе к домашней странице вы увидите такое сообщение:

../../_images/server-greeting.png

Совет

См. https://docs.asp.net/en/latest/mvc/controllers/testing.html, чтобы лучше изучить `напрямую запрашиваемые зависимости `<http://deviq.com/explicit-dependencies-principle>`_.

У вас должен быть только один конструктор для классов, запрашивающих сервисы. Если у вас больше одного конструктора, вы получите исключение:

An unhandled exception occurred while processing the request.

InvalidOperationException: Multiple constructors accepting all given argument types have been found in type ‘ControllerDI.Controllers.HomeController’. There should only be one applicable constructor. Microsoft.Extensions.DependencyInjection.ActivatorUtilities.FindApplicableConstructor(Type instanceType, Type[] argumentTypes, ConstructorInfo& matchingConstructor, Nullable`1[]& parameterMap)

Здесь говорится, что вы можете исправить эту проблему, если у вас будет только один конструктор. Вы также можете :ref:`заменить поддержку внедрения зависимостей по умолчанию сторонней реализацией <replacing-the-default-services-container>`_, многие из которых поддерживают несколько конструкторов.

Внедрение метода действия с помощью FromServices

Иногда вам не нужен сервис для более чем одного действия внутри контроллера. В данном случае имеет смысл внедрить сервис в качестве параметра в метод действия. Это можно сделать, если пометить параметр атрибутом [FromServices]:

public IActionResult About([FromServices] IDateTime dateTime)
{
    ViewData["Message"] = "Currently on the server the time is " + dateTime.Now;

    return View();
}

Доступ к настройкам из контроллера

Часто мы получаем доступ к приложению или настройкам из контроллера. При таком доступе нужно использовать паттерн Options, описанный в конфигурации. Как правило, не стоит запрашивать настройки напрямую из контроллера, используя внедрение зависимостей. Лучше запросить экземпляр IOptions<T>, где T - это нужный конфигурационный класс.

Чтобы работать с паттерном Options, нужно создать класс, представляющий опции, например:

namespace ControllerDI.Model
{
    public class SampleWebSettings
    {
        public string Title { get; set; }
        public int Updates { get; set; }
    }
}

Затем вам нужно настроить приложение, чтобы оно использовало модель Options и добавить ваш конфигурационный класс к коллекции сервисов в ConfigureServices:

public Startup(IHostingEnvironment env)
{
    var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("samplewebsettings.json");
    Configuration = builder.Build();
}

public IConfigurationRoot Configuration { get; set; }

// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
    // Required to use the Options<T> pattern
    services.AddOptions();

    // Add settings from configuration
    services.Configure<SampleWebSettings>(Configuration);

    // Uncomment to add settings from code
    //services.Configure<SampleWebSettings>(settings =>
    //{
    //    settings.Updates = 17;
    //});

    services.AddMvc();

    // Add application services.
    services.AddTransient<IDateTime, SystemDateTime>();
}

Примечание

В листинге выше мы настраиваем приложение, чтобы оно считывало настройки из JSON файла. Также вы можете поработать с настройками только в самом коде, как показано в комментарии сверху. См. Конфигурация.

После того как вы указали строго типизированный конфигурационный объект (в данном случае SampleWebSettings) и добавили его к коллекции сервисов, вы можете запросить его из любого контроллера или метода действия, запросив экземпляр IOptions<T> (в данном случае IOptions<SampleWebSettings>). Здесь вы увидите, как запрашиваются настройки из контроллера:

    public class SettingsController : Controller
    {
        private readonly SampleWebSettings _settings;

        public SettingsController(IOptions<SampleWebSettings> settingsOptions)
        {
            _settings = settingsOptions.Value;
        }

        public IActionResult Index()
        {
            ViewData["Title"] = _settings.Title;
            ViewData["Updates"] = _settings.Updates;
            return View();
        }
    }

Если вы будете следовать паттерну Options, то настройки и конфигурация могут быть отделены друг от друга, и контроллер будет следовать разделению ответственности, поскольку ему не нужно знать, как или где находить информацию о настройках. Также это облегчает модульное тестирование Testing Controller Logic контроллера, поскольку тогда не будет статической зависимости или прямого создания экземпляров классов настроек внутри класса контроллера.

Поделись хорошей новостью с друзьями!
Следи за новостями!