How to Implement Custom URLs in Kentico Xperience 12 MVC 
In the last post, How to Layout Your URLs, Don't Get Lost in the (Content) Trees, I described what an optimal URL structure looks like for your new site, but glossed over how you’d actually go about implementing it in Kentico Xperience MVC. What we really need is a way that’s simple enough that anyone with a history with ASP.MVC should find natural and trivial, but powerful enough to give everyone what they need.

What’s the Secret Sauce?

Routing in ASP.MVC is complex, and understanding the entire request pipeline pre-controller can take some time to wrap your head around. Luckily for us, there’s a single almost comically simple point that Microsoft has given us developers to control routing. The humble IRouteConstraint interface Match function. For your consideration below: a constraint that limits a part of your route to start with the word “bob.”
public class RoutesThatStartWithBobConstraint : IRouteConstraint
{
    public bool Match(
        HttpContextBase httpContext, 
        Route route, 
        string parameterName, 
        RouteValueDictionary values, 
        RouteDirection routeDirection)
    {
        return values[parameterName].ToString().StartsWith("bob");
    }
}
Match is a very simple function that gives you about everything there is to know about a request pre-controller and asks one simple question. Should I route to this or not? It’s one of those simple and very powerful concepts that any MVC developer should have in their toolbox.

Once you’ve got your simple constraint, go ahead and add it to a route like so.  
routes.MapRoute(
    name: "Bob Route",
    url: "{name}",
    defaults: new { controller = "Uncle", action = "Index" },
    constraints: new { name = new RoutesThatStartWithBobConstraint() });
In the above example, what I’m doing is routing any URLs that start with the word “bob” to the Index method on a controller named “UncleController”.
public class UncleController : Controller
{
    public ActionResult Index(string name)
    {
        return View(name);
    }
  

    [HttpPost]
    public ActionResult GiveProps(string name, string message)
    {
        return View(name, message);
    }
}
So in the above example, we have the following URLs: 
  • [get] /bob -> Hits the index method and passes along the name bob.
  • [get] /bobby -> Hits the index method and passes in the name bobby.
  • [post] /bobbert/giveprops -> Posts to the GiveProps method with the name bobbert and whatever message you have in your post data.
  • [get] /robbert -> 404 Bob not found.

More Power

Let’s go over one more powerful feature of the route constraint before we break out of demo mode. You can pass constructor parameters into them to maximize reuse. Let’s refactor this a bit to enable weird uncle dave to have his controller.
public class StartsWithConstraint : IRouteConstraint
{
    private string phrase;
 

    public StartsWithConstraint(string phrase)
    {
        this.phrase = phrase;
    }
 

    public bool Match(
        HttpContextBase httpContext, 
        Route route, 
        string parameterName, 
        RouteValueDictionary values, 
        RouteDirection routeDirection)
    {
        return values[parameterName].ToString().StartsWith(phrase);
    }
}
Then we can refactor our route definitions to let uncle dave have his /index/daveyboydave urls.
routes.MapRoute(
    name: "Bob Route",
    url: "{name}",
    defaults: new { controller = "Uncle", action = "Index" },
    constraints: new { name = new StartsWithConstraint("bob") });
 

routes.MapRoute(
    name: "Dave Route",
    url: "{adjective}/{uncle}",
    defaults: new { controller = "Uncle", action = "Dave" },
    constraints: new { uncle = new StartsWithConstraint("dave") });
If you noticed, we switched up the front of the URL, allowing any adjective to be tossed in there. Uncle Dave is a bit more of a free spirit, and you can route to him using any word followed by his name.
  • /weird/dave
  • /goofy/daveyboy
  • /chilled-out/davearoo
All of the above route to the Dave action on the Uncle controller here:
public class UncleController : Controller
{
    [HttpGet]
    public ActionResult Index(string name)
    {
        return View(name);
    }
 

    [HttpGet]
    public ActionResult Dave(string adjective, string uncle)
    {
        return View(adjective, uncle);
    }
}

It Gets Easier

The above method is ok, and a decade back I probably would have been very happy with it as a solution. These days I’m used to modern niceties like having attribute routing defined right on the controller, so I don’t have to dig around in the startup code for my routes. So how do we specify our constraints on attribute routes? It’s actually even less work than the demo above, but we have to learn about one more concept, the ConstraintResolver. You could go read the linked documentation, however, it’s kinda dense for what you really need to know. TLDR: The constraint resolver allows you to add custom syntax to your attribute routes.

