Фабрика контроллеров

142

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

Вызов метода действия MVC

В центре внимания будет фабрика контроллеров и активатор действий. Названия этих компонентов отражают их назначение. Фабрика контроллеров отвечает за создание экземпляров контроллеров для обслуживания запроса. Активатор действий занимается поиском и вызовом метода действия в классе контроллера. Инфраструктура MVC Framework включает стандартные реализации обоих этих компонентов, и мы покажем, как конфигурировать их поведение. Мы также объясним, как полностью заменить эти компоненты собственной специальной логикой.

Пример приложения

Для этой и последующих статей мы создали новый проект MVC по имени ControllerExtensibility с использованием шаблона Empty (Пустой) и отметкой флажка MVC в разделе Add folders and core references for (Добавить папки и основные ссылки для). Здесь мы будем работать с рядом простых контроллеров, чтобы продемонстрировать различные виды возможностей расширения, которые доступны в рамках инфраструктуры.

Для начала в папке Models необходимо создать файл Result.cs, содержащий определение класса Result, как показано в примере ниже:

namespace ControllerExtensibility.Models
{
    public class Result
    {
        public string ControllerName { get; set; }
        public string ActionName { get; set; }
    }
}

Следующий шаг заключается в создании папки /Views/Shared и добавлении в нее нового файла представления по имени Result.cshtml. Это представление будет визуализировать все методы действий в контроллерах. В примере ниже приведено содержимое файла Result.cshtml:

@model ControllerExtensibility.Models.Result
@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Result</title>
</head>
<body>
    <div>Контроллер: @Model.ControllerName</div>
    <div>Метод действия: @Model.ActionName</div>
</body>
</html>

Данное представление в качестве своей модели использует класс Result, определенный ранее, и просто отображает значения свойств ControllerName и ActionName. Наконец, нужно создать несколько базовых контроллеров. В примере ниже показан код контроллера Product:

using System.Web.Mvc;
using ControllerExtensibility.Models;

namespace ControllerExtensibility.Controllers
{
    public class ProductController : Controller
    {
        public ViewResult Index()
        {
            return View("Result", new Result
            {
                ControllerName = "Product",
                ActionName = "Index"
            });
        }

        public ViewResult List()
        {
            return View("Result", new Result
            {
                ControllerName = "Product",
                ActionName = "List"
            });
        }
    }
}

В примере ниже приведен код контроллера Customer:

using System.Web.Mvc;
using ControllerExtensibility.Models;

namespace ControllerExtensibility.Controllers
{
    public class CustomerController : Controller
    {
        public ViewResult Index()
        {
            return View("Result", new Result
            {
                ControllerName = "Customer",
                ActionName = "Index"
            });
        }

        public ViewResult List()
        {
            return View("Result", new Result
            {
                ControllerName = "Customer",
                ActionName = "List"
            });
        }
    }
}

Все эти контроллеры не выполняют никаких полезных действий кроме сообщения о факте их вызова через представление Result.cshtml.

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

Как и с большинством компонентов MVC Framework, наилучший способ понять работу фабрик контроллеров - построить собственную реализацию. Мы не рекомендуем делать это в реальном проекте, поскольку создать специальное поведение намного проще за счет расширения встроенной фабрики, но так удобнее продемонстрировать, каким образом MVC Framework создает экземпляры контроллеров. Фабрики контроллеров определяются с помощью интерфейса IControllerFactory, который показан в примере ниже:

using System.Web.Routing;
using System.Web.SessionState;

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

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

using System;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.SessionState;
using ControllerExtensibility.Controllers;

namespace ControllerExtensibility.Infrastructure
{
    public class CustomControllerFactory : IControllerFactory
    {

        public IController CreateController(RequestContext requestContext,
            string controllerName)
        {

            Type targetType = null;
            switch (controllerName)
            {
                case "Product":
                    targetType = typeof(ProductController);
                    break;
                case "Customer":
                    targetType = typeof(CustomerController);
                    break;
                default:
                    requestContext.RouteData.Values["controller"] = "Product";
                    targetType = typeof(ProductController);
                    break;
            }

            return targetType == null ? null :
                (IController)DependencyResolver.Current.GetService(targetType);
        }

        public SessionStateBehavior GetControllerSessionBehavior(RequestContext
            requestContext, string controllerName)
        {
            return SessionStateBehavior.Default;
        }

        public void ReleaseController(IController controller)
        {
            IDisposable disposable = controller as IDisposable;
            if (disposable != null)
            {
                disposable.Dispose();
            }
        }
    }
}

Наиболее важным методом в этом интерфейсе является CreateController(), который инфраструктура MVC Framework вызывает, когда ей нужен контроллер для обслуживания запроса. В качестве параметров этому методу передается объект RequestContext, который позволяет фабрике инспектировать детали запроса, и строка, содержащая значение сегмента controller из маршрутизированного URL. В классе RequestContext определены свойства, описанные в таблице ниже:

