📜 ⬆️ ⬇️

Feature Folders structure in ASP.NET Core MVC



The first version of ASP.NET MVC appeared in 2009, and the first restart of the platform (ASP.NET Core) began to be delivered from last summer. During this time, the project’s default structure has remained almost unchanged: folders for controllers, views, and often for models (or, perhaps, ViewModels). This approach is called Tech folders . After creating a new ASP.NET Core MVC project, the organizational structure of folders is as follows:


What is the problem with the default folder structure?


Larger web applications require better organization than small ones. When there is a big project, the folder organization structure, which is used by default in ASP.NET MVC (and Core MVC), stops working for you.

Tech folders has its advantages, here are some of them:
')

When the new project starts, the tech folders work quite well until there is a lot of functionality and there are not many files. Once the project starts to grow, it becomes quite difficult to search for the desired controller or View in a large number of files.

We give a simple example. Imagine that you organized your files on your computer in the same structure. Instead of having separate folders for different projects, you only have folders that are organized by file type. For example, a folder for text documents, PDF files, spreadsheets, etc. When working on a specific task, which includes changes in several types, you will need to jump between different folders and scroll or search for the desired file in a large number of files in each of the folders. It doesn't look very comfortable, does it? But this is exactly the approach that ASP.NET MVC uses by default.

The main disadvantage is that the group of files is organized by type and not by purpose (features). And these files lack connectivity ( high cohesion ). In a typical ASP.NET MVC project, a controller will be associated with one or more View (in the folder that corresponds to the name of the controller). The controller has communication with models (and / or ViewModels). Models / ViewModels will be used in View, etc. In order to make changes, you will have to search for the necessary files throughout the project.

Simple example


Consider a simple project whose task is to manage four loosely coupled components: User, Customer, Client and Payment. The default folder structure for this project will look something like this:


In order to add a new field to the Client model, reflect it on the View and add some checks before saving, you will need to move to the Models folder, find a suitable model, then go to Controllers and find the ClientController, then to the Views folder. Even with only four controllers, you can see that you need to do a lot of navigation through the project. Basically the project includes many more folders.

An alternative approach to organizing files by their type is organizing files according to what the application does (features). Instead of folders for controllers, models and Views, your project will consist of folders organized around certain features. When working on a bug that is associated with a specific feature, you will need to keep fewer folders open, since the corresponding files can be stored in one place.

This can be implemented in several ways. We can use Areas, but in my opinion they do not solve the main problem, or create our own structure for folders with features.

Feature Folders in ASP.NET Core MVC


Recently, a new approach to organizing the structure of folders for large projects called Feature Folders is very popular. This is especially true for the commands used by the Vertical slice approach.

When organizing a project by features, a root folder is created, as a rule, (for example, Features), in which you will have subfolders for each of the features. This is very similar to how Areas is organized. However, each folder with a feature will include all the necessary controllers, View, ViewModel, etc. In most cases, as a result we will get a folder with, perhaps, from 5 to 15 files, which are all closely related to each other. All contents of the feature folder are easy to keep in focus in Solution Explorer. An example of this organization:


Benefits of using Feature Folders:


Implementing Feature Folders in ASP.NET MVC


To implement this folder organization, you need to have a custom implementation of the IViewLocationExpander and IControllerModelConvention interfaces. By convention, the controller is expected to be in a namespace with the name “Features” and for the next element in the namespace hierarchy after “Features”, there must be a name for a specific feature. An example implementation of IControllerModelConvention for finding controllers:

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; } } 


The IViewLocationExpander interface provides a method, ExpandViewLocations, which is used to identify folders containing 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) { } } 



It remains only to use the interface implementations and add some parameters to the Startup class:

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()); }); } 



What about models?


Here it is necessary to make an exception for my previous structure. In the real world, your domain model will be much more complicated. The traditional three-layer architecture (data, business logic, presentation) is still one of the most important concepts for structuring software. It is important to understand that ASP.NET MVC does not provide any built-in support for "models". ASP.NET MVC is focused on the presentation layer and should not cover responsibility from other layers. For this reason, we need to move the model files (Client.cs, ClientAddress.cs, Customer.cs, Payment.cs, User.cs) to a separate library.

Summary


Feature folders provide less cohesion (low coupling) and at the same time group the coded code together (high cohesion). With this approach, it is much easier to maintain the folder structure, which allows us to remain focused and productive while writing code, and not to waste time searching for files in different folders. The only drawback is that by default ASP.NET MVC does not support the Feature folders structure, therefore you need to configure it manually.

Thanks for attention!

Source: https://habr.com/ru/post/321392/


All Articles