Внедрение зависимостей и контроллеры¶
Контроллеры 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), чтобы понять, как каждая из этих опций повлияет на поведение вашего сервиса.
После настройки сервиса при запуске приложения и переходе к домашней странице вы увидите такое сообщение:

Совет
См. 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 контроллера, поскольку тогда не будет статической зависимости или прямого создания экземпляров классов настроек внутри класса контроллера.