Построение ASP.NET MVC приложений

ASP.NET MVC был создан с выраженным стремлением стать DI-дружественным, и именно таким он и является. Он не вынуждает использовать механизм внедрения зависимостей, но с легкостью разрешает его, не заставляя нас при этом делать предположения о том, какой вид механизма внедрения зависимостей мы будем применять. Мы можем использовать Poor's Man DI или такой DI-контейнер, какой только пожелаем.

Расширяемость ASP.NET MVC

Как всегда и происходит с механизмом внедрения зависимостей, ключ к его применению заключается в обнаружении корректных мест расширяемости. В ASP.NET MVC таким ключом является интерфейс под названием IControllerFactory. Рисунок 7-5 иллюстрирует то, как он вписывается в фреймворк.

Рисунок 7-5: Когда рабочая среда ASP.NET MVC получает запрос, он просит свою фабрику контроллеров создать Controller для запрашиваемого URL. Фабрика контроллеров определяет корректный тип контроллера, который используется для данного запроса, создает и возвращает новый экземпляр этого типа. Затем ASP.NET MVC вызывает соответствующий метод действия для экземпляра Controller. После создания экземпляра контроллера ASP.NET MVC дает фабрике контроллеров освободить ресурсы путем вызова ReleaseController.

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

Контроллер не должен выполнять такие действия сам, а должен делегировать работу соответствующим зависимостям. Это именно то место, где в игру вступает механизм внедрения зависимостей. Мы хотим уметь снабжать зависимостями данный класс Controller, в идеале с помощью внедрения через конструктор. Это можно сделать при помощи пользовательского IControllerFactory.

IDependencyResolver

После выхода в 2011 году ASP.NET MVC 3 одной из новых реализованных возможностей стала "поддержка DI". Оказалось, что эта поддержка сосредоточена вокруг нового интерфейса с названием IDependencyResolver. Этот интерфейс и тот способ, при помощи которого он используется в фреймворке ASP.NET MVC, являются проблематичными.

На концептуальном уровне предполагалось использовать IDependencyResolver в качестве Service Locator, и именно так фреймворк его и использует.

На более конкретном уровне интерфейс обладает ограниченной полезностью, поскольку в нем отсутствует метод Release. Другими словами, мы не можем правильно управлять жизненным циклом диаграмм объектов посредством этого интерфейса. Для некоторых DI-контейнеров этот факт гарантирует утечку ресурсов.

С учетом его текущего воплощения я считаю, что безопаснее и правильнее будет игнорировать IDependencyResolver. Ирония данной ситуации заключается в том, что истинный механизм внедрения зависимостей поддерживался ASP.NET MVC еще со времен первой его версии через интерфейс IControllerFactory.

Создание пользовательской фабрики контроллеров

ASP.NET MVC поставляется с DefaultControllerFactory, которая требует, чтобы классы Controller имели конструктор по умолчанию. Именно разумное поведение по умолчанию не вынуждает нас использовать механизм внедрения зависимостей, если мы этого не хотим. Тем не менее, конструкторы по умолчанию и механизм внедрения через конструктор являются взаимно исключающими, поэтому нам необходимо изменить это поведение посредством реализации пользовательской фабрики контроллеров.

Это не так уж сложно. Для этого необходимо реализовать интерфейс IControllerFactory.

public interface IControllerFactory
{
	IController CreateController(RequestContext requestContext,
		string controllerName);
	SessionStateBehavior GetControllerSessionBehavior(
		RequestContext requestContext, string controllerName);
	void ReleaseController(IController controller);
}

Метод CreateController предоставляет RequestContext, который содержит такую информацию, как HttpContext, тогда как controllerName указывает на то, какой контроллер запрашивается.

Вы можете решить игнорировать RequestContext и использовать только controllerName для определения того, какой контроллер необходимо вернуть. Вне зависимости от того, что вы делаете, этот метод является местом, в котором вы получаете шанс подключить все необходимые зависимости и поставить их в контроллер прежде, чем вернуть экземпляр. Вы увидите пример в разделе "Пример: реализация CommerceControllerFactory".

Если вы создали какие-либо ресурсы, которые должны быть явно уничтожены, то вы можете сделать это при вызове метода ReleaseController.

Подсказка

DefaultControllerFactory реализует IControllerFactory и имеет несколько виртуальных методов. Вместо того чтобы реализовывать IControllerFactory с самого начала, часто легче наследовать его от DefaultControllerFactory.

Несмотря на то, что реализация пользовательской фабрики контроллеров является трудной частью, она не будет использоваться, пока мы не скажем о ней ASP.NET MVC.

Регистрация пользовательской фабрики контроллеров

Пользовательские фабрики контроллеров регистрируются как часть последовательности запуска приложения – обычно в файле Global.asax. Они регистрируются при помощи вызова ControllerBuilder.Current.SetControllerFactory. Ниже приведен фрагмент из шаблонного приложения Commerce:

var controllerFactory = new CommerceControllerFactory();
ControllerBuilder.Current.SetControllerFactory(controllerFactory);

