СЕНТЯБРЬ 2016
ТОМ 31, НОМЕР 9
ASP.NET Core - Срезы функций для ASP.NET Core MVC
Стив Смит | Сентябрь 2016
Продукты и технологии:
ASP.NET Core MVC
В статье рассматриваются:
- ASP.NET Core MVC;
- ASP.NET Core MVC Areas;
- папки функций (feature folders) в ASP.NET Core MVC.
Исходный код можно скачать по ссылке
Крупные веб-приложения требуют лучшей организации, чем малые. В случае крупных приложений организационная структура по умолчанию, используемая ASP.NET MVC (и Core MVC), начинает работать против вас. Вы можете применить два простых метода, чтобы обновить своей организационный подход и идти в ногу с развитием приложения.
Шаблон Model-View-Controller (MVC) является зрелым, даже в пространстве Microsoft ASP.NET. Первая версия ASP.NET MVC была поставлена в 2009 году, а первая полная перезагрузка платформы, ASP.NET Core MVC, была закончена к началу этого лета. На протяжении этого времени, по мере развития ASP.NET MVC структура проектов по умолчанию оставалась неизменной: папки для контроллеров и представлений, а также нередко для моделей (или, возможно, моделей представлений). По сути, если вы сегодня начнете разрабатывать новое приложение ASP.NET Core, то увидите эти папки, созданные шаблоном по умолчанию, как показано на рис. 1.
Рис. 1. Структура шаблона по умолчанию для веб-приложения ASP.NET Core
В этой организационной структуре много преимуществ. Она знакома; если за последние несколько лет вы работали над проектом ASP.NET MVC, то сразу же узнаете его. Он структурирован; если вы ищете контроллер или представление, вам известно, откуда начинать поиск. Когда вы приступаете к новому проекту, эта организационная структура работает вполне хорошо, поскольку на этот момент файлов не так много. Однако по мере роста проекта так же нарастает сложность нахождения нужного файла контроллера или представления в увеличивающемся количестве файлов и папок в этих иерархиях.
Чтобы понять, что я имею в виду, вообразите, будто бы вы организуете файлы на своем компьютере в такую же структуру. Вместо отдельных папок для разных проектов или видов работы у вас есть только каталоги, организованные исключительно по видам файлов. Это могли бы быть папки для текстовых документов, PDF, изображений и электронных таблиц. При работе над конкретной задачей, которая охватывает несколько типов документов, вам пришлось бы скакать между разными папками и прокручивать или вести поиск по множеству файлов в каждой папке, которая никак не связана с текущей задачей. Это как раз и соответствует тому, как вы работаете с функциями (features) в MVC-приложении, организованном в стиле по умолчанию.
Причина проблемы в том, что группы файлов организованы по типу, а не по предназначению, что ведет к потере связности. Связность (cohesion) обозначает степень того, насколько элементы одного модуля связаны друг с другом. В типичном проекте ASP.NET MVC данный контроллер будет ссылаться на одно или более связанных представлений (в папке, соответствующей имени контроллера). И контроллер, и представление будут ссылаться на один или более ViewModel, относящихся к обязанностям контроллера. Но, как правило, несколько типов ViewModel или представлений используются более чем одним типом контроллеров (и обычно модель предметной области или модель сохранения выносится в отдельный проект).
Проект-пример
Рассмотрим простой проект, в задачу которого входит управление четырьмя свободно связанными видами данных (concepts) приложения: Ninjas, Plants, Pirates и Zombies. Сам пример позволяет лишь перечислять, просматривать и добавлять эти данные. Но вообразите дополнительную сложность, если бы он включал больше представлений. Организационная структура по умолчанию для этого проекта выглядела бы, как на рис. 2.
Рис. 2. Проект-пример с организацией по умолчанию
Чтобы работать с новой частью функциональности, включающей Pirates, вам понадобилось бы перейти в Controllers и найти PiratesController, а затем перейти от Views в Pirates и найти подходящий файл представления. Даже при наличии всего пяти контроллеров видно, что это требует уймы переходов по папкам вверх и вниз. Зачастую становится еще хуже, когда корень проекта включает намного больше папок, поскольку в алфавитном порядке Controllers и Views разносятся далеко друг от друга (дополнительные папки, как правило, попадают в список между этими двумя папками).
Альтернативный подход к организации файлов по их типу — структурировать их по аналогии с тем, что делает приложение. Вместо папок Controllers, Models и Views в проекте находились бы папки, организованные по функциям (features) или областям обязанностей. При работе над ошибкой или какой-то функцией, связанной с конкретным функционалом приложения, вам понадобилось бы держать открытыми меньше папок, поскольку связанные файлы можно было бы хранить вместе. Это можно сделать разными способами, в том числе используя встроенную функцию Areas (Areas feature) и устанавливая собственное соглашение по папкам функций.
Как ASP.NET Core MVC видит файлы
Стоит потратить минутку и поговорить о том, как ASP.NET Core MVC работает со стандартными видами файлов, из которых состоит приложение. Большинство файлов, размещаемых на серверной стороне, будет классами, написанными на каком-то .NET-языке. Эти файлы кода могут находиться на диске где угодно, если приложение может на них ссылаться и их можно скомпилировать. В частности, файлы класса Controller необязательно хранить в некоей конкретной папке. То же самое относится и к классам различных видов моделей (предметной области, представления, сохранения и т. д.) — они запросто могут находиться в проектах, отдельных от проекта ASP.NET MVC Core. Вы можете упорядочивать и переупорядочивать большинство файлов кода в приложении как угодно.
Но представления — дело другое. Это файлы контента. Где они хранятся относительно классов контроллеров приложения, особого значения не имеет, но важно, чтобы MVC было известно, где их искать. Areas предоставляют встроенную поддержку для поиска представлений в местах, отличных от папки Views по умолчанию. Вы также можете настроить то, как MVC определяет местоположение представлений.
Организация MVC-проектов с применением Areas
Areas предоставляют способ организовывать независимые модули в приложении ASP.NET MVC. Каждая Area имеет структуру папок, которая имитирует соглашения по корню проекта. В этом случае ваше MVC-приложение имеет те же соглашения по корневой папке и дополнительную папку Areas, внутри которой находится по одной папке на каждый раздел приложения, содержащей папки Controllers и Views (и, возможно, Models или ViewModels).
Areas (области) — мощная функция, позволяющая сегментировать крупное приложение на логически отделенные подприложения. Например, у контроллеров может быть одно и то же имя в разных областях, и фактически наличие класса HomeController в каждой области приложения — весьма распространенная практика.
Чтобы добавить поддержку Areas в проект ASP.NET MVC Core, вам нужно просто создать новую папку корневого уровня — Areas. В этой папке создайте новую папку для каждой части вашего приложения, которую вы хотите организовать в какой-либо Area. Затем в эту папку добавьте новые папки Controllers и Views.
Таким образом, ваши файлы контроллеров должны находиться в:
/Areas/[имя области]/Controllers/[имя контроллера].cs
У ваших контроллеров должен быть атрибут Area, который сообщает инфраструктуре об их принадлежности к конкретной области:
namespace WithAreas.Areas.Ninjas.Controllers
{
[Area("Ninjas")]
public class HomeController : Controller
Ваши представления должны быть расположены в:
/Areas/[имя области]/Views/[имя контроллера]/
[имя операции].cshtml
Любые имевшиеся у вас ссылки на представления, которые были перемещены в области, должны быть обновлены. Если вы используете вспомогательные теги, то можете указать имя области как часть вспомогательного тега, например:
<a asp-area="Ninjas" asp-controller="Home" asp-action="Index">Ninjas</a>
В ссылках между представлениями в пределах одной области можно опускать атрибут asp-area.
Последнее, что надо сделать для поддержки областей в приложении, — обновить правила маршрутизации по умолчанию для приложения в файле Startup.cs (метод Configure):
app.UseMvc(routes =>
{
// Поддержка областей
routes.MapRoute(name: "areaRoute", template:
"{area:exists}/{controller=Home}/{action=Index}/{id?}");
routes.MapRoute(name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
Так, приложение-пример для управления различными Ninjas, Pirates и прочих могло бы использовать Areas для достижения организационной структуры проекта, как на рис. 3.
Рис. 3. Организация проекта ASP.NET Core с помощью Areas
Функция Areas превосходит стандартное соглашение, предоставляя отдельные папки для каждого логического раздела приложения. Areas — встроенная функция ASP.NET Core MVC, требующая минимальной настройки. Если вы еще не используете эту функцию, имейте в виду, что она является простым способом группирования связанных разделов вашего приложения и отделения их от остальной части этого приложения.
Однако организация на основе Areas все равно сильно зависит от папок. Вы можете увидеть это в вертикальном пространстве, необходимым для того, чтобы показать сравнительно малое количество файлов в папке Areas. Если у вас нет множества контроллеров в каждой области и множества представлений на каждый контроллер, эта папка может создать издержки, во многом аналогичные издержкам стандартного соглашения (по умолчанию).
К счастью, можно легко создать собственное соглашение.
Папки функций в ASP.NET Core MVC
Помимо стандартного соглашения по папкам или использования встроенной функции Areas, самый популярный способ организации MVC-проектов — создание папок на каждую функцию (feature). Это особенно справедливо для групп, которые приняли доставку функциональности в вертикальных срезах (vertical slices) (см. bit.ly/2abpJ7t), поскольку большая часть обязанностей UI вертикального среза может находиться в одной из этих папок функций.
Организуя проект по функциям (а не по типам файлов), вы, как правило, получаете корневую папку (например, Features), в которой создается по одной подпапке на каждую функцию. Это очень похоже на то, как организованы области. Однако в каждую папку функции вы включите все необходимые контроллеры, представления и типы ViewModel. В большинстве приложений это дает папку с 5–15 элементами, и все они тесно связаны друг с другом. Полное содержимое папки функции можно увидеть в Solution Explorer. На рис. 4 показан пример такой организации проекта-примера.
Рис. 4. Организация папки функции
Заметьте, что исключены даже папки Controllers и Views корневого уровня. Основная страница приложения теперь находится в своей папке функции под названием Home, а общие файлы вроде _Layout.cshtml помещаются в папку Shared в папке Features. Эта организация проекта весьма хорошо масштабируется и позволяет разработчикам уделять внимание гораздо меньшему количеству папок при работе над конкретным разделом приложения.
В отличие от Areas в этом примере нет дополнительных маршрутов и не требуются никакие атрибуты для контроллеров (но заметьте, что в этой реализации имена контроллеров должны быть уникальны во всех функциях). Для поддержки такой организации нужны собственные IViewLocationExpander и IControllerModelConvention. Они используются совместно с каким-либо своим ViewLocationFormats для конфигурирования MVC в классе Startup.
Для данного контроллера полезно знать, с какой функцией (feature) он сопоставлен. В Areas это достигается использованием атрибутов, а здесь применяется соглашение. По этому соглашению предполагается, что контроллер находится в пространстве имен Features, а следующий элемент в иерархии пространства имен после Features является именем функции (feature). Это имя добавляется к свойствам, доступным при поиске представлений, как показано на рис. 5.
Рис. 5. FeatureConvention : IControllerModelConvention
{
public void Apply(ControllerModel controller)
{
controller.Properties.Add("feature",
GetFeatureName(controller.ControllerType));
}
private string GetFeatureName(TypeInfo controllerType)
{
string[] tokens = controllerType.FullName.Split('.');
if (!tokens.Any(t => t == "Features")) return "";
string featureName = tokens
.SkipWhile(t => !t.Equals("features",
StringComparison.CurrentCultureIgnoreCase))
.Skip(1)
.Take(1)
.FirstOrDefault();
return featureName;
}
}
Вы добавляете это соглашение как часть MvcOptions при добавлении MVC в Startup:
services.AddMvc(o => o.Conventions.Add(new FeatureConvention()));
Чтобы заменить стандартную логику поиска представлений, используемую MVC в соглашении на основе функций, можно очистить список ViewLocationFormats, задействованный MVC, и поменять его на собственный список. Это делается в вызове AddMvc (рис. 6).
Рис. 6. Замена стандартной логики поиска представлений, используемой MVC
services.AddMvc(o => o.Conventions.Add(
new FeatureConvention()))
.AddRazorOptions(options =>
{
// {0} – имя операции
// {1} – имя контроллера
// {2} – имя области
// {3} – имя функции
// Полностью заменяем обычную логику поиска представлений
options.ViewLocationFormats.Clear();
options.ViewLocationFormats.Add(
"/Features/{3}/{1}/{0}.cshtml");
options.ViewLocationFormats.Add(
"/Features/{3}/{0}.cshtml");
options.ViewLocationFormats.Add(
"/Features/Shared/{0}.cshtml");
options.ViewLocationExpanders.Add(
new FeatureViewLocationExpander());
}
По умолчанию эти форматирующие строки включают поля подстановки для операций («{0}»), контроллеров («{1}») и областей («{2}»). В этом подходе добавляется четвертая лексема — «{3}» для функций.
Применяемые форматы поиска должны поддерживать представления с одинаковыми именами, но используемые разными контроллерами в пределах функции. Например, не редкость, когда в функции есть более одного контроллера и когда для нескольких контроллеров имеется метод Index. Это поддерживается за счет поиска представлений в папке, чье имя соответствует имени контроллера. Таким образом, NinjasController.Index и SwordsController.Index нашли бы представления в /Features/Ninjas/Ninjas/Index.cshtml и /Features/Ninjas/Swords/Index.cshtml соответственно (рис. 7).
Рис. 7. Несколько контроллеров на функцию
Заметьте, что это не обязательно, если вашим функциям не требуется устранять неоднозначность имен представлений (скажем, в функции всего один контроллер); вы можете просто помещать представления непосредственно в папку функции. Кроме того, если бы предпочли создавать префиксы файлов вместо папок, то могли бы легко подстроить форматирующую строку под использование «{3}{1}» вместо «{3}/{1}», что дало бы в результате имена файлов представлений наподобие NinjasIndex.cshtml и SwordsIndex.cshtml.
Общие представления тоже поддерживаются — как в корне папок функций, так и в подпапке Shared.
Интерфейс IViewLocationExpander предоставляет метод ExpandViewLocations, используемый инфраструктурой для идентификации папок, в которых содержатся представления. Поиск по этим папкам выполняется, когда операция возвращает представление. Этот подход требует, только чтобы ViewLocationExpander заменял лексему «{3}» именем функции контроллера, указанным в ранее описанном FeatureConvention:
public IEnumerable<string> ExpandViewLocations(
ViewLocationExpanderContext context,
IEnumerable<string> viewLocations)
{
// Проверка ошибок удалена для краткости
var controllerActionDescriptor =
context.ActionContext.ActionDescriptor
as ControllerActionDescriptor;
string featureName = controllerActionDescriptor.Properties[
"feature"] as string;
foreach (var location in viewLocations)
{
yield return location.Replace("{3}", featureName);
}
}
Для поддержки корректной публикации вам также понадобится обновить publishOptions в project.json, чтобы включить папку Features:
"publishOptions": {
"include": [
"wwwroot",
"Views",
"Areas/**/*.cshtml",
"Features/**/*.cshtml",
"appsettings.json",
"web.config"
]
},
Новое соглашение по использованию папки Features находится под вашим полным контролем, равно как и организация папок внутри папки Features. Модифицируя набор ViewLocationFormats (и, возможно, поведение типа FeatureViewLocationExpander), вы можете полностью контролировать то, где располагаются представления вашего приложения, — это единственное, что нужно для реорганизации ваших файлов, так как типы контроллеров распознаются независимо от папки, в которой они содержатся.
Использование Feature Folders совместно с другими соглашениями
Если вы хотите опробовать применение Feature Folders совместно с соглашениями MVC Areas и Views, то можете добиться этого с помощью небольших модификаций. Вместо очистки ViewLocationFormats вставьте форматы функций (feature formats) в начало списка (обратите внимание на обратный порядок):
options.ViewLocationFormats.Insert(0, "/Features/Shared/{0}.cshtml");
options.ViewLocationFormats.Insert(0, "/Features/{3}/{0}.cshtml");
options.ViewLocationFormats.Insert(0, "/Features/{3}/{1}/{0}.cshtml");
Для поддержки функций в сочетании с областями измените и набор AreaViewLocationFormats:
options.AreaViewLocationFormats.Insert(0, "/Areas/{2}/Features/Shared/{0}.cshtml");
options.AreaViewLocationFormats.Insert(0, "/Areas/{2}/Features/{3}/{0}.cshtml");
options.AreaViewLocationFormats.Insert(0, "/Areas/{2}/Features/{3}/{1}/{0}.cshtml");
Как насчет моделей?
Проницательные читатели заметят, что я не выносил типы моделей в папки функций (или в области). В этом примере нет отдельных типов ViewModel, поскольку используемые мной модели невероятно просты. В реальном приложении ваша модель предметной области или модель сохранения окажется сложнее, чем требуется вашим представлениям, и она будет определена в отдельном проекте. Ваше MVC-приложение скорее всего будет определять типы ViewModel, которые просто содержат данные, необходимые для конкретного представления и оптимизированные для отображения (или использования из клиентского API). Эти типы ViewModel нужно абсолютно обязательно помещать в папку той функции, где они задействованы (и эти типы редко бывают общими для функций).
Заключение
В пример включены три версии приложения-органайзера NinjaPiratePlantZombie с поддержкой добавления и просмотра каждого типа данных. Скачайте его (или просмотрите на GitHub) и поразмыслите о том, как каждый подход мог бы работать в контексте приложения, над которым вы работаете сейчас. Поэкспериментируйте с добавлением папки области или функции для своего более крупного приложения и решите, предпочитаете вы работать со срезами функций на верхнем уровне организации структуры папок вашего приложения или с папками верхнего уровня на основе типов файлов.
Стив Смит (Steve Smith) — независимый тренер, наставник и консультант, а также обладатель звания ASP.NET MVP. Написал десятки статей для официальной документации по ASP.NET Core (docs.asp.net). Помогает группам разработчиков быстрее освоить ASP.NET Core. С ним можно связаться через сайт ardalis.com; также следите за его заметками в Twitter (@ardalis).
Выражаю благодарность за рецензирование статьи эксперту Райену Новаку (Ryan Nowak).