September 2016

Volume 31 Number 9

[ASP.NET Core]

Feature Slices for ASP.NET Core MVC

By Steve Smith

Large Web apps require better organization than small ones. With large apps, the default organizational structure used by ASP.NET MVC (and Core MVC) starts to work against you. You can use two simple techniques to update your organizational approach and keep up with a growing application.

The Model-View-Controller (MVC) pattern is mature, even in the Microsoft ASP.NET space. The first version of ASP.NET MVC shipped in 2009 and the first full reboot of the platform, ASP.NET Core MVC, shipped early this summer. Throughout this time, as ASP.NET MVC has evolved, the default project structure has remained unchanged: folders for Controllers and Views and often for Models (or perhaps ViewModels). In fact, if you create a new ASP.NET Core app today, you’ll see these folders created by the default template, as shown in Figure 1.

Default ASP.NET Core Web App Template Structure
Figure 1 Default ASP.NET Core Web App Template Structure

There are many advantages to this organizational structure. It’s familiar; if you’ve worked on an ASP.NET MVC project in the last few years, you’ll immediately recognize it. It’s organized; if you’re looking for a controller or a view, you have a good idea where to start. When you’re beginning a new project, this organizational structure works reasonably well, because there aren’t yet many files. As the project grows, however, so does the friction involved in locating the desired controller or view file within the growing numbers of files and folders in these hierarchies.

To see what I mean, imagine if you organized your computer files in this same structure. Instead of having separate folders for different projects or kinds of work, you had only directories organized solely by what kinds of files. There might be folders for Text Documents, PDFs, Images and Spreadsheets. When working on a particular task that involves multiple document types, you would need to keep bouncing between the different folders and scrolling or searching through the many files in each folder that are unrelated to the task at hand. This is exactly how you work on features within an MVC app organized in the default fashion.

The reason this is an issue is that groups of files organized by type, rather than purpose, tend to lack cohesion. Cohesion refers to the degree to which elements of one module belong together. In a typical ASP.NET MVC project, a given controller will refer to one or more related views (in a folder corresponding to the controller’s name). Both the controller and the view will reference one or more ViewModels related to the controller’s responsibility. Typically, though, few ViewModel types or views are used by more than one controller type (and, typically, the domain model or persistence model is moved to its own separate project).

A Sample Project

Consider a simple project tasked with managing four loosely related application concepts: Ninjas, Plants, Pirates and Zombies. The actual sample only lets you list, view and add these concepts. However, imagine there’s additional complexity that would involve more views. The default organizational structure for this project would look something like Figure 2.

Sample Project with Default Organization
Figure 2 Sample Project with Default Organization

To work on a new bit of functionality involving Pirates, you would need to navigate down into Controllers and find the PiratesController, and then navigate down from Views into Pirates into the appropriate view file. Even with only five controllers, you can see that’s a lot of folder up-and-down navigation. This is often made worse when the root of the project includes many more folders, because Controllers and Views aren’t near one another alphabetically (so additional folders tend to fall between these two in the folder list).

An alternative approach to organizing files by their type is to organize them along the lines of what the application does. Instead of folders for Controllers, Models, and Views, your project would have folders organized around features or areas of responsibility. When working on a bug or feature related to a particular feature of the app, you would need to keep fewer folders open because the related files could be stored together. This can be done in a number of ways, including using the built-in Areas feature and rolling your own convention for feature folders.

How ASP.NET Core MVC Sees Files

It’s worth spending a moment to talk about how ASP.NET Core MVC works with the standard kinds of files an application built in it uses. Most of the files involved in the server side of the application are going to be classes written in some .NET language. These code files can live anywhere on disk, as long as they can be compiled and referenced by the application. In particular, Controller class files don’t need to be stored in any particular folder. Various kinds of model classes (domain model, view model, persistence model and so on) are the same, and can easily live in separate projects from the ASP.NET MVC Core project. You can arrange and rearrange most of the code files in the application in whatever way you like.

Views, however, are different. Views are content files. Where they’re stored relative to the application’s controller classes is irrel­evant, but it’s important that MVC knows where to look for them. Areas provide built-in support for locating views in different locations than the default Views folder. You can also customize how MVC determines the location of views.

Organizing MVC Projects Using Areas

