Matthew Renze

Clean Architecture in ASP.NET MVC 5

Author: Matthew Renze
Posted: 04/27/2015

I'm a big fan of Clean Architecture patterns, practices, and principles. One of the Clean Architecture practices that I'm particularly fond of is the Screaming Architecture practice promoted by Robert C. Martin (aka. Uncle Bob). This practice states that we should organize our software's architecture in such a way that it should scream the intent of our system; hence the name "Screaming Architecture". One of the ways we can implement this practice is by having the top-level folder structure of our source code represent the high-level use cases of our application.

While I've been able to implement this architectural practice in many of the applications I've built over the past few years, I have always struggled with implementing it in my ASP.NET MVC applications. There are several technical challenges to get this to work in ASP.NET MVC and I have yet to find a single source of information on the internet that shows you how to overcome all of these technical obstacles in one place.

So, I decided this topic was important enough to take the time to create an open-source sample project and write an article to document the steps necessary to implement the Screaming Architecture practice in ASP.NET MVC 5. The goal of this article and corresponding open-source sample project is to help others learn how to incorporate this practice into their ASP.NET MVC web applications.

Create a new empty ASP.NET MVC project

To keep things simple, we're going to use an empty ASP.NET project template as our starting point. However, you could also start with an MVC project template and modify the instructions below to preserve the automatically generated models, views, and controllers, and refactor them into the new folder layout.

  1. Create a new ASP.NET MVC 5 project.

    New Project dialog box
  2. Add folders and core references for MVC.

    New ASP.NET Project dialog box

Create a custom razor view engine

Since our use-case-driven folder structure does not match the out-of-the-box MVC folder structure, we need to tell ASP.NET MVC where to find the view files, given our custom folder structure. We do this with a custom razor view engine.

  1. Create a new class called CustomRazorViewEngine.cs, which inherits from RazorViewEngine, in the root folder of the project.

  2. Add "~/{1}/Views/{0}.cshtml" to the view location formats property of the new custom view engine.

  3. Add "~/Shared/Views/{0}.cshtml" to the partial view location formats property of the new custom view engine.

public class CustomRazorViewEngine : RazorViewEngine
{
    public CustomRazorViewEngine()
    {
        ViewLocationFormats = new string[]
        {
            "~/{1}/Views/{0}.cshtml",
        };

        PartialViewLocationFormats = new string[]
        {
            "~/Shared/Views/{0}.cshtml"
        };
    }
}

The {1} and {0} in the location formats correspond to the name of the controller and the name of the view respectively.

Add the custom razor view engine to Global.asax

In order to use the new custom view engine, we need to clear the existing default view engines and add our new custom view engine in Global.asax.

  1. Clear the view engines in Global.asax.

  2. Add the new custom razor view engine in Global.asax.

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        ViewEngines.Engines.Clear();

        ViewEngines.Engines.Add(new CustomRazorViewEngine());

        AreaRegistration.RegisterAllAreas();
            
        RouteConfig.RegisterRoutes(RouteTable.Routes);
    }
}

Create shared views

We need to create a root-level folder for views that are shared across the high-level use cases of our application (e.g. our site layout page) and configure our MVC application to use these shared views.

  1. Create a new root folder called Shared and a child folder called Views.

    Creating Shared/Views folder in solution explorer
  2. Create a _Layout.cshtml file in the Shared/Views folder.

    @using System.Web.Mvc.Html
    @{
        Layout = null;
    }
    <!DOCTYPE html>
    <html>
        <head>
            <meta name="viewport" content="width=device-width" />
            <title>Clean Architecture in ASP.NET MVC 5</title>
            <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
            <link href="@Url.Content("~/Content/Bootstrap.css")" rel="stylesheet" type="text/css" />
        </head>
        <body>
            <div class="navbar navbar-inverse navbar-fixed-top">
                <div class="container">
                    <div class="navbar-header">
                        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                            <span class="icon-bar"></span>
                            <span class="icon-bar"></span>
                            <span class="icon-bar"></span>
                        </button>
                        @Html.ActionLink("Clean Architecture", "index", "home", new { area = "" }, new { @class = "navbar-brand" })
                    </div>
                    <div class="navbar-collapse collapse">
                        <ul class="nav navbar-nav">
                            <li>@Html.ActionLink("Home", "Index", "home")</li>
                            <li>@Html.ActionLink("Products", "index", "products")</li>
                            <li>@Html.ActionLink("Contact", "index", "contact")</li>
                        </ul>
                    </div>
                </div>
            </div>
            <div class="container body-content">
                @RenderBody()
                <hr />
                <footer>
                    <p>&copy; @DateTime.Now.Year - <a href="http://www.matthewrenze.com" target="blank">Matthew Renze</a></p>
                </footer>
            </div>
        </body>
    </html>

  3. Create an Error.cshtml file in the Shared/Views folder (optional).

    @model System.Web.Mvc.HandleErrorInfo

    @{
        ViewBag.Title = "Error";
    }

    <h1 class="text-danger">Error.</h1>
    <h2 class="text-danger">An error occurred while processing your request.</h2>

  4. Create a _ViewStart.cshtml file in the root folder with Layout = "~/Shared/Views/_Layout.cshtml".

    @{
        Layout = "~/Shared/Views/_Layout.cshtml";
    }

  5. Merge the contents of the Shared/Views web.config into the app-level web.config.

    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
      <configSections>
        <sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
          <section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
          <section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
        </sectionGroup>
      </configSections>
      <appSettings>
        <add key="webpages:Version" value="3.0.0.0"/>
        <add key="webpages:Enabled" value="false"/>
        <add key="ClientValidationEnabled" value="true"/>
        <add key="UnobtrusiveJavaScriptEnabled" value="true"/>
      </appSettings>
      <system.web>
        <compilation debug="true" targetFramework="4.5"/>
        <httpRuntime targetFramework="4.5"/>
      </system.web>
      <system.web.webPages.razor>
        <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.2.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
        <pages pageBaseType="System.Web.Mvc.WebViewPage">
          <namespaces>
            <add namespace="System.Web.Mvc" />
            <add namespace="System.Web.Mvc.Ajax" />
            <add namespace="System.Web.Mvc.Html" />
            <add namespace="System.Web.Routing" />
            <add namespace="CleanArchitectureInAspNetMvc5" />
          </namespaces>
        </pages>
      </system.web.webPages.razor>
      <system.webServer>
        <handlers>
          <remove name="BlockViewHandler"/>
          <add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
        </handlers>
      </system.webServer>
      <!-- Remaining web.config sections go here. -->
    </configuration>

    This is done so that the Shared/Views web.config settings apply to all views in our application and thus we don't have to create a web.config file for each view folder in our project.

  6. Set the BlockViewHandler path to "*.cshtml" in the merged web.config.

    <system.webServer>
        <handlers>
          <remove name="BlockViewHandler"/>
          <add name="BlockViewHandler" path="*.cshtml" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
        </handlers>
      </system.webServer>