Свойства класса RequestContext
Имя Тип Описание
HttpContext HttpContextBase

Предоставляет информацию о запросе HTTP

RouteData RouteData

Предоставляет информацию о маршруте, который соответствует запросу

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

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

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

Соглашения, показанные ранее в предыдущих статьях, объясняются способом реализации стандартной фабрики контроллеров. Например, одно из этих соглашений реализовано в коде - когда мы получаем запрос для контроллера, мы добавляем к имени класса строку Controller, поэтому запрос для контроллера Product приводит к созданию экземпляра класса ProductController.

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

Работа с резервным контроллером

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

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

По умолчанию инфраструктура MVC Framework выбирает представление на основе значения controller в данных маршрутизации, а не имени класса контроллера. Таким образом, если в рассматриваемом примере необходимо, чтобы резервный вариант работал с представлениями, которые следуют соглашению относительно организации по именам контроллеров, понадобится изменить значение свойства маршрутизации controller:

requestContext.RouteData.Values["controller"] = "Product";

Это изменение вынудит MVC Framework искать представления, ассоциированные с резервным контроллером, а не с контроллером, который запросил пользователь.

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

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

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

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

return targetType == null ? null :
    (IController)DependencyResolver.Current.GetService(targetType);

Статическое свойство DependencyResolver.Current возвращает реализацию интерфейса IDependencyResolver, в котором определен метод GetService(). Этому методу передается объект System.Type, а в качестве результата возвращается его экземпляр. Существует строго типизированная версия метода GetService(), но поскольку заранее не известно, с каким типом придется иметь дело, мы применяем версию, которая возвращает object, и затем выполняем явное приведение к IController.

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

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

Реализация других методов интерфейса

Ниже описаны два других метода интерфейса IControllerFactory:

Метод GetControllerSessionBehavior()

Используется MVC Framework при определении, должны ли для контроллера поддерживаться данные сеанса Session.

Метод ReleaseController()

Вызывается, когда объект контроллера, созданный методом CreateController(), больше не нужен. В нашей реализации мы проверяем, реализует ли класс интерфейс IDisposable. Если реализует, мы вызываем метод Dispose() для освобождения всех занятых ресурсов.

Построенные реализации методов GetControllerSessionBehavior() и ReleaseController() подходят для большинства проектов и могут использоваться без изменений.

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

Для сообщения MVC Framework об использовании специальной фабрики контроллеров применяется класс ControllerBuilder. Специальная фабрика контроллеров должна быть зарегистрирована при запуске приложения, что предполагает использование метода Application_Start() в файле Global.asax.cs, как показано в примере ниже:

using System.Web.Mvc;
using System.Web.Routing;
using ControllerExtensibility.Infrastructure;

namespace ControllerExtensibility
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);

            ControllerBuilder.Current.SetControllerFactory(
                new CustomControllerFactory());
        }
    }
}

Сразу после регистрации фабрика контроллеров будет отвечать за обработку всех запросов, получаемых приложением. Увидеть результаты работы специальной фабрики можно, просто запустив приложение - браузер запросит корневой URL, который системой маршрутизации будет отображен на контроллер Home. Специальная фабрика обработает запрос для контроллера Home, создав экземпляр класса ProductController, который выдаст результат, показанный на рисунке ниже:

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

Работа со встроенной фабрикой контроллеров

Создание специальной фабрики контроллеров было описано потому, что это самый эффективный способ демонстрации функционирования любой фабрики контроллеров. Тем не менее, в большинстве приложений вполне достаточно класса встроенной фабрики контроллеров, который имеет имя DefaultControllerFactory. При получении запроса от системы маршрутизации эта фабрика просматривает данные маршрутизации в поисках значения для свойства controller и пытается найти в веб-приложении класс, который удовлетворяет перечисленным ниже критериям:

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

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

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

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

В статье Настройки маршрутизации было показано, как назначать приоритеты одному или более пространств имен при создании маршрута. Это делалось для решения проблемы с неоднозначными контроллерами, когда классы контроллеров называются одинаково, но находятся в разных пространствах имен. Обработкой списка пространств имен и назначением им приоритетов занимается DefaultControllerFactory.

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

Если есть приложение с множеством маршрутов, может быть намного удобнее назначить приоритеты пространствам имен глобально, чтобы они применялись ко всем маршрутам. В примере ниже показано, как это сделать в методе Application__Start() файла Global.asax. (При желании можно также использовать для этого файл RouteConfig.cs из папки App_Start.)

using System.Web.Mvc;
using System.Web.Routing;
using ControllerExtensibility.Infrastructure;

namespace ControllerExtensibility
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);

            ControllerBuilder.Current.DefaultNamespaces.Add("MyNamespaceController");
            ControllerBuilder.Current.DefaultNamespaces.Add("MyProject.*");
        }
    }
}