Этот пример создает и определяет новый экземпляр пользовательской CommerceControllerFactory. ASP.NET MVC теперь будет использовать экземпляр controllerFactory в качестве фабрики контроллеров данного приложения.

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

Пример: реализация CommerceControllerFactory

Для шаблонного приложения Commerce нужна пользовательская фабрика контроллеров, чтобы подсоединить необходимые зависимости к контроллерам. Несмотря на то, что полноценная диаграмма зависимостей для всех контроллеров значительно глубже, с точки зрения самих контроллеров, соединение всех ближайших зависимостей достигает трех элементов, что продемонстрировано на рисунке 7-6.

Рисунок 7-6: Диаграмма зависимостей для трех контроллеров шаблонного приложения Commerce. Конкретные реализации каждой из трех зависимостей имеют другие зависимости, но они здесь не показаны. BasketController и HomeController имеют одну общую зависимость CurrencyProvider. AccountController унаследован неизмененным от шаблона ASP.NET MVC по умолчанию; поскольку он использует Bastard Injection, он не имеет неразрешенных зависимостей

Несмотря на то, что вы могли бы реализовать IControllerFactory напрямую, проще наследовать его от DefaultControllerFactory и переопределить его метод GetControllerInstance. Это означает, что DefaultControllerFactory заботится о преобразовании имени контроллера в тип контроллера, и все, что вам приходится делать – это возвращать экземпляры необходимых типов.

Листинг 7-3: Создание контроллеров
protected override IController GetControllerInstance(
	RequestContext requestContext, Type controllerType)
{
	string connectionString =
		ConfigurationManager.ConnectionStrings
		["CommerceObjectContext"].ConnectionString;
	var productRepository =
		new SqlProductRepository(connectionString);
	var basketRepository =
		new SqlBasketRepository(connectionString);
	var discountRepository =
		new SqlDiscountRepository(connectionString);
	var discountPolicy =
		new RepositoryBasketDiscountPolicy(
			discountRepository);
	var basketService =
		new BasketService(basketRepository,
			discountPolicy);
	var currencyProvider = new CachingCurrencyProvider(
		new SqlCurrencyProvider(connectionString),
		TimeSpan.FromHours(1));
	if (controllerType == typeof(BasketController))
	{
		return new BasketController(
			basketService, currencyProvider);
	}
	if (controllerType == typeof(HomeController))
	{
		return new HomeController(
			productRepository, currencyProvider);
	}
	return base.GetControllerInstance(
		requestContext, controllerType);
}

Строка 1-2: Переопределяет

Строка 4-21: Создает зависимости

Строка 22-31: Возвращает подключенные контроллеры

Строка 32-33: Использует базу для других контроллеров

Этот метод переопределяет DefaultControllerFactory.GetControllerInstance для того, чтобы создать экземпляры необходимых типов контроллера. Если требуемый тип – это BasketController или HomeController, то вы явно соединяете их с необходимыми зависимостями и возвращаете их. Оба типа используют внедрение через конструктор, поэтому вы поставляете зависимости через их конструкторы.

Для упрощения кода я решил соединить все зависимости до проверки controllerType. Очевидно, это означает, что некоторые созданные зависимости не будут использоваться, поэтому это не слишком рациональная реализация. Вы можете выполнить рефакторинг листинга 7-3 в более подходящую (но слегка более сложную) форму.

Для тех типов, которые не обрабатываются явно, вы по умолчанию обращаетесь к базовому поведению, которое заключается в создании необходимого контроллера при помощи его конструктора по умолчанию. Обратите внимание на то, что вы не обрабатываете явно AccountController, поэтому вместо этого вы позволяете базовому поведению справляться с AccountController. AccountController является остатком шаблона ASP.NET MVC проекта и использует Bastard Injection, который дает ему конструктор по умолчанию.

Примечание

Я считаю Bastard Injection анти-паттерном, но я оставляю AccountController в таком состоянии, поскольку у меня есть множество других корректных примеров механизма внедрения зависимостей для демонстрации. Я сделал это, в конце концов, потому что это шаблонный код, но я бы никогда не оставил это в подобном состоянии в рабочем коде.

После регистрации экземпляра CommerceControllerFactory в файле Global.asax он будет корректно создавать все необходимые контроллеры с необходимыми зависимостями.

Подсказка

Подумайте над тем, чтобы не писать пользовательскую фабрику контроллеров самостоятельно. Вместо этого используйте универсальную фабрику контроллеров, которая работает совместно с выбранным вами DI-контейнером. Для вдохновения посмотрите MVC Contrib проект или используйте одну из доступных в нем многократно используемых реализаций. Некоторые DI-контейнеры также имеют "официальную" интеграцию с ASP.NET MVC.

Прекрасно, что ASP.NET MVC был сконструирован таким образом, что уже подразумевал DI, поэтому нам всего лишь нужно знать и использовать единственное место расширяемости для того, чтобы разрешить DI для нашего приложения. В других фреймворках разрешение DI может быть гораздо более сложной задачей. Windows Communication Foundation (WCF), хотя и является расширяемым, выступает в качестве примера такого фреймворка.

или RSS канал: Что новенького на smarly.net