Areas provide a way of organizing independent modules within an ASP.NET MVC application. Each Area has a folder structure that mimics the project root conventions. Therefore, your MVC application would have the same root folder conventions, and an additional folder called Areas, within which would be one folder for each section of the app, containing folders for Controllers and Views (and perhaps Models or ViewModels, if desired).

Areas are a powerful feature that let you segment a large application into separate, logically distinct sub-applications. Controllers, for example, can have the same name across areas, and in fact, it’s common to have a HomeController class in each area within an application.

To add support for Areas to an ASP.NET MVC Core project, you just need to create a new root-level folder called Areas. In this folder, create a new folder for each part of your application you want to organize within an Area. Then, inside this folder, add new folders for Controllers and Views.

Your controller files should thus be located in:

/Areas/[area name]/Controllers/[controller name].cs

Your controllers need to have an Area attribute applied to them to let the framework know they belong within a particular area:

namespace WithAreas.Areas.Ninjas.Controllers
{
  [Area("Ninjas")]
  public class HomeController : Controller

Your views should then be located in:

/Areas/[area name]/Views/[controller name]/[action name].cshtml

Any links you had to views that have moved into areas should be updated. If you’re using tag helpers, you can specify the area name as part of the tag helper. For example:

<a asp-area="Ninjas" asp-controller="Home" asp-action="Index">Ninjas</a>

Links between views within the same area can omit the asp-­area attribute.

The last thing you need to do to support areas in your app is update the default routing rules for the application in Startup.cs in the Configure method:

app.UseMvc(routes =>
{
  // Areas support
  routes.MapRoute(
    name: "areaRoute",
    template: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
  routes.MapRoute(
    name: "default",
    template: "{controller=Home}/{action=Index}/{id?}");
});

For example, the sample application for managing various Ninjas, Pirates and so on could utilize Areas to achieve the project organization structure, as shown in Figure 3.

Organizing an ASP.NET Core Project with Areas
Figure 3 Organizing an ASP.NET Core Project with Areas

The Areas feature provides an improvement over the default convention by providing separate folders for each logical section of the application. Areas are a built-in feature in ASP.NET Core MVC, requiring minimal setup. If you’re not already using them, keep them in mind as an easy way to group related sections of your app together and separate from the rest of the app.

However, the Areas organization is still very folder-heavy. You can see this in the vertical space required to show the relatively small number of files in the Areas folder. If you don’t have many controllers per area and you don’t have many views per controller, this folder overhead may add friction in much the same way as the default convention.

Fortunately, you can easily create your own convention.

Feature Folders in ASP.NET Core MVC

Outside of the default folder convention or the use of the built-in Areas feature, the most popular way to organize MVC projects is with folders per feature. This is especially true for teams that have adopted delivering functionality in vertical slices (see bit.ly/2abpJ7t), because most of a vertical slice’s UI concerns can exist within one of these feature folders.

When organizing your project by feature (instead of by file type), you’ll typically have a root folder (such as Features) within which you’ll have a subfolder per feature. This is very similar to how areas are organized. However, within each feature folder, you’ll include all of the required controllers, views and ViewModel types. In most applications, this results in a folder with perhaps five to 15 items in it, all of which are closely related to one another. The entire contents of the feature folder can be kept in view in the Solution Explorer. You can see an example of this organization for the sample project in Figure 4.

Feature Folder Organization
Figure 4 Feature Folder Organization

Notice that even the root-level Controllers and Views folders have been eliminated. The homepage for the app is now in its own feature folder called Home, and shared files like _Layout.cshtml are located in a Shared folder within the Features folder, as well. This project organization structure scales quite well, and lets developers keep their focus on far fewer folders while working on a particular section of an application.

In this example, unlike with Areas, no additional routes are required and no attributes needed for the controllers (note, how­ever, that controller names must be unique between features in this implementation). To support this organization, you need a custom IViewLocationExpander and IControllerModelConvention. These are both used, along with some custom ViewLocationFormats, to configure MVC in your Startup class.

For a given controller, it’s useful to know with what feature it’s associated. Areas achieve this using attributes; this approach uses a convention. The convention expects the controller to be in a namespace called “Features,” and for the next item in the namespace hierarchy after “Features” to be the feature name. This name is added to properties that are available during view location, as shown in Figure 5.

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

You add this convention as part of the MvcOptions when adding MVC in Startup:

services.AddMvc(o => o.Conventions.Add(new FeatureConvention()));

To replace the normal view location logic used by MVC with the feature-based convention, you can clear the list of View­LocationFormats used by MVC and replace it with your own list. This is done as part of the AddMvc call, as shown in Figure 6.

Figure 6 Replacing the Normal View Location Logic Used by MVC

services.AddMvc(o => o.Conventions.Add(new FeatureConvention()))
  .AddRazorOptions(options =>
  {
    // {0} - Action Name
    // {1} - Controller Name
    // {2} - Area Name
    // {3} - Feature Name
    // Replace normal view location entirely
    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());
  }

By default, these format strings include placeholders for actions (“{0}”), controllers (“{1}”), and areas (“{2}”). This approach adds a fourth token (“{3}”) for features.

The view location formats used should support views with the same name but used by different controllers within a feature. For example, it’s quite common to have more than one controller in a feature and for multiple controllers to have an Index method. This is supported by searching for views in a folder matching the controller name. Thus, NinjasController.Index and SwordsController.Index would locate views in /Features/Ninjas/Ninjas/Index.cshtml and /Features/Ninjas/Swords/Index.cshtml, respectively (see Figure 7).

Multiple Controllers Per Feature
Figure 7 Multiple Controllers Per Feature

Note that this is optional—if your features don’t have a need to disambiguate views (say, because the feature has only one controller), you can just put the views directly into the feature folder. Also, if you’d rather use file prefixes than folders, you could easily adjust the format string to use “{3}{1}” instead of “{3}/{1},” resulting in view filenames like NinjasIndex.cshtml and SwordsIndex.cshtml.

Shared views are also supported, both in the root of the features folder and in a Shared subfolder.

The IViewLocationExpander interface exposes a method, ExpandViewLocations, that’s used by the framework to identify folders containing views. These folders are searched when an action returns a view. This approach only requires the ViewLocation­Expander to replace the “{3}” token with the controller’s feature name, specified by the FeatureConvention described earlier:

public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context,
  IEnumerable<string> viewLocations)
{
  // Error checking removed for brevity
  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);
  }
}