Create folders based on high-level use cases

Now we can create our root-level folders that correspond to the high-level use cases of our application. In the case of our sample application, we're going to create three high-level use cases (i.e. Home, Products, and Contact) and corresponding models, views, and controllers.

  1. Delete the unused root-level Models, Views, and Controllers folders.

    Deleting rool-level controllers, models and views folders.
  2. Create top-level use-case folders.

    Creating new rool-level use-case folders.
  3. Create the corresponding Models, Views, and Controllers folders for each high-level use case.

    Creating new model, view, and controller subfolders.
  4. Create our models.

    public class Product
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public decimal Price { get; set; }
    }

  5. Create our views.

    @model IEnumerable<CleanArchitectureInAspNetMvc5.Products.Models.Product>

    @{
        ViewBag.Title = "Products";
    }

    <h2>Products</h2>
    <table class="table">
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Name)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Price)
            </th>
            <th></th>
        </tr>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Name)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    @Html.ActionLink("Details", "Details", new { id = item.Id })
                </td>
            </tr>
        }
    </table>

    @model CleanArchitectureInAspNetMvc5.Products.Models.Product

    @{
        ViewBag.Title = "Product Details";
    }

    <h2>Product Details</h2>
    <div>
        <h4>Product</h4>
        <hr />
        <dl class="dl-horizontal">
            <dt>
                @Html.DisplayNameFor(model => model.Name)
            </dt>

            <dd>
                @Html.DisplayFor(model => model.Name)
            </dd>

            <dt>
                @Html.DisplayNameFor(model => model.Price)
            </dt>

            <dd>
                @Html.DisplayFor(model => model.Price)
            </dd>
        </dl>
    </div>
        <p>
        @Html.ActionLink("Back to List", "Index")
    </p>

  6. Create our controllers.

    public class ProductsController : Controller
    {
        private readonly List<Product> _products = new List<Product>
        {
            new Product { Id = 1, Name = "Ice Cream", Price = 1.23m },
            new Product { Id = 2, Name = "Cake", Price = 2.34m }
        };

        public ActionResult Index()
        {
            return View(_products);
        }

        public ActionResult Details(int id)
        {
            var product = _products
                .First(p => p.Id == id);

            return View(product);
        }
    }

Please note that we should not use the Create Controller command from the context menu of the Controllers folder to create our controller as it will attempt to scaffold out a folder containing the views for the new controller in the first folder that it finds named Views. Instead, create a new empty class called [OurUseCaseName]Controller.cs and have it inherit from System.Web.Mvc.Controller.

Enable routing to existing files

If we implement the screaming architecture folder structure, our root-level folder names will exactly match the first segment of our URI. For example, if we have ~/Customers as our folder for the customer use cases we will navigate to that resource's controller via http://www.example.com/customers. However, by default, IIS thinks we are asking to browse the Customers folder in the root of our application, and since we have directory browsing disabled by default, we will get a 403.14 error (Forbidden).

The solution to this problem is to complete the following step:

  1. Add "routes.RouteExistingFiles = true;" to RouteConfig.cs.

    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.RouteExistingFiles = true;
                
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
        }
    }

This tells ASP.NET routing to handle all incoming requests, even those that match an existing file or folder.

Add content

  1. Create a folder called Content in the root folder.

  2. Add any CSS files or content to this folder.

  3. Adding content.

Conclusion

We now have a functional ASP.NET MVC application with our root-level folders named after the high-level use cases of our application. From here, we can continue to add other use cases as necessary without the framework-driven folder organization that ASP.NET MVC imposes on us.

Please note that there are some tradeoffs to this solution. The most obvious disadvantage is we can no longer use the automatic scaffolding features that are integrated with Visual Studio when we create our controllers and views. Instead, we have to create them by hand like we would any other class file or view file. In addition, if we need to serve static content from our website, since we set our routes.RouteExistingFile = true in our RouteConfig.cs file, we may have to create a controller that can serve this static content on request.

If you have any questions, comments, or feedback, please let me know.

Share this article: Share on Facebook Share on Twitter Share on LinkedIn Share on Google+