Vertical slices in ASP.NET MVC

Why?

In ASP.NET MVC, applications are divided into horizontal layers, which is reflected in the project structure:

  • Controllers
  • Views
  • Models
  • Scripts
  • Style

It’s a good idea to divide you application into logical parts. While the idea of horizontal slices might look like a good idea, in practice I have noticed that it’s not necessarily the only way and more often than not, not the best way. Why not?

The following is a diagram of how a standard MVC application is structured:

horizontal-slices

From the diagram above, you can see that our application has 5 layers (the standard MVC ones) and then probably a few other ones, depending on the application. Cutting through these layers are the features: all of them have styles, scripts, views, controllers and models.

There are a few reasons why this separation does not make sense:

  • Changes usually happen vertically, not horizontally. As an example, if you need to add a field to your database, you need to add it to your model, do some validation in the controller, display it in the view, style it in your CSS and do something funky with it in JavaScript.
  • When you make horizontal slices, you automatically limit your application to the same model over all the slices. It’s weird to have one controller that accesses the database directly and another that first goes through another layer. Because you have layered the application, everything is suddenly layered. In a vertically structured application you can choose which paradigm to use for each part of the application. Even though it doesn’t physically limit you from mixing it up, it certainly pushes you in that direction (and I have seen those exact guidelines on several projects).
  • From a purely practical point of view, it’s very difficult having all these files that relate to the same feature in different folders.
  • It’s an arbitrary structure. From the diagram above, you can see that the natural structure our application wants to have is vertical (all our features are vertical), but the actual structure is horizontal.

A better way to structure an application would be in vertical layers. We want all code belonging to a feature to sit together in a vertical slice (also called a feature slice) so it’s easy to work on that specific feature. That allows us to work in a very tight scope and makes our solution explorer work for us, instead of having to swap between folders all the time. A more optimal structure would look like this:

vertical-slices

In the above diagram, you can see that our slices are following the natural structure of the application. Furthermore, if we go one level deeper, we see that not all slices have controllers, not all of them depend on a domain (why do you need a domain model for content rendering?). That means we can tailor our code to exactly what is needed for that part of the application. A folder structure could look like this:

Features
    -> Users
        - users.css
        - users.js
        - UserController.cs
        - Index.cshtml
        - Detail.cshtml
    -> Search
        - search.css
        - search.js
        - SearchController.cs
        - Index.cshtml
    -> Content
        - about-us.html
        - content.css
    -> Invoicing
        - invoices.css
        - invoices.js
        - InvoiceController.cs
        - InvoiceViewModel.cs
        - Invoice.cs
Styles
    - layout.css
Scripts
    - app.js

In the above folder structure you can see that:

  • Working on a feature can be done by just opening one folder and modifying only the files in that folder, so you have less context switching going on
  • Some features may have more or less horizontal layers
  • Even scripts and CSS live in the same folder as the server-side code

I have used this structure in several projects and even though the code complexity is the same (you still need the same amount and type of code), I found that the perceived complexity is dramatically lower. Since the scope seems smaller, it feels like the application is just a bunch of smaller apps working together (under a shared structure, but still).

How?

Even though ASP.NET MVC comes standard in a horizontal flavor, it’s actually relatively simple to change this.

Controllers

If you use attribute routing, controllers are very easy, you can just move them wherever you want and they will still work. Yay, that was easy.

Models

Another easy one, just move them around at will, change a couple of namespaces and you’re done.

Views

Views are a bit trickier. By convention MVC looks for them under Views –> ControllerName or under Views –> Shared. To change this, we need to swap out the ViewEngine. That may sound difficult, but it’s actually rather simple. For the folder structure above, you can create the following class:

public class FeatureViewLocationRazorViewEngine : RazorViewEngine
{
    public FeatureViewLocationRazorViewEngine()
    {
        var featureFolderViewLocationFormats = new[]
        {
            "~/Features/{1}/{0}.cshtml",
            "~/Features/Shared/Views/{0}.cshtml",
        };

        ViewLocationFormats = featureFolderViewLocationFormats;
        MasterLocationFormats = featureFolderViewLocationFormats;
        PartialViewLocationFormats = featureFolderViewLocationFormats;
    }
}

Once you have this class, you can substitute the standard ViewEngine with this one in your global.asax:

ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new FeatureViewLocationRazorViewEngine());

Scripts

For scripts, we will have a shared part, which will still live in the default location (think about common modules for the entire application) and then there will be scripts that are specific to each feature. Those will live in the feature folders. Using ES6 modules makes this easy since you can use a bundler to bundle and minify everything. Because of the way modules work, it will automatically bundle all the scripts that are referenced from your entry point. Here’s an example of how something like that might work:

In the /scripts folder I have a file called app.js, which is my main point of entry. I trigger general modules from here and then based on the URL, I also trigger specific JS-files:

// Need to go 1 level up and into the feature folders
import ContactForm from '../Features/Contact/contact.js';
import Search from '../Features/Search/search.js';
import Users from './../Features/Users/logon.js';
import Invoices from './../Features/Invoices/invoices.js';

// this is a shared module that lives in the same directory
import Analytics from './analytics.js';

// This module always gets called, regardless of the page
new Analytics();

// Based on the path we activate a different module
var path = document.location.pathname.toLowerCase();
if(path.indexOf('contact') !== -1){
    new ContactForm();   
}
if(path.indexOf('search') !== -1){
    new Search();   
}
if(path.indexOf('invoices') !== -1){
    new Invoices();   
}
if(path.indexOf('logon') !== -1){
    new Users();   
}

Once you have the main entry point in place, you can use Gulp to bundle and minify all of this into a single file:

gulp.task('js', function () {
    return gulp.src('scripts/app.js')
             .pipe(jspm({ selfExecutingBundle: true }))
             .pipe(rename('app.min.js'))
             .pipe(gulp.dest('scripts'));
});

I’m using JSPM here, but it would work with any other bundler.

Styles

For our style sheets, I’m also going to use Gulp. In this example, I will be using SCSS because it allows me to include other files. First of all, I’m going to define my main entry point, let’s call it style.scss, which will live under the /styles folder. In this file, I only include references to other files:

// These are shared files that live under the same directory
@import "base/*";
@import "layout/*";

// using a glob I also include all .scss file in the feature-folders
@import "../Features/**/*.scss";

Note that I have used globs to include all files that are in the feature folders. This is not natively supported by most SCSS-compilers, but there’s a plugin (sassGlob) for Gulp that makes this possible (I’m sure there are plugins for Grunt as well, if you prefer to use Grunt). The following Gulp-task will make sure that all SCSS-files are combined, compiled to CSS and minified into one file:

gulp.task('sass', function () {
    return gulp.src('styles/style.scss')
               .pipe(sassGlob()) // Need to use a gulp plugin to make sure globs work
               .pipe(sass()))
               .pipe(rename({ suffix: '.min' }))
               .pipe(gulp.dest('styles'));
});

Conclusion

I find feature slices to be a huge improvement over the standard structure of an MVC-application. With a bit of infrastructure code this is easy to set up and it makes working with large application a lot easier.

Comments are closed.