Для добавления пространств имен, которые должны иметь приоритет, используется статический метод ControllerBuilder.Current.DefaultNamespaces.Add(). Порядок, в котором добавляются пространства имен, не предполагает какого-либо упорядочивания при поиске или относительных приоритетов. Все пространства имен, определяемые методом Add(), трактуются эквивалентно, и приоритет будет относительным только для тех пространств имен, которые не указаны с помощью метода Add(). Это означает, что фабрика контроллеров будет производить поиск во всем приложении, если не сможет найти подходящий класс контроллера в пространствах имен, определенных посредством метода Add().

Обратите внимание на использование символа звездочки (*) во втором операторе. Это позволяет указать, что фабрика контроллеров должна просматривать пространство имен MyProject и все его дочерние пространства имен. Хотя это выглядит похожим на синтаксис регулярных выражений, таковым оно не является; завершать с помощью "*" пространства имен можно, но применять другой синтаксис регулярных выражений в методе Add() не допускается.

Настройка создания экземпляров контроллеров классом DefaultControllerFactory

Существует несколько способов настройки поведения класса DefaultControllerFactory, связанного с созданием экземпляров контроллеров. До сих пор самой распространенной причиной настройки фабрики контроллеров было добавление поддержки внедрения зависимостей (DI). Для этого имеется множество путей. Наиболее подходящий путь основан на том, как DI используется в других местах приложения.

Использование распознавателя зависимостей

Для создания контроллеров класс DefaultControllerFactory будет использовать распознаватель зависимостей, если он доступен. Например, класс NinjectDependencyResolver реализует интерфейс IDependencyResolver для обеспечения поддержки DI с помощью Ninject. Применение класса DependencyResolver также демонстрировалось в примере ранее, когда создавалась специальная фабрика контроллеров.

Класс DefaultControllerFactory будет вызывать метод IDependencyResolver.GetService() для запроса экземпляра контроллера, который предоставляет возможность распознать и внедрить любые зависимости.

Использование активатора контроллеров

Ввести DI в контроллеры можно также за счет создания активатора контроллеров. Такой активатор создается путем реализации интерфейса IControllerActivator, который показан в примере ниже:

using System.Web.Routing;

namespace System.Web.Mvc
{
    public interface IControllerActivator
    {
        IController Create(RequestContext requestContext, Type cohtrollerType);
    }
}

Этот интерфейс содержит единственный метод по имени Create(), которому передается объект RequestContext, описывающий запрос, и Type, который указывает, экземпляр какого класса контроллера должен быть создан.

Чтобы продемонстрировать реализацию интерфейса IControllerActivator, в папку Infrastructure добавлен новый файл класса по имени CustomControllerActivator.cs, в котором определен класс, показанный в примере ниже:

using System;
using System.Web.Mvc;
using System.Web.Routing;
using ControllerExtensibility.Controllers;

namespace ControllerExtensibility.Infrastructure
{
    public class CustomControllerActivator : IControllerActivator
    {
        public IController Create(RequestContext requestContext,
            Type controllerType)
        {
            if (controllerType == typeof(ProductController))
            {
                controllerType = typeof(CustomerController);
            }
            return (IController)DependencyResolver.Current.GetService(controllerType);
        }
    }
}

Приведенная реализация IControllerActivator довольно проста. Если запрашивается класс ProductController, она отвечает возвратом экземпляра класса CustomerController. Это вряд ли будет делаться в реальном проекте, но позволяет продемонстрировать применение интерфейса IControllerActivator для перехвата запросов между фабрикой контроллеров и распознавателем зависимостей.

Чтобы использовать специальный активатор, необходимо передать экземпляр класса реализации конструктору DefaultControllerFactory и зарегистрировать результат в методе Application_Start() файла Global.asax:

using System.Web.Mvc;
using System.Web.Routing;
using ControllerExtensibility.Infrastructure;

namespace ControllerExtensibility
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);

            ControllerBuilder.Current.SetControllerFactory(
                new DefaultControllerFactory(new CustomControllerActivator()));
        }
    }
}

Увидеть эффект от применения специального активатора можно, запустив приложение и перейдя на URL вида /Product. Маршрут будет нацелен на контроллер Product и фабрика DefaultControllerFactory запросит у активатора создание экземпляра класса ProductFactory, но активатор перехватит этот запрос и создаст взамен экземпляр класса CustomerController:

Перехват запросов на создание экземпляров с использованием специального активатора контроллеров

Переопределение методов DefaultControllerFactory

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

Переопределяемые методы класса DefaultContollerFactory
Метод Тип результата Описание
CreateController() IController

Реализация метода CreateController() из интерфейса IControllerFactory. По умолчанию этот метод вызывает метод GetControllerType() для определения, экземпляр какого типа должен быть создан, и затем получает объект контроллера, передавая результат методу GetControllerInstance()

GetControllerType() Type

Отображает запросы на типы контроллеров. Именно здесь применяется большинство соглашений, упомянутых ранее

GetControllerInstance() IController

Создает экземпляр указанного типа

Пройди тесты
Лучший чат для C# программистов