ASP.NET MVC root url’s with generic routing

Posted by William on Sep 19, 2009

Normally, the url to handle a contact view would look similar to /Home/Contact. I’m really not keen on that and wanted my top level view’s url to look like /Contact. This in itself is easy enough, you just create a route {action} and set that routes default controller value to { controller = “Root” }.

However I wanted to keep the default route mapping “{controller}/{action}/{id}” to handle generic formats along with this top level action route. The problem with allowing both these situations is the routes to handle both will always match each other. Whichever comes first will try to handle the routing for either.

For example a request for url /Admin would be happily matched by the route {action} or {controller}/{action}. In this case, if we wanted the /Admin url to be handled by the AdminController, the {action} route would match the request and try to route it to RootController’s Admin action; which of course does not exists. If we swapped the order of these routes in the global.asax file then /Admin would be matched properly and routed to AdminController’s Index action. However, now a requested for url /Contact would result in the generic route assuming /Contact is a controller and routing the request to ContactController’s Index method. Which does not exist because Contact is in-fact an action in RootController.

1
2
3
4
5
6
7
8
9
10
11
routes.MapRoute(
    "Root",
    "{action}",
    new { controller = "Root", action = "Index" }
);
 
routes.MapRoute(
    "Generic",
    "{controller}/{action}/{id}",
    new { controller = "Generic", action = "Index", id = "" }
);

The solution was to create a custom constraint on the root route. This constraint would check to see if a controller exists that matches the {action} parameter’s value. If it does then we know that a controller has been requested and that the root route should not handle it. This will result in the next route in the table (the generic route) being assessed to handle the request.

In order to implement this you simply create a new custom contraint object and inherit IRouteContraint. In the constructor of the custom contraint assess the assembly for all types that inherit from Controller and create a dictionary object to keep hold of them. Then, every time the route engine assesses the request by calling Match check the action that has been requested and see if there is a specific controller that has the same name.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System.Collections.Generic;
using System.Linq;
using System.Web.Routing;
using System.Reflection;
 
namespace System.Web.Mvc
{
    public class IsRootActionConstraint : IRouteConstraint
    {
        private Dictionary<string, Type> _controllers;
 
        public IsRootActionConstraint()
        {
            _controllers = Assembly
                                .GetCallingAssembly()
                                .GetTypes()
                                .Where(type => type.IsSubclassOf(typeof(Controller)))
                                .ToDictionary(key => key.Name.Replace("Controller", ""));
        }
 
        #region IRouteConstraint Members
 
        public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
        {
            return !_controllers.Keys.Contains(values["action"] as string);
        }
 
        #endregion
    }
}

Now all that is left to do is pass a new instance of the custom constraint in the routing table.

1
2
3
4
5
6
7
8
9
10
11
12
routes.MapRoute(
    "Root",                                                 
    "{action}",
    new { controller = "Root", action = "Index" }
    new { IsRootAction = new IsRootActionConstraint() }  // Route Constraint
);
 
routes.MapRoute(
    "Generic",
    "{controller}/{action}/{id}",
    new { controller = "Generic", action = "Index", id = "" }
);

You will now be able to use much neater root url’s and still have the advantage of a generic route in your routing table