Главная страница   /   18.2. Использование механизма внедрения зависимостей в ASP.NET MVC (ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

ASP.NET MVC 4 в действии

Джеффри Палермо

18.2. Использование механизма внедрения зависимостей в ASP.NET MVC

Одним из преимуществ ASP.NET MVC Framework является концепция разделения, которую он предоставляет. Когда вы разделяете ваш код на контроллеры, модели и представления, его становится легче понимать и поддерживать в работоспособном состоянии. Концепция разделения – это один из самых лучших атрибутов, которым может обладать ваш код, если вы хотите, чтобы он был работоспособным.

Представьте себе, что вы работаете над системой, которая позволяет пользователям загружать файлы на сервер. Это приложение должно содержать FileUploadController, который принимает в качестве параметра загружаемый файл и вставляет его в базу данных. Тем не менее, в случае если это большой бинарный файл, вместо того, чтобы хранить его в базе данных, вам может потребоваться хранить его на отдельном сервере-хранилище. Это означает, что контроллеру необходимо обрабатывать загружаемый файл и взаимодействовать как с базой данных, так и с файловой системой. То есть множество различных обязанностей может быть размещено в одном месте, и не трудно представить себе, что контроллер будет расти и расти до тех пор, пока им не станет трудно управлять.

Размещение слишком большого количества обязанностей в вашем контроллере – верный способ создания захламленного проекта, с которым так трудно работать, что вы чувствуете будто пробираетесь сквозь тину.

Ниже приведен краткий список того, что ваш контроллер, как правило, не должен делать:

  • Напрямую выполнять запросы обращения к данным.
  • Напрямую обращаться к файловой системе.
  • Напрямую отправлять e-mail.
  • Напрямую вызывать веб-сервисы.

Заметили схему? Любая внешняя зависимость от некоторого рода инфраструктуры – прекрасный кандидат для извлечения в интерфейс, который может использоваться вашим контроллером. Такое разделение имеет пару преимуществ:

  • Контроллер становится "тоньше" и, таким образом, более легким для понимания.
  • Контроллер становится тестируемым – вы можете записывать модульные тесты и заглушки для зависимостей, изолируя класс для тестирования.

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

Мы можем использовать механизм DI для достижения такой концепции разделения в наших контроллерах. Мы можем реализовывать DI в ASP.NET MVC приложениях с помощью фабрик контроллеров и DR (dependency resolver).

Пользовательские фабрики контроллеров

Фабрики контроллеров – важная возможность расширения в рамках ASP.NET MVC Framework. Они позволяют нам перенимать обязанности для создания экземпляров контроллеров. Мы можем использовать фабрику контроллеров для того, чтобы разрешить внедрение через конструкторы для наших контроллеров.

В идеале всем контроллерам необходимо содержать в себе по умолчанию конструктор без параметров. Это все потому, что DefaultControllerFactory (или если быть более конкретными, DefaultControllerActivator, который мы рассмотрим далее) для того, чтобы создать экземпляры контроллеров, полагается на вызов Activator.CreateInstance. Для иллюстрации этого давайте обратимся к примеру простого интерфейса, который может использоваться для генерации некоторого текста:

public interface IMessageProvider
{
	string GetMessage();
}

Реализация этого интерфейса просто возвращает строку:

public class SimpleMessageProvider : IMessageProvider
{
	public string GetMessage()
	{
		return "Hello Universe!";
	}
}

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

Листинг 18-4: Использование внедрения через конструкторы в контроллере
public class HomeController : Controller
{
	private IMessageProvider _messageProvider;
	public HomeController(IMessageProvider messageProvider)
	{
		_messageProvider = messageProvider;
	}

	public ActionResult Index()
	{
		ViewBag.Message = _messageProvider.GetMessage();
		return View();
	}
}

Строка 3: Хранит зависимость в отдельном поле

Строка 4: Внедряет зависимость через конструктор

Строка 11: Использование зависимости внутри метода действия

В данном примере HomeController принимает в свой конструктор IMessageProvider, который он затем хранит в закрытом поле. Действие Index использует провайдер для извлечения сообщения и хранит его в ViewBag, готовым к тому, чтобы быть переданным в представление. Когда это будет запускаться, в идеале, нам хотелось бы увидеть это сообщение отображенным на экране, как это продемонстрировано на рисунке 18-2.

Рисунок 18-2: Отображение сообщения, возвращаемого SimpleMessageProvider

Примечание.

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

К несчастью, получается совсем не так. Поскольку DefaultControllerActivator требует, чтобы контроллеры обладали не параметризованным конструктором, фреймворк выдает исключение, как это показано на рисунке 18-3.

Рисунок 18-3: По умолчанию ASP.NET MVC требует, чтобы контроллеры содержали конструктор по умолчанию

Вместо того чтобы полагаться на поведение MVC по умолчанию, мы можем дать фреймворку указание использовать DI-контейнер для создания экземпляров контроллеров посредством пользовательской фабрики контроллеров. Так же, как ранее, мы можем использовать для этого StructureMap. Мы начнем с создания пользовательского StructureMapControllerFactory.

Листинг 18-5: Фабрика контроллеров, возможная благодаря StructureMap
public class StructureMapControllerFactory : DefaultControllerFactory
{
	protected override IController GetControllerInstance(
		RequestContext requestContext,
		Type controllerType)
	{
		if (controllerType == null)
		{
			throw new HttpException(404, "Controller not found.");
		}
		return ObjectFactory.GetInstance(controllerType) as IController;
	}
}

Строка 7: Создает экземпляры только валидных контроллеров

Строка 11: Использование StructureMap для создания контроллера

StructureMapControllerFactory наследуется от DefaultControllerFactory и переопределяет метод GetControllerInstance. Этот метод принимает два параметра: первый параметр – это RequestContext, который предоставляет нам доступную информацию о текущем запросе (включая HttpContext и роут которого был выбран для обработки запроса), а второй – тип контроллера, который был выбран для управления запросом.

Нашей фабрике контроллеров сначала приходится проверять, является ли типом контроллера null, и выдавать исключение HTTP 404 Not Found, если это так. Это важная проверка, поскольку controllerType будет иметь тип null, если URL преобразуется в контроллер, который вы еще не создали, или если вы сделали опечатку в URL.

Вслед за этим мы просим ObjectFactory контейнера StructureMap создать экземпляр типа контроллера и вернуть его из метода. Это блокирует логику MVC создания экземпляров контроллеров, используемую по умолчанию.

DefaultControllerFactory

В листинге 18-5 мы переопределили метод GetControllerInstance для настройки того, как будут создаваться экземпляры контроллеров. DefaultControllerFactory обладает несколькими другими методами, которые можно переопределить.

Например, метод GetControllerType используется для поиска типа контроллера, который должен применяться для определенного имени контроллера, а метод ReleaseController можно переопределить таким образом, чтобы он обеспечивал пользовательскую логику постобработки, как только вызывается действие контроллера.

Актуальная логика создания экземпляра контроллера делегирована ControllerActivator, который мы рассмотрим далее.

Теперь нам необходимо настроить StructureMap и подключить новую фабрику контроллеров во фреймворк. Мы можем выполнить обе эти задачи внутри метода Application_Start файла Global.asax.

Листинг 18-6: Настройка StructureMapControllerFactory
protected void Application_Start()
{
	ObjectFactory.Initialize(cfg =>
	{
		cfg.For<IMessageProvider>()
		.Use<SimpleMessageProvider>();
	});
	ControllerBuilder.Current.SetControllerFactory(
		new StructureMapControllerFactory());
	AreaRegistration.RegisterAllAreas();
	RegisterRoutes(RouteTable.Routes);
}

Строки 3-7: Конфигурация маппинга типов

Строки 8-9: Установка фабрики контроллеров

Первое, что мы делаем – это вызываем метод Initialize для ObjectFactory для того, чтобы настроить преобразования между интерфейсами и конкретными типами. В данном примере мы преобразуем IMessageProvider к его реализации (SimpleMessageProvider).

Далее мы заменяем используемую в MVC по умолчанию фабрику контроллеров нашей StructureMapControllerFactory путем вызова метода SetControllerFactory для ControllerBuilder. Теперь каждый раз, когда фреймворку необходимо будет создавать экземпляр контроллера, он будет вызывать StructureMap, который знает, как правильно конструировать зависимости нашего контроллера.

Этот механизм использования пользовательской фабрики контроллеров для создания экземпляров контроллеров стал доступен со времен первой версии ASP.NET MVC. Несмотря на то, что этот механизм на сегодняшний день все еще является действующим, существует другой альтернативный подход, доступный в форме DR (dependency resolver).

Использование DR

Одной из новых возможностей, введенных в ASP.NET MVC 3, является DR (dependency resolver = механизм, который отвечает за создание зависимостей). Это реализация паттерна Service Locator, которая позволяет фреймворку запускать в действие ваш DI-контейнер всякий раз, когда фреймворку необходимо работать с реализацией определенного типа.

Так же, как и фабрика контроллеров, которую мы рассматривали до этого, DR может использоваться для создания экземпляров контроллеров в случае, если нам необходимо выполнить внедрение через конструктор. Тем не менее, DR также может использоваться для обеспечения реализаций других сервисов, используемых MVC Framework (мы рассмотрим их вкратце).

DR состоит из двух основных частей: статический класс DependencyResolver, который выступает в роли статического шлюза для разрешения зависимостей, и интерфейс IDependencyResolver. Этот интерфейс может быть реализован с помощью классов, которые знают, как разрешать зависимости (посредством использования DI-контейнера), а статический DependencyResolver будет запускать в действие эту реализацию для того, чтобы выполнить его работу.

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

Создание экземпляров контроллеров с помощью DR

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

Когда DefaultControllerFactory просят создать контроллер, он сначала просит DependencyResolver создать IControllerActivator. Если DR способен обеспечить реализацию, то фабрика просит активатор создать экземпляр контроллера. Если DependencyResolver не обеспечивает реализацию (что является поведением по умолчанию), то DefaultControllerFactory возвращается к тому, что просит DefaultControllerActivator создать экземпляр контроллера.

DefaultControllerActivator следует простому алгоритму – сначала он просит DependencyResolver создать экземпляр контроллера. Если экземпляр контроллера не создается, он возвращается к использованию Activator.CreateInstance, который требует, чтобы контролер по умолчанию обладал не параметризованным конструктором.

На рисунке 18-4 продемонстрирована блок-схема, которая описывает данный процесс.

Рисунок 18-4: Алгоритм создания экземпляра контроллера. DefaultControllerFactory сначала проверяет, может ли DependencyResolver создать экземпляр IControllerActivator. Если нет, то вместо него он использует DefaultControllerActivator, чтобы попытаться создать экземпляр контроллера.

Вы можете подумать, что это сбивает с толку, и окажетесь правы! Процесс размещения контроллера слегка свернут, что главным образом, обусловлено наследованием ASP.NET MVC – первоначально в нем не подразумевалось использование DI.

Если мы обеспечиваем свою собственную реализацию IDependencyResolver на основе StructureMap, мы можем исследовать этот процесс. Реализация StructureMapDependencyResolver продемонстрирована в следующем листинге.

Листинг 18-7: Реализация DR в StructureMap
public class StructureMapDependencyResolver : IDependencyResolver
{
	public object GetService(Type serviceType)
	{
		var instance = ObjectFactory.TryGetInstance(serviceType);

		if (instance == null
			&& !serviceType.IsAbstract
			&& !serviceType.IsInterface)
		{
			instance = ObjectFactory.GetInstance(serviceType);
		}
		return instance;
	}

	public IEnumerable<object> GetServices(Type serviceType)
	{
		return ObjectFactory.GetAllInstances(serviceType).Cast<object>();
	}
}

Строка 5: Попытка создать экземпляр предварительно зарегистрированного типа

Строки 7-12: Создание экземпляра незарегистрированного конкретного типа

Строка 18: Решение всех реализаций типа

StructureMapDependencyResolver сложнее, чем StructureMapControllerFactory, о котором мы писали ранее. Это является результатом некоторых предпосылок, внесенных инфраструктурой DependencyResolver.

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

Начнем с вызова метода TryGetInstance контейнера StructureMap, передавая тип. Как и подразумевает имя, этот метод пытается создать экземпляр конкретного типа, если он точно был зарегистрирован в контейнере. Если тип был зарегистрирован, то создается экземпляр типа. В противном случае возвращается null.

Но StructureMap не всегда требует точной регистрации типов. StructureMap достаточно умен, чтобы быть способным создавать экземпляры конкретных типов, если они не зарегистрированы (то же самое не применимо к интерфейсам, поскольку вам приходится обеспечивать преобразование между интерфейсами и реализацией). Самым очевидным использованием этого являются контроллеры – вы не пишите интерфейсы для каждого контроллера, поэтому StructureMap может создавать их экземпляры напрямую. Мы можем использовать для этого регулярный метод GetInstance контейнера StructureMap, но только для конкретных типов, которые еще не были разрешены предыдущим вызовом TryGetInstance.

Наконец, нам приходится реализовывать метод GetServices. Этот метод вызывается, когда MVC запрашивает множественные реализации определенного интерфейса, например, извлечение всех движков представлений, представленных интерфейсом IViewEngine. Это реализуется с помощью метода GetAllInstances контейнера StructureMap.

Мы можем зарегистрировать этот новый DependencyResolver путем размещения следующего кода в методе Application_Start файла Global.asax:

DependencyResolver.SetResolver(new StructureMapDependencyResolver());

Теперь MVC будет пытаться использовать StructureMap каждый раз, когда ему будет необходимо создать экземпляр либо контроллера, либо одного из его собственных внутренних компонентов.

DR или фабрика контроллеров?

Мы рассмотрели использование для реализации DI для контроллеров как DR, так и фабрик контроллеров, и вам может быть интересно, какой подход лучше.

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

Если все, что вам нужно сделать – это задействовать DI для контроллеров, то мы советуем вам придерживаться использования фабрики контроллеров в связи с ее упрощенностью. Помимо этого, определенные DI-контейнеры лучше подходят для использования в фабрике контроллеров.

Например, контейнер Windsor (с сайта http://castleproject.org) требует, чтобы контроллеры явно освобождались сразу же после того, как они были вызваны. Пользовательская фабрика контроллеров может использоваться для реализации такого поведения через метод ReleaseController, но не существует эквивалентного метода, доступного посредством DR, который мог бы привести к утечке памяти при использовании Windsor в ваших приложениях.

Дополнительные возможности расширения

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

Все компоненты, которые могут использовать механизм создания зависимостей (DR), продемонстрированы в таблице 18-1.

Таблица 18-1: Возможности расширения, которые используют DR
Компонент Описание
IControllerFactory Находит контроллер для данного запроса
IControllerActivator Создает экземпляр контроллера
IViewEngine Находит и отображает представления
IViewPageActivator Создает экземпляры представлений
IFilterProvider Извлекает фильтры для действия контроллера
IModelBinderProvider Получает механизм связывания данных (model binder) для определенного типа
ModelValidatorProvider Получает средства проверки допустимости для определенной модели
ModelMetadataProvider Получает метаданные для определенной модели
ValueProviderFactory Создает провайдер значения, который может использоваться для преобразования значения типа raw (например, из строки запроса) в значение, которое может участвовать в механизме связывания данных

Например, в главе 10 (листинг 10-1) мы создали пользовательский ModelBinderProvider под названием EntityModelBinderProvider, который мы использовали для создания экземпляра пользовательского механизма связывания данных модели при работе с объектами определенного типа. Он был зарегистрирован во фреймворке посредством добавления его в коллекцию ModelBinderProviders в Application_Start:

ModelBinderProviders.BinderProviders.Add(new EntityModelBinderProvider());

Вместо регистрации его таким образом мы могли бы зарегистрировать его в StructureMap в конфигурации нашей ObjectFactory:

ObjectFactory.Initialize(cfg =>
{
	cfg.For<IMessageProvider>().Use<SimpleMessageProvider>();
	cfg.For<IModelBinderProvider>().Use<EntityModelBinderProvider>();
});

Теперь фреймворк приобретает новый провайдер с помощью создания его посредством DR.

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

В этом разделе мы увидели, как DependencyResolver предоставляет альтернативный механизм для реализации DI в ASP.NET MVC как в контроллерах, так и в дополнительных областях расширения, например, ModelBinderProviders и фильтрах. Несмотря на то, что DR открывает дополнительные возможности расширения, он не может использоваться со всеми DI-контейнерами в связи с ограничениями его API.