The first version of ASP.NET MVC appeared back in 2009, and the platform (ASP.NET Core) was first relaunched last summer. During this time, the default project 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 organization structure of folders is the following:
What is the problem with the default folder structure?
Large web applications require a better organization than small ones. When there is a large project, the organizational folder structure that is used by default in ASP.NET MVC (and Core MVC) stops working for you.
Tech folders have advantages, here are some of them:
- a familiar structure, if you worked with an ASP.NET MVC project, you can immediately navigate through the project
- logical organization
- convenience, if you need to find a controller or View, you know where to start
When you start a new project, Tech folders work quite well, until there is a lot of functionality and there are many files. As soon as the project starts to grow, it becomes quite difficult to find the desired controller or View in a large number of files.
For instance, imagine that you organized your files on a computer by the same structure. Instead of having separate folders for different projects, you only have folders that are organized by file types. For example, a folder for text documents, PDF files, spreadsheets, etc. When working on a specific task that involves changes in several types, you will need to jump from folder to folder and scroll or search for the desired file in a large number of files in each folder. It does not look very comfortable, does it? But this is the approach that ASP.NET MVC uses by default.
The main drawback is that the group of files is organized by type and not by purpose (features). And these files do not have enough cohesion. In a typical ASP.NET MVC project, the controller will be associated with one or more View (in the folder that corresponds to the controller name). The controller has a link to the 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.
A simple example
Consider a simple project that manages four loosely coupled components: User, Customer, Client, and Payment. The default organizational folder structure for this project will look something like this:
In order to add a new field to the Client model, display it on the View and add certain checks before saving, you need to open the Models folder, find the appropriate model, then go to Controllers and find ClientController, and after that go to the Views folder. Even with only four controllers, you can notice that this requires a lot of navigation through the project. In fact, the project includes a lot more folders.
An alternative approach to organizing files by their type is to organize the files according to what the application (features) does. 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 particular feature, you will need to keep fewer folders open, as 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 a custom structure for folders with features.
Feature Folders in ASP.NET Core MVC
Recently, a new approach in the organization of folder structure for large projects, called Feature Folders, has become very popular. This is especially true for the teams that use the Vertical slice approach.
When organizing a project by features, you usually create a root folder (for example, Features), to which you will have subfolders for each of the features. This is very similar to how Areas are organized. However, each folder with feature will include all the necessary controllers, View, ViewModel, etc. In most cases, as a result, we will get a folder with, perhaps, 5 to 15 files that are all closely related to each other. You can easily keep in focus all contents of the feature folder in the Solution Explorer. An example of this organization is as follows:
Advantages of using Feature Folders:
- In contrast to Areas, we do not need additional routes
- Reduces navigation and file search time
- Can be easily scaled and modified independently of other features
- Allows you to keep fewer open folders in the Solution Explorer
- Gives an understanding of what exactly the application does and what files are needed for this
- Gives us the option to reuse the feature in other projects by simply copying a folder
- In the version control system, it is possible to see all the changes that relate to a particular feature
- Improves file coupling
Implementing Feature Folders in ASP.NET MVC
In order to implement this organization of folders, you need to have a custom implementation of the IViewLocationExpander and IControllerModelConvention interfaces. By convention, the controller is expected to be in the namespace under the name “Features”, and for the next element in the namespace hierarchy, there must be the name of a specific feature after “Features”. An example of implementation of IControllerModelConvention for searching 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 the folders that contain 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) { } }
The only thing left is 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 the models?
Here, an exception should be made for my previous structure. In the real world, your domain model will be much more difficult. 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 oriented to the presentation layer and should not cover the responsibility of 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 provides low coupling and simultaneously groups the coherent code together (high cohesion). With this approach, it is much easier to maintain the folder structure, which allows us to stay focused and productive while writing the code, rather than waste time on searching for files in different folders. The only drawback is that by default, ASP.NET MVC does not support the structure of Feature folders, so you need to configure it manually.
Thank you for attention!
Tags: asp.net core, c# Last modified: September 23, 2021
I think there is a small bug in your code. Regarding to that example you have to replace
yield return location.Replace(“{3}”, featureName);
with
yield return location.Replace(“{2}”, featureName);
FeatureFoldersRazorViewEngine class.