Kentico 12: Design Patterns Part 10 - MVC Routing with NodeAliasPath
Tags: dotnet, kentico, mvc, cms
Published: 2019-08-12 13:07:43

---
title: Kentico 12: Design Patterns Part 10 - MVC Routing with NodeAliasPath
published: true
description: Learn how to build your Kentico 12 MVC applications with the same dynamic routing, based on NodeAliasPath, used in Kentico CMS Portal Engine sites.
tags: dotnet, kentico, mvc, cms
series: Kentico 12 - Design Patterns
---

![](https://thepracticaldev.s3.amazonaws.com/i/ojg9hpamdx8mab6qmh3g.jpg)
<figcaption>Photo by <a href="https://unsplash.com/@hilesy">George Hiles</a> on <a href="https://unsplash.com">Unsplash</a></figcaption>

> To celebrate #10 in my _Kentico 12 - Design Patterns_ series 🎉, I've written up a long post describing a cool approach for unlocking the power of traditional Portal Engine routing in our Kentico 12 MVC apps. Read on!

Building Kentico CMS applications using the ASP.NET MVC technology stack enables developers to use a great framework and take full control over the delivery and presentation of all the content managed within Kentico.

Kentico 12 MVC has many advantages compared to previous versions of the platform (which you can [read more about here](https://www.kentico.com/product/kentico12)), but there are some features of Kentico's previous, Web Forms based, Portal Engine architecture that don't have a clear counterpart in MVC.

One of the features, that developers with prior Kentico experience, might find themselves looking for, is a way to have routing in their MVC applications work with the `NodeAliasPath` value of pages in the CMS content tree.

This isn't possible with the pieces that Kentico provides out-of-the-box 😟, but with a few key integration points into the MVC framework we can effectively replicate this behavior and build sites with very dynamic routing 🙌.

Let's take a look at an example using the Kentico demo Dancing Goat site of how things are built by default, and then we'll look at how we get to `NodeAliasPath` driven MVC routing.

## MVC Driven Convention Based Routing

### Articles in the CMS Content Tree

If we look at the content tree of the Dancing Goat demo we can see a node named `Articles` which is an instance of the custom Page Type `Article section (MVC)`.

This page exists only to organize content within the content tree - as it doesn't have a "Page" tab - and it isn't represented as a navigable item in the MVC application.

![](https://thepracticaldev.s3.amazonaws.com/i/a3i9c1ukyn4pgdpxf9w3.jpg)

The child pages are instances of the custom Page Type `Article (MVC)` and they do have the "Page" tab enabled - which means they can be routed to in the MVC application by specific URL patterns.

How is the routing handled for this page and the content nested beneath it?

Let's take a look...

### Articles in MVC

First we note that there is an explicit route defined in `App_Start\RouteConfig.cs` in the `DancingGoat` application:

```csharp
var route = routes.MapRoute(
    name: "Article",
    url: "{culture}/Articles/{guid}/{pageAlias}",
    defaults: new 
    { 
        culture = defaultCulture.Name, 
        controller = "Articles", 
        action = "Show" 
    },
    constraints: new 
    { 
        culture = new SiteCultureConstraint(), 
        guid = new GuidRouteConstraint() 
    }
);
```

The route configuration for this content is using [MVC's convention based routing](http://www.dotnetfunda.com/articles/show/3029/different-types-of-routing-in-aspnet-mvc) (as opposed to [Attribute Routing](https://devblogs.microsoft.com/aspnet/attribute-routing-in-asp-net-mvc-5/)).

> Given that we have an explicit route defined for this content that only uses a single controller, it's not very convention based - this is feeling more like configuration. This is normal - MVC's routing conventions only go so far without customization 🤷‍♀️.

We can already see how the URLs for our content are tied to the MVC application route configuration. All articles in the site are found under a `{culture}/Articles/{guid}/{pageAlias}` path pattern.

We can see that the route parameter `{guid}`, which is the `NodeGuid` of the `Article (MVC)` page instance in the tree, is included to ensure the MVC application can find the right page.

![](https://thepracticaldev.s3.amazonaws.com/i/q47oyq4wmkf15sq90oua.jpg)
<figcaption>An example of a URL for the Article - "Coffee Beverages Explained"</figcaption>

> The case where we would need `NodeGuid` to uniquely identify a page is if there are multiple `Article (MVC)` pages under different parents but with the same name (`{pageAlias}`), as `{pageAlias}` means `NodeAlias` - the 'slugified' version of the page's friendly name.

> The `Guid` is kinda forced on us to bridge 🌉 the gap between content management in the CMS and content delivery in MVC.

The rendering controller of `Article (MVC)` content in our site is exclusively handled by `Controllers\ArticlesController.cs`.

This isn't a bad thing - in fact for most use cases this will make sense - no matter where an instance of `Article (MVC)` is in the content tree, we will want it to be retrieved from the database, mapped to a view model, and rendered in the same way on the MVC side.

The primary things I want us to note from this example are the following:

1. We are using Convention Based Routing, but it's looking more like Configuration Based.
1. Our URL patterns are not configurable by the content editors in the CMS - the `/Articles/` segment of the URL pattern above is hard-coded.
1. We need to insert a `Guid` value into the URL to make it unique enough for the MVC application to find the right `Article (MVC)` instance - `Guid`s aren't very user friendly and it feels like a necessary evil.

> To be fair, Kentico provides a way around #2 above using [MVC Alternative URLs](https://docs.kentico.com/k12sp/developing-websites/configuring-page-urls-on-mvc-sites/enabling-alternative-urls-for-pages), but this feature is best used as a way to generate nice looking URLs for marketing purposes, not as the primary URL generation for all site content.

## But Does it Scale?

This routing design pattern presented by Kentico in the Dancing Goat demo site __is a good pattern__! It will work well for you! 💪

ASP.NET MVC developers have been building apps for a decade using this approach and if it fits your needs, then the rest of this post will be for education only.

Allow me to posit a scenario where the above routing architecture will not work.

Imagine we have two authors of articles on our site, Axel and Rose, and they each want their own vanity URLs with their names in them as follows:

`/axels-coffee-corner/{articleName}`
`/rose-knows-coffee/{month}-{year}/{articleName}`

Axel writes his articles infrequently so having all of his articles nested under `/axels-coffee-corner/` is good enough.

Rose, however, writes articles on coffee twice a day, even on weekends (she really likes coffee ☕ and writing 📝!), so we want to add a `{month}-{year}` pattern in her URLs so her readers know how fresh her content is.

Rose also mentioned some other people in the company have become interested in writing about coffee and might want to start their own sections of articles on the site... 😨

We have a couple problems here with our existing MVC routing architecture.

- ❌ We don't have a good way to separate one author's articles from another's via URLs - we only have 1 pattern: `{culture}/Articles/{guid}/{pageAlias}`.
- ❌ If we use a page container (`{month}-{year}`) in the CMS to organize Rose's articles, it will be tricky to parse those out of the URL on the MVC side, and then use those values to query for the right item in the database. We _will_ need to use them for querying as they effectively take the place of using `NodeGuid` to guarantee uniqueness.
- ❌ Adding any new authors to the site with their own vanity URLs will require new routing patterns in the MVC application to be deployed.

Our requirements are looking more and more like the organization of the content in the CMS should match the URLs we use to navigate to it.

We don't want to parse URLs into logical segments for querying since a 'full' URL already exists in the database - the `NodeAliasPath`.

At the same time we want to easily and consistently render the content in the same way on the MVC site without needing to continuously deploy route configuration updates. 😒

MVC is all about content delivery, not content organization, so how do we truly abstract the rendering of the content in MVC from the organization and URL identification of it in the CMS? 🤔

## Routing by `NodeAliasPath` with `IRouteConstraint`

Let's start by defining what we need to accomplish:

1. ✅ Configure a Route that matches any `NodeAliasPath`.
1. ✅ Only have our Route match for the given `NodeAliasPath` if the Page associated with it is an instance of a specific Page Type.
1. ✅ Have this Route connected to a specific `Controller` and `Action` which are coded to handle pages of the required Page Type.

### Configuring Our Route

All of these requirements can be fulfilled by implementing the `System.Web.Routing.IRouteConstraint` interface.

`IRouteConstraint` has 1 method, `Match`, which returns `true` when the context of the request and Route match our requirements and `false` when they do not:

```csharp
bool Match(
    HttpContextBase httpContext, 
    Route route, 
    string parameterName, 
    RouteValueDictionary values, 
    RouteDirection routeDirection)
```

We assign instances of classes that implement `IRouteConstraint` to our Convention based route definitions in an object that we pass to the `constraints` parameter.

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

In the above example we are constraining the `{id}` route parameter by the implementation of `MyCustomConstraint`.

Its `Match` method will be passed `"id"` as the value of the `string parameterName` parameter, which can be used to look up the value in the `RouteValueDictionary values` parameter.

Let's first define our Route and then implement `IRouteConstraint` for our use-case below:

```csharp
routes.MapRoute(
    name: "Articles",
    url: "{*nodeAliasPath}",
    defaults: new
    {
        controller = "Articles",
        action = "Show"
    },
    constraints: new
    {
        nodeAliasPath = new NodeAliasPathConstraint(Article.CLASS_NAME)
    });
```

Above we are defining a route with a very loose URL pattern of `{*nodeAliasPath}`, which effectively matches _every URL_.

This would normally be bad since only URLs for Articles should be handled by `ArticlesController.Show`, but here our `NodeAliasPathConstraint` helps us.

The constraint takes the Kentico `CLASS_NAME` of the `Article` (`Article (MVC)` Page Type) class as a parameter and constrains the `nodeAliasPath` token of the Route pattern template to meet its requirements. 👍

### Defining Our Constraint

Below is a yet-to-be completed implementation of our constraint:

```csharp
public class NodeAliasPathConstraint : IRouteConstraint
{
    private readonly string nodeClassName;

    public NodeAliasPathConstraint(string nodeClassName) => 
        this.nodeClassName = nodeClassName;

    public bool Match(
        HttpContextBase httpContext, 
        Route route, 
        string parameterName, 
        RouteValueDictionary values, 
        RouteDirection routeDirection)
    {
        // ❗ If we didn't configure the route correctly and no
        // nodeAliasPath was defined in the route URL template
        // we can't match

        if (!values.TryGetValue(parameterName, out object nodeAliasPathObj))
        {
            return false;
        }

        // Converts nodeAliasPathObj into a TreeNode.NodeAliasPath
        // by prefixing with a /

        string nodeAliasPath = $"/{nodeAliasPathObj}";

        // TODO: Determine if nodeAliasPath matches any page in the CMS
        // with a TreeNode.NodeClassName matching our nodeClassName

        return false;
    }
}
```

Above, `NodeAliasPathConstraint`, takes a `nodeClassName` and constructs a `nodeAliasPath` from the Route values.

We now need to query the database to check for two conditions:

1. There exists a page in the CMS that has a `TreeNode.NodeAliasPath` matching our `nodeAliasPath` captured by the route parameters for the current request.
1. That page has a `TreeNode.NodeClassName` which matches the types of pages our Route can handle, as defined by `nodeClassName`.

Let's add the logic to query the database:

```csharp
// Additional filters (e.g. culture) should be applied as needed

bool isPreview = HttpContext.Current.Kentico().Preview().Enabled;

var node = DocumentHelper.GetDocuments()
    .WhereEquals(nameof(TreeNode.NodeAliasPath), nodeAliasPath)
    .LatestVersion(isPreview)
    .Published(!isPreview)
    .OnSite(SiteContext.SiteName)
    .CombineWithDefaultCulture()
    .TopN(1)
    .Column(nameof(TreeNode.ClassName))
    .FirstOrDefault();

if (node is null)
{
    return false;
}

if (string.Equals(node.ClassName, nodeClassName, StringComparison.OrdinalIgnoreCase))
{
    // ❗ We replace the `nodeAliasPath` route value with the one we prefixed
    // above so that the ArticlesController.Show(string nodeAliasPath)
    // call is passed a correct TreeNode.NodeAliasPath value

    values[parameterName] = nodeAliasPath;

    return true;
}

return false;
```

We want to keep our query fast 🏃‍♀️ since it will be called any time MVC's routing process needs to know if our "anything" Route of `"{*nodeAliasPath}"` is a match for a specific URL.

So, we limit the columns to `ClassName`, and take only the first result (we should only have 1).

If no page existed with a `NodeAliasPath` matching the URL, we return `false`.

If the query above returns a node for a page and the page's `NodeClassName` doesn't match the Page Type handled by this route, we return `false`.

We only return `true` when a page exists in the content tree with a matching `NodeAliasPath` and and the page `NodeClassName` matches the one provided to the Constraint's constructor.

### Optimizing Our Logic with Caching

Currently, if we use this Constraint on multiple routes, all with the same `"{*nodeAliasPath}"` URL template, our constraint - and therefore our query - will continue to be executed until it succeeds - once for each Route that MVC checks the requested URL against.

In the worst case, for example, with 15 Controllers, each handling a different Page Type, we will have 15 Routes defined with unique instances of this Constraint. If the last Route checked is the correct one then we will run this query 15 times. 😝 Blech!

Our Constraint will also be called on every subsequent HTTP request for the same route (i.e., you make a request and then refresh the page).

We don't want to repeat this query needlessly when the result is already known!

The quick and effective way to resolve this performance scalability issue is to use caching.

Kentico provides a `CacheHelper` class to help developers add items to the cache and set the dependencies of those cached items.

Here is how our use of `CacheHelper` will look:

```csharp
Func<TreeNode> query = () => DocumentHelper.GetDocuments()
    .WhereEquals(nameof(TreeNode.NodeAliasPath), nodeAliasPath)
    .LatestVersion(isPreview)
    .Published(!isPreview)
    .OnSite(SiteContext.SiteName)
    .CombineWithDefaultCulture()
    .TopN(1)
    .Column(nameof(TreeNode.ClassName))
    .FirstOrDefault();

string scope = nameof(NodeAliasPathConstraint);
string cacheItemName = $"{scope}|preview:{isPreview}|" +
    {SiteContext.SiteName}|{nodeAliasPath}"

var node = CacheHelper.Cache(
    query,
    new CacheSettings(
        cacheMinutes: 5, 
        cacheItemNameParts: cacheItemName)
    {
        GetCacheDependency = () => CacheHelper.GetCacheDependency(new[]
        {
            $"node|{SiteContext.CurrentSiteName}|{nodeAliasPath}"
        })
    });

// Check if node is a match ...
```

This use of `CacheHelper` will cache the result of our query for a `TreeNode` matching the given `nodeAliasPath` for 5 minutes and also clear the cache if the CMS updates any node matching the `nodeAliasPath` for the current site.

We now have a reusable `NodeAliasPathConstraint` class that can be used to constrain which URLs match all the various Routes our MVC application defines.

Since the Route we defined above matches on any `NodeAliasPath` for an `Article (MVC)` page, our `ArticlesController` can handle any Article anywhere in the content tree, no matter how it is organized or what the URL looks like - as long as the URL is the page's `NodeAliasPath`.

Both Axel and Rose's Article URLs will work how they expected. 🤸‍♂️

Any new authors simply need to create a container page in the tree and start nesting new `Article (MVC)` page instances beneath it.

## Caveats of Convention Based Routing

Our `NodeAliasPath` based routing works exactly how we hoped, and there's nothing wrong with stopping here 😅.

However, with the current design we're going to end up with a bunch of `RouteCollection.MapRoute()` calls in our MVC application that effectively do the same thing, all in slightly different ways.

We also have the problem with "Scattered Strings" that I've mentioned in previous posts: 

```csharp
defaults: new
{
    controller = "Articles",
    action = "Show"
}
```

If we rename our `ArticlesController` to `ArticleController` (no `s`), our Route will break unless we update the above line to `controller = "Article"` 😢.

Since we are applying Constraints to specific Controller/Action pairs to match specific `NodeAliasPath`/`NodeClassName` pairs, we will need a Route defined, in most cases, for both the "List" view and "Detail" view for each Page Type.

If we want to use a method named `List` instead of the `Index` method for showing a list of posts, we need to update the methods and also the Route configuration 😤.

If we only have a handful of Routes where we are using this Constraint then the above pattern will work well...

But, there is [a final form](https://www.youtube.com/watch?v=V3XtDFbwJ3E) to this design that will elegantly handle our `NodeAliasPath` based routing across many Controllers throughout our application - it will scale as much as we want.

Read ahead if you dare! 🎃👻

## The Ultimate Approach!

### `PageTypeRouteAttribute`

First, we are going to create a custom `Attribute` that is not based on any MVC classes.

```csharp
[AttributeUsage(AttributeTargets.Method)]
public class PageTypeRouteAttribute : Attribute
{
    public PageTypeRouteAttribute(params string[] classNames) =>
        ClassNames = classNames;

    public string[] ClassNames { get; }
}
```

This Attribute will act as a marker on all of our Controller Action methods, indicating, which custom Page Types the method supports for rendering.

> This Attribute allows for multiple class names since a single Action method could potentially handle different Page Types in a very generic and abstract way, using only their common `TreeNode` properties, or through a custom interface we create and apply to all of them 🧐.

Below are examples of how we'd use this Attribute:

```csharp
// ArticlesController.cs

[PageTypeRoute(Article.CLASS_NAME)]
public ActionResult Index(string nodeAliasPath)
{
    // ...
}

// or maybe HomeController.cs

[PageTypeRoute(Home.CLASS_NAME, "CMS.Root")]
public ActionResult Index(string nodeAliasPath)
{
    // ...
}
```

### Updating Our `NodeAliasPathConstraint`

We want to use the standard MVC routing behavior when we find a `NodeAliasPath` match, except we want the `"controller"` and `"action"` values in the `RouteValueDictionary` to come from the Controller and Action with the right `PageTypeRouteAttribute`, matched by `NodeClassName`.

We can first remove the constructor for `NodeAliasPathConstraint` we defined above because our new implementation will be used for all custom Page Types.

We will use the `NodeClassName` value returned by our query to find the correct Controller and Action to process the request, using the `PageTypeRouteAttributeCacheHelper`:

```csharp
if (!values.TryGetValue(parameterName, out object nodeAliasPathObj))
{
    return false;
}

string nodeAliasPath = $"/{nodeAliasPathObj as string}";

// Use the same CacheHelper.Cache call to query for matching Node

var node = ...

if (node is null)
{
    return false;
}

if (!PageTypeRouteAttributeCacheHelper
        .ClassNameLookup
        .TryGetValue(nodeClassName, out var pair))
{
    return false;
}

values["action"] = pair.ActionName;
values["controller"] = pair.ControllerName;
values[parameterName] = nodeAliasPath;
values["nodeClassName"] = node.ClassName;

return true;
```

> The `PageTypeRouteAttributeCacheHelper` class can be found in [a GitHub Gist here](https://gist.github.com/seangwright/d84ef3189d724359c291be2fa463f3b6) as it's too long to put in the post.

> It will reflect over all types in the application, find `Controller` instances, find all Actions on those Controllers with `PageTypeRouteAttribute`s, and finally adds a ActionName/ControllerName `struct` to a `Dictionary` with the `PageTypeRouteAttribute.ClassNames` values as keys.

> Ideally, the lookup would be performed by a service implementing an interface that is injected into the constructor of `NodeAliasPathRouteHandler` to make it more testable. I'll leave this up to the reader 🤓!

### Register Our Route

We now only need to register a single Route that uses the `NodeAliasPathConstraint`.

```csharp
routes.MapRoute(
    name: "NodeAliasPath",
    url: "{*nodeAliasPath}",
    defaults: null,
    constraints: new
    {
        nodeAliasPath = new NodeAliasPathConstraint()
    });
```

### Updating the CMS Page Type Configuration

The last thing for us to do is to change the "URL Pattern" field of the `Article (MVC)` Page Type to `{% NodeAliasPath %}`.

![](https://thepracticaldev.s3.amazonaws.com/i/yp1kpellm5p235m5ld6p.jpg)

This will ensure that when we view the "Page" tab of an Article page in the content tree, it will send the correct request to the MVC app - using the `NodeAliasPath` as the URL - and render the page using everything we built above.

## Where To Go From Here

We can use this pattern on as many custom Page Types as we want and we'll be guaranteed that the correct Controller and Action will be selected no matter where in the tree you place your content 👏.

We can also be assured that the organized structure of the CMS content tree, and the names of all the pages, will be reflected in the URLs used to access the content in the MVC application ⚡.

A pattern we've been using at [WiredViews](https://www.wiredviews.com) is to create a custom Page Type for the "list page" for each content Page Type.

So, similar to the above example with `Article (MVC)`, the `Article section (MVC)` would have the "Page" tab enabled.

We would configure the `ArticlesController` as follows:

```csharp
[PageTypeRoute(ArticleSection.CLASS_NAME)]
public ActionResult Index(string nodeAliasPath)
{
    // ...
}
```

Every Article Section would be rendered by the same logic in the MVC application but different sections could be created anywhere in the content tree, each with their own `NodeAliasPath` to be used as the URL for rendering their content 💪.

Example:

Imagine we have a "Article Container" -> "Article Author" -> "Article" content hierarchy:

`/Company-Articles/Sean-G-Wright` could list the top 10 most recent articles I've written.
`/Company-Articles/Moe-Tucker` would list Moe's top 10 most recent.
`/Company-Articles/Moe-Tucker/How-To-Make-Coffee` would be a specific article of Moe's.
`/Guest-Articles/Exene-Cervenka/Rock-And-Roll-Coffee` would show Exene's guest article.

Being able to have multiple "homes" for the same content type and not having to worry about adjusting the MVC routing is a pretty powerful capability 🚀🤘!

Two bonus perks, that I won't cover here, are:

- The ability to much more easily generate dynamic menus by using a page's `NodeAliasPath` instead of having to generate the URL via MVC's APIs.
- Creating an XML sitemap using `NodeAliasPath` for pages that would normally have complex requirements for generating URLs. Using `NodeAliasPath` can also simplify the query we'd need to source the sitemap data - the URL, image, and modification date _could_ all come from the same `DocumentQuery` for all documents, regardless of the Page Type.

## Summary

Ok, that was long... but if you've read my posts before you know I enjoy digging deep into design patterns, Kentico, and ASP.NET MVC - so, sorry #notsorry 🤣.

We have a solution for `NodeAliasPath` based routing in our Kentico 12 MVC application, but what all did we accomplish exactly?

- ✅ We adapted the `NodeAliasPath` based URL feature from Kentico CMS Portal Engine sites to Kentico 12 MVC, so content editors have control over URLs.
- ✅ We made it possible for Page Types to have multiple homes in the CMS without increasing the complexity of MVC Route configuration.
- ✅ We made an easily re-usable `Attribute` that clearly defines what content is connected to which Controller Action.
- ✅ We leveraged Kentico's caching API to ensure our frequently called Route Constraint doesn't impact site performance.

I'd love to get feedback on this pattern from all the Kentico developers out there that are building Kentico 12 MVC applications.

I know that Kentico is planning on adding a "dynamic routing" solution in Kentico 2020, but since we aren't in that wonderful Valhalla 🌞 of ASP.NET Core Kentico just yet, we Kentico developers need to get creative sometimes.

Thanks for reading 🙏!

---

If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:

{% tag kentico %}

Or my Kentico blog series:

- [Kentico 12: Design Patterns](https://dev.to/search?q=Kentico%2012%20-%20Design%20Patterns)
- [Kentico CMS Quick Tips](https://dev.to/search?q=Kentico%20CMS%20Quick%20Tip)