Первая версия ASP.NET MVC появилась еще в 2009 году а первый перезапуск платформы (ASP.NET Core) начал поставляться с прошлого лета. На протяжении этого времени структура проекта по умолчанию осталась почти неизменной: папки для контроллеров, представлений (views) и часто для моделей (или, возможно, ViewModels). Такой подход называется Tech folders. После создания нового проекта ASP.NET Core MVC организационная структура папок имеет следующий вид:
В чем проблема со структурой папок по умолчанию?
Большие веб-приложения требуют лучшей организации чем маленькие. Когда есть большой проект, организационная структура папок, которую используется по умолчанию в ASP.NET MVC (и Core MVC), перестает работать на вас.
Tech folders имеет свои преимущества, вот некоторые из них:
- знакомая структура, если вы работали с проектом ASP.NET MVC, вы сразу сможете сориентироваться в проекте
- логическая организация
- удобство, если нужно найти контроллер или View, то вы хорошо знаете с чего начать
Когда стартует новый проект, Tech folders работает достаточно хорошо, пока не большой функционал и нет много файлов. Как только проект начинает расти, становится довольно трудно искать нужный контроллер или View в большом количестве файлов.
Приведем простой пример. Представьте, что вы организовали свои файлы на компьютере по этой же структуре. Вместо того, чтобы иметь отдельные папки для различных проектов, у вас есть только папки которые организованы по типам файлов. Например, папка для текстовых документов, PDF-файлов, электронных таблиц и т.д. При работе на конкретной задачей, которое включает в себя изменения в нескольких типах, нужно будет прыгать между различными папками и скролить или искать нужный файл в большом количестве файлов по каждой из папок. Выглядит не очень удобно, не правда ли? Но это именно тот подход который по умолчанию использует ASP.NET MVC.
Основной недостаток заключается в том, что группа файлов, организованная по типу а не по цели (features). И этим файлам не хватает связности (high cohesion). В типичном проекте ASP.NET MVC, контроллер будет связан с одним или более View (в папке, которая соответствует имени контроллера). Контроллер имеет связь с моделями (и / или ViewModels). Models / ViewModels будут использоваться в View и т.д. Для того, чтобы сделать изменения, придется искать нужные файлы по всему проекту.
Простой пример
Рассмотрим простой проект, в задачу которого входит управление четырьмя слабо связанными компонентами: User, Customer, Client и Payment. Организационная структура папок по умолчанию для этого проекта будет выглядеть примерно так:
Для того, чтобы добавить новое поле в модель Client, отразить его на View и добавить некоторые проверки перед сохранением, потребуется переместиться в папку Models, найти подходящую модель, затем перейти в Controllers и найти ClientController, дальше в папку Views. Даже только с четырьмя контроллерами можно заметить, что нужно делать много навигации по проекту. В основном проект включает в себя гораздо больше папок.
Альтернативным подходом к организации файлов по их типу, является организация файлов по тому, что делает приложение (features). Вместо папок для контроллеров, моделей и Views, ваш проект будет состоять из папок организованных вокруг определенных features. При работе над багом, который связан с конкретным feature, вам нужно будет держать меньше папок открытыми, так как соответствующие файлы могут быть сохранены в одном месте.
Это может быть реализовано несколькими путями. Мы можем использовать Areas, но по моему мнению они не решают главной проблемы, или создать свою собственную структура для папок с features.
Feature Folders в ASP.NET Core MVC
В последнее время большой популярностью пользуется новый подход в организации структуры папок для крупных проектов который называется Feature Folders. Это особенно актуально для команд используемых подход Vertical slice.
При организации проекта по features, создается, как правило, корневая папка (например, Features), в которой вы будете иметь вложенные папки для каждой из features. Это очень похоже на то, как организованы Areas. Однако, каждая папка с feature, будет включать в себя все необходимые контроллеры, View, ViewModel и т.д. В большинстве случаев в результате мы получим папку с, возможно, от 5 до 15 файлов, которые есть все тесно связаны друг с другом. Все содержимое папки feature легко держать в фокусе в Solution Explorer. Пример этой организации:
Преимущества использования Feature Folders:
- в отличие от Areas, нам не нужны дополнительные роуты
- уменьшается время на навигацию и поиск файлов по проекту
- можно легко масштабировать и изменять независимо от других features
- позволяет держать меньше открытых папок в Solution Explorer
- дает понимание того, что именно делает приложение и какие файлы для этого нужны
- дает нам возможность повторного использования feature в других проектах, путем простого копирования папки
- в системе контроля версий можно посмотреть все изменения, которые касаются конкретной feature
- повышает связность файлов
Реализация Feature Folders в ASP.NET MVC
Для того чтобы реализовать такую организацию папок нужно иметь кастомную реализацию интерфейсов IViewLocationExpander и IControllerModelConvention. По конвеншену ожидается, что контроллер находится в namespace с названием «Features» и для следующего элемента в иерархии namespace после «Features», должно быть имя конкретного feature. Пример реализации IControllerModelConvention для поиска контроллеров:
FeatureConvention
public class FeatureConvention : IControllerModelConvention
{
public void Apply(ControllerModel controller)
{
controller.Properties.Add("feature", GetFeatureName(controller.ControllerType));
}
private static string GetFeatureName(TypeInfo controllerType)
{
var tokens = controllerType.FullName.Split('.');
if (tokens.All(t => t != "Features"))
return "";
var featureName = tokens
.SkipWhile(t => !t.Equals("features", StringComparison.CurrentCultureIgnoreCase))
.Skip(1)
.Take(1)
.FirstOrDefault();
return featureName;
}
}
Интерфейс IViewLocationExpander предоставляет метод, ExpandViewLocations, который используется для того, чтобы идентифицировать папки, содержащие Views.
FeatureFoldersRazorViewEngine
public class FeatureFoldersRazorViewEngine : IViewLocationExpander
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
IEnumerable<string> viewLocations)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (viewLocations == null)
{
throw new ArgumentNullException(nameof(viewLocations));
}
var controllerActionDescriptor = context.ActionContext.ActionDescriptor as ControllerActionDescriptor;
if (controllerActionDescriptor == null)
{
throw new NullReferenceException("ControllerActionDescriptor cannot be null.");
}
string featureName = controllerActionDescriptor.Properties["feature"] as string;
foreach (var location in viewLocations)
{
yield return location.Replace("{3}", featureName);
}
}
public void PopulateValues(ViewLocationExpanderContext context) { }
}
Осталось только использовать реализации интерфейсов и добавить некоторые параметры в Startup классе:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(o => o.Conventions.Add(new FeatureConvention()))
.AddRazorOptions(options =>
{
// {0} - Action Name
// {1} - Controller Name
// {2} - Feature Name
// Replace normal view location entirely
options.ViewLocationFormats.Clear();
options.ViewLocationFormats.Add("/Features/{2}/{1}/{0}.cshtml");
options.ViewLocationFormats.Add("/Features/{2}/{0}.cshtml");
options.ViewLocationFormats.Add("/Features/Shared/{0}.cshtml");
options.ViewLocationExpanders.Add(new FeatureFoldersRazorViewEngine());
});
}
А как насчет моделей?
Тут нужно сделать исключение для моей предыдущей структуры. В реальном мире, ваша модель предметной области будет гораздо сложнее. Традиционная трехслойная архитектура (data, business logic, presentation) до сих пор является одним из наиболее важных концепций для структурирования программного обеспечения. Важно понимать, что ASP.NET MVC не дает никакой встроеной поддержки для «моделей». ASP.NET MVC ориентирован на слой представления и не должен покрывать ответственность от других слоев. По этой причине, мы должны переместить файлы моделей (Client.cs, ClientAddress.cs, Customer.cs, Payment.cs, User.cs) в отдельную библиотеку.
Summary
Feature folders обеспечивает меньшую связанность (low coupling) и одновременно группирует связной код вместе (high cohesion). С этим подходом гораздо легче поддерживать структуру папок, что позволяет нам оставаться сосредоточенными и продуктивными при написании кода, а не тратить время на поиск файлов в разных папках. Единственным недостатком является то, что по умолчанию ASP.NET MVC не поддерживает структуру Feature folders, следовательно вам нужно настраивать ее вручную.
Спасибо за внимание!