See this magic in action as we get rid of those old school hardcoded routes and constraints with our custom one.

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreStaticContentRoutes();
    routes.Kentico().MapRoutes();

    var constraintsResolver = new DefaultInlineConstraintResolver();
    constraintsResolver.ConstraintMap.Add("startsWith", typeof(StartsWithConstraint));

    // Now go register all the attribute routes but with our constraints added.
    routes.MapMvcAttributeRoutes(constraintsResolver);
    routes.MapRoute(
        name: "404 Route",
        url: "{**notFound}",
        defaults: new { controller = "Error", action = "NotFound" });
}
Now when we’re working on our controller, we can see what URLs our controllers are controlling and what constraints are in place. We don’t have to give up the modern conveniences of attribute routing to enable an optimal URL structure.
public class UncleController : Controller
{
    [HttpGet]
    [Route("{name:startsWith(bob)}")]
    public ActionResult Index(string name)
    {
        return View(name);
    }
 

    [HttpGet]
    [Route("{adjective}/{uncle:startsWith(dave)}")]
    public ActionResult Dave(string adjective, string uncle)
    {
        return View(adjective, uncle);
    }
}

How Does This Hook Into Kentico Xperience?

If you’re fresh from my last blog post you probably remember routing looking something like this:

routing code

Hopefully,  the gears in your head are turning now.  If we wanted a website where sales can tell their customers, “Hey, visit our website at website.com/mens/shirts,” we can now see how this could be implemented.

I’ve built myself a route constraint that looks up (heavily cached) nodes in Kentico Xperience by their node alias. Instead of our demo example where we’re doing a simple StartsWith() function, we’re going out and checking if there’s a node alias in Kentico Xperience that matches the incoming route part.
public class NodeAliasConstraint : IRouteConstraint
{
    public bool Match(
        HttpContextBase httpContext, 
        Route route, 
        string parameterName, 
        RouteValueDictionary values, 
        RouteDirection routeDirection)
    {
        var alias = values[parameterName].ToString();
        var results = CacheHelper.Cache(cs =>
        {
            var tree = new TreeProvider();
            List<TreeNode> nodes = tree.SelectNodes()
                .WhereLike("NodeAlias", alias)
                .OnSite("CorporateSite", true)
                .ToList();
 

            if (cs.Cached)
            {
                cs.GetCacheDependency = () => CacheHelper.GetCacheDependency($"nodes|CorporateSite");
            }
 

            return nodes;
        }, new CacheSettings(3600, "pagesnamed|get", alias, "CorporateSite"));
 

        return results.Any();
    }
}
This is obviously demo code, and you’ll need to modify it to account for all your normal preview and localization needs. The basics are all here, though. You can have your route query Kentico Xperience and respond if it’s a valid route or not.  It’s not just node alias either; you can write any query you want to in here. Extending its abilities is very simple and can be done by passing in parameters through your constructor. You can mix and match multiple constraints on a route, or you could also read from the RouteValueDictionary to grab more data to narrow down your query.

Careful Now!

All this power comes with some responsibility. You’ll want to make a small set of flexible, yet specific constraints.  Flexible enough to give your fellow developers an easy and enjoyable experience, but specific enough that you’re not going to run into multiple routes, both attempting to match the same URL. It’s a balancing act, but when it’s done right, you and your users will really enjoy the result.

Share This Post:

Twitter Pinterest Facebook Google+
Click here to read more Kentico posts
Start a Project with Us
Photo of the author, Jordan Whiteley

About the author

Jordan is a low key guy with the drive to learn about software architecture, functional programming, and web automation. He’s been into computers since the day his uncle gave him his old commodore 64. He even chose to buy his own computer instead of a car when he turned 16. Currently, he enjoys being a Lego robotics coach for middle schoolers, playing Super Nintendo, taking walks in the woods, and snuggling on the couch with his 3 kids and 2 cats.

View other posts by Jordan

Subscribe to Updates

Stay up to date on what BizStream is doing and keep in the loop on the latest with Kentico.