To support publishing correctly, you’ll also need to update project.json’s publishOptions to include the Features folder:

"publishOptions": {
  "include": [
    "wwwroot",
    "Views",
    "Areas/**/*.cshtml",
    "Features/**/*.cshtml",
    "appsettings.json",
    "web.config"
  ]
},

The new convention of using a folder called Features is completely under your control, along with how the folders are organized within it. By modifying the set of View­LocationFormats (and possibly the FeatureViewLocationExpander type’s behavior), you can have full control over where your app’s views are located, which is the only thing needed to reorganize your files, because controller types are discovered regardless of the folder in which they’re located.

Side-By-Side Feature Folders

If you want to try out Feature Folders side-by-side with the default MVC Area and View conventions, you can do so with only small modifications. Instead of clearing the ViewLocationFormats, insert the feature formats into the start of the list (note the order is reversed):

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");

To support features combined with areas, modify the AreaViewLocationFormats collection, as well:

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");

What About Models?

Astute readers will notice that I didn’t move my model types into the feature folders (or Areas). In this sample, I don’t have separate ViewModel types, because the models I’m using are incredibly simple. In a real-world app, it’s likely your domain or persistence model will have more complexity than your views require, and that it’ll be defined in its own, separate project. Your MVC app will likely define ViewModel types that contain just the data required for a given view, optimized for display (or consumption from a client’s API request). These ViewModel types absolutely should be placed in the feature folder where they’re used (and it should be rare for these types to be shared between features).

Wrapping Up

The sample includes all three versions of the NinjaPiratePlant­Zombie organizer application, with support for adding and viewing each data type. Download it (or view it on GitHub) and think about how each approach would work in the context of an application you work on today. Experiment with adding an Area or a feature folder to a larger application you work on and decide if you prefer working with feature slices as the top-level organization of your app’s folder structure rather than having top-level folders based on file types.

The source code for this sample is available at bit.ly/29MxsI0.


Steve Smith is an independent trainer, mentor and consultant, as well as an ASP.NET MVP. He has contributed dozens of articles to the official ASP.NET Core documentation (docs.asp.net), and helps teams quickly get up to speed with ASP.NET Core. Contact him at ardalis.com.


Thanks to the following technical expert for reviewing this article: Ryan Nowak
Ryan Nowak is a developer working on the ASP.NET team at Microsoft.



Discuss this article in the MSDN Magazine forum