Modular ASP.NET Core 1.0 project structure

21 March 2016 marrrcin asp.net-core , asp.net-5 , components , project , areas

ASP.NET Core 1.0 (formely ASP.NET 5) introduced some new framework concepts as well as cut loose the project structure, giving the developers ability to fully customize it. In example, we are no more bound to the Areas folder structure, which in my opinion unnecessarily duplicated Controllers/Models/Views folder structure in each of them.

Moreover, new version of the framework added a feature called View Components, which finally resolved issue of having additional controllers returning only partial views. From now on, we can make true components in ASP.NET without any workarounds.

I've already written a few projects in ASP.NET Core 1.0 and here is how I worked my way into creating elastic project structure.

Well structured areas

First of all - the areas. They are in ASP.NET MVC for quite a while and they are really useful. Personally, I've used them every time when I had to write application with both admin panel and client facing pages. The bigger part usually landed inside main project structure and the smaller one inside the Areas folder. And... I hated that. I wanted Controllers inside the Controllers folder, Views inside Views folder and Models inside Models folder. Finally - I can do that really easily.

Let's say we have three areas: Main (client facing), Administrator and Content Authors. The output of the following configuration will look like this.

<projectroot>
└───Controllers
    └───Admin
    └───Authors
    └───HomeController.cs
    └───(...) other client controllers
└───Views
    └───Admin
    └───Authors
    └───Shared
    └───Home
        └───Index.cshtml
        └───(...) other client views
└───Models
    └───Admin
    └───Authors
    └───(...)HomePageViewModel.cs

As you can see, Admin and Author controllers are placed inside Controllers folder - it is where you would start looking for any controller in somebody else's project, don't you? The same rule applies for the Views and Models (actually - View Models) folders.

To make this structure work, the only thing we need to do is to extend Razor View Engine, so it will know how to find Views for particular Area. This can be done by implementing IViewLocationExpander interface:

public class ViewLocationExpander : IViewLocationExpander
{
    public void PopulateValues(ViewLocationExpanderContext context)
    {
        // nothing here
    }

    public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
    {
        var area =
            context.ActionContext.ActionDescriptor.RouteConstraints.FirstOrDefault(rc => rc.RouteKey == "area");
        var additionalLocations = new LinkedList<string>();
        if (area != null)
        {
            additionalLocations.AddLast($"/Views/{area.RouteValue}" + "{1}/{0}.cshtml");
        }
        return viewLocations.Concat(additionalLocations);
    }
}

Now we need to attach this class to Razor engine. Head to Startup.cs and find the line in which you add MVC to application services:

services.AddMvc();

and extend this line in order to add Razor View Engine Options:

services.AddMvc().AddRazorOptions(razorOptions =>
{
    razorOptions.ViewLocationExpanders.Add(new ViewLocationExpander());
});

From now on, for every controller marked with [Area] atrribute, the framework will search for it's views inside proper folder located under projects Views folder. Of course, this solution requires to have exactly the same name in the folder structure as in the attribute parameter. If you want to have different area name and different folder names, just add some if/switch inside of the ExpandViewLocations method and you will be good to go.

Reusable & isolated components

Some time ago, somebody asked on Stackoverflow, where the JavaScript files for the View Components should be placed in ASP.NET Core 1.0 projects. I've responed for that question, and here is my point of view (link to the answer bellow the post).

Isolation of components should not only consider JavaScript files, but also backing code (ViewComponent class) and the component's view razor file. In my projects, I tend to make this kind of structure for View Components:

<projectroot>
└───Components
    └───AwesomeComponent
        └───Awesome.cshtml
        └───AwesomeComponent.cs
        └───AwesomeComponentModel.cs
    └───CommentsComponent
        └───Comments.cshtml
        └───CommentsComponent.cs
        └───CommentsComponentModel.cs

I treat every component as totally isolated entity. It is concise and it only contains necessary files. I can easily cut the component from my project and put it into another. For example, the Comments Component (actually used on this blog to show Discuss) just renders the required HTML with some dynamically rendered parameters passed to it's view via CommentsComponentModel (which is the View Model).

View, Backend code and Model are all in one place, so when you move around the component's folder, you are sure that you move whole component. Moreover, when you are modyfying them, you have quick access to all of their parts.

It will be convinient to put JavaScript and/or CSS files which are highly coupled with a component also in such structure. You can do this by simply creating the file under the component's folder, and then writing a GULP TASK that will copy JS/CSS files to wwwroot. From that point, you will be able to link that JavaScript code on component's .cshtml using standard syntax:

<script src="~/Components/awesomecomponent.js"></script>

After such setup, in order to render View Component with it's parameters, use the following syntax:

@await Component.InvokeAsync("AwesomeComponent",new AwesomeComponentModel())

And remember to put [ViewComponent(Name = "Awesome")] annotation on the top of component's backing class if you want to avoid adding full (Name)ViewComponent convention and just have (Name)Component classes.

Additional links

Comments