William Duffy

Glasgow Based C# ASP.NET Web Developer

ASP.NET MVC root url’s with generic routing

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 :)


Categorized as Uncategorized

13 Comments

  1. Thanks for this, it was just the kind of thing I was looking for. Being new to MVC and routing, this was the kind of thing I expected the routing would do by default. Now I have a generic constraint class that validates the controller method and parameters :)

  2. Awesome! :) Thank you

  3. Thank you! I have been searching for hours to find this!

  4. Great Solution, Thanks William. I was about to give up on achieving this and was dreading having to make a controller for each desired root URL (not that I’d have gone that far… I hope).

    However, I had one small problem… for a generic controllers default page it would only work if the URL was typed with a capital letter. E.g. /Admin would be routed correctly but /admin would go to the RootController and fail.

    I got around it by converting all the comparison strings to lower case in the IsRootActionConstraint class:

    .ToDictionary(key => key.Name.Replace(“Controller”, “”).ToLower());

    return !_controllers.Keys.Contains(value.ToLower());

    Hope this helps other readers of this post.

  5. Many Thanks. Just run through the MVC Music store app and then started building my own and this was the first issue i needed to solve.

  6. Great article.

    One minor correction to Chris Day’s suggested improvement is the last line:

    return !_controllers.Keys.Contains((values["action"] as string).ToLower());
    :)

  7. That is a very slick and nice solution to a vexing problem
    - should have been in MVC from the beginning.

  8. Thanks for this article, a superb solution and well written too. My site now copes with hyphenated urls at the root address.

  9. Alternatively, I would say ASP.Net MVC should have included a constraint to check the default route, that the controller & action actually exist.
    ………………………………….

    routes.MapRoute(
    “default”,
    “{controller}/{action}/{id}”,
    new { id = UrlParameter.Optional }
    new { found = new FoundDefaultConstraint() }
    );

    routes.MapRoute(
    “root”,
    “”,
    new { controller = “pages”, action = “home” }
    new { found = new FoundPageConstraint() }
    );

    routes.MapRoute(
    “page”,
    “{action}”,
    new { controller = “pages” }
    new { found = new FoundPageConstraint() }
    );

    routes.MapRoute(
    “generic”,
    “{*path}”,
    new { controller = “pages”, action = “missing” }
    new { missing = new MissingPageConstraint() }
    );

  10. This is a cool generic route solution. What changes would you make to this to allow for hyphenated url’s? I have a solution for hyphens, but having problems combining it with your code.

    For hyphens I used a hyphenated rount handler that removes hyphen’s before executing. Here’s my class declaration:

    public class HyphenatedRouteHandler : MvcRouteHandler
    {
    protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
    requestContext.RouteData.Values["controller"] = requestContext.RouteData.Values["controller"].ToString().Replace(“-”, “”);
    requestContext.RouteData.Values["action"] = requestContext.RouteData.Values["action"].ToString().Replace(“-”, “”);
    return base.GetHttpHandler(requestContext);
    }
    }

    Then in global.asax I have:

    routes.Add(
    new Route(“{controller}/{action}/{id}”, new RouteValueDictionary(
    new { controller = “Home”, action = “Index”, id = “” }),
    new HyphenatedRouteHandler()));

    It works, but I need your solution integrated too. Any suggestions are greatly appreciated.

comment_type == "trackback" || $comment->comment_type == "pingback" || ereg("", $comment->comment_content) || ereg("", $comment->comment_content)) { ?>

Trackbacks & Pingbacks

  1. Supporting two domains in one ASP.NET MVC site – A Poor Man’s Approach « Coding Out Loud

    [...] ASP.NET MVC root url’s with generic routing   [...]

  2. comment_type == "trackback" || $comment->comment_type == "pingback" || ereg("", $comment->comment_content) || ereg("", $comment->comment_content)) { ?>

    Trackbacks & Pingbacks

    1. PJH

      Thanks for this, it was just the kind of thing I was looking for. Being new to MVC and routing, this was the kind of thing I expected the routing would do by default. Now I have a generic constraint class that validates the controller method and parameters :)

    2. comment_type == "trackback" || $comment->comment_type == "pingback" || ereg("", $comment->comment_content) || ereg("", $comment->comment_content)) { ?>

      Trackbacks & Pingbacks

      1. Moulde

        Awesome! :) Thank you

      2. comment_type == "trackback" || $comment->comment_type == "pingback" || ereg("", $comment->comment_content) || ereg("", $comment->comment_content)) { ?>

        Trackbacks & Pingbacks

        1. Jared

          Thank you! I have been searching for hours to find this!

        2. comment_type == "trackback" || $comment->comment_type == "pingback" || ereg("", $comment->comment_content) || ereg("", $comment->comment_content)) { ?>

          Trackbacks & Pingbacks

          1. Chris Day

            Great Solution, Thanks William. I was about to give up on achieving this and was dreading having to make a controller for each desired root URL (not that I’d have gone that far… I hope).

            However, I had one small problem… for a generic controllers default page it would only work if the URL was typed with a capital letter. E.g. /Admin would be routed correctly but /admin would go to the RootController and fail.

            I got around it by converting all the comparison strings to lower case in the IsRootActionConstraint class:

            .ToDictionary(key => key.Name.Replace(“Controller”, “”).ToLower());

            return !_controllers.Keys.Contains(value.ToLower());

            Hope this helps other readers of this post.

          2. comment_type == "trackback" || $comment->comment_type == "pingback" || ereg("", $comment->comment_content) || ereg("", $comment->comment_content)) { ?>

            Trackbacks & Pingbacks

            1. Hot Custard

              Many Thanks. Just run through the MVC Music store app and then started building my own and this was the first issue i needed to solve.

            2. comment_type == "trackback" || $comment->comment_type == "pingback" || ereg("", $comment->comment_content) || ereg("", $comment->comment_content)) { ?>

              Trackbacks & Pingbacks

              1. .Net MVC & SEO Friendly URLs | HotCustard Web Designer Blog

                [...] some more searcing i located a solution to the root actions problem here http://www.wduffy.co.uk/blog/aspnet-mvc-root-urls-with-generic-routing/. This relies on using a routing constraint that checks for the existence of a matching [...]

              2. comment_type == "trackback" || $comment->comment_type == "pingback" || ereg("", $comment->comment_content) || ereg("", $comment->comment_content)) { ?>

                Trackbacks & Pingbacks

                1. Tony Gorman

                  Great article.

                  One minor correction to Chris Day’s suggested improvement is the last line:

                  return !_controllers.Keys.Contains((values["action"] as string).ToLower());
                  :)

                2. comment_type == "trackback" || $comment->comment_type == "pingback" || ereg("", $comment->comment_content) || ereg("", $comment->comment_content)) { ?>

                  Trackbacks & Pingbacks

                  1. DE

                    That is a very slick and nice solution to a vexing problem
                    - should have been in MVC from the beginning.

                  2. comment_type == "trackback" || $comment->comment_type == "pingback" || ereg("", $comment->comment_content) || ereg("", $comment->comment_content)) { ?>

                    Trackbacks & Pingbacks

                    1. ASP.NET MVC Archived Buzz, Page 1

                      [...] to Vote[Del.icio.us] ASP.NET MVC root url’s with generic routing | William Duffy (5/5/2011)Thursday, May 05, 2011 from [...]

                    2. comment_type == "trackback" || $comment->comment_type == "pingback" || ereg("", $comment->comment_content) || ereg("", $comment->comment_content)) { ?>

                      Trackbacks & Pingbacks

                      1. Rob

                        Thanks for this article, a superb solution and well written too. My site now copes with hyphenated urls at the root address.

                      2. comment_type == "trackback" || $comment->comment_type == "pingback" || ereg("", $comment->comment_content) || ereg("", $comment->comment_content)) { ?>

                        Trackbacks & Pingbacks

                        1. Alex

                          Alternatively, I would say ASP.Net MVC should have included a constraint to check the default route, that the controller & action actually exist.
                          ………………………………….

                          routes.MapRoute(
                          “default”,
                          “{controller}/{action}/{id}”,
                          new { id = UrlParameter.Optional }
                          new { found = new FoundDefaultConstraint() }
                          );

                          routes.MapRoute(
                          “root”,
                          “”,
                          new { controller = “pages”, action = “home” }
                          new { found = new FoundPageConstraint() }
                          );

                          routes.MapRoute(
                          “page”,
                          “{action}”,
                          new { controller = “pages” }
                          new { found = new FoundPageConstraint() }
                          );

                          routes.MapRoute(
                          “generic”,
                          “{*path}”,
                          new { controller = “pages”, action = “missing” }
                          new { missing = new MissingPageConstraint() }
                          );

                        2. comment_type == "trackback" || $comment->comment_type == "pingback" || ereg("", $comment->comment_content) || ereg("", $comment->comment_content)) { ?>

                          Trackbacks & Pingbacks

                          1. jibran

                            This is a cool generic route solution. What changes would you make to this to allow for hyphenated url’s? I have a solution for hyphens, but having problems combining it with your code.

                            For hyphens I used a hyphenated rount handler that removes hyphen’s before executing. Here’s my class declaration:

                            public class HyphenatedRouteHandler : MvcRouteHandler
                            {
                            protected override IHttpHandler GetHttpHandler(RequestContext requestContext)
                            {
                            requestContext.RouteData.Values["controller"] = requestContext.RouteData.Values["controller"].ToString().Replace(“-”, “”);
                            requestContext.RouteData.Values["action"] = requestContext.RouteData.Values["action"].ToString().Replace(“-”, “”);
                            return base.GetHttpHandler(requestContext);
                            }
                            }

                            Then in global.asax I have:

                            routes.Add(
                            new Route(“{controller}/{action}/{id}”, new RouteValueDictionary(
                            new { controller = “Home”, action = “Index”, id = “” }),
                            new HyphenatedRouteHandler()));

                            It works, but I need your solution integrated too. Any suggestions are greatly appreciated.

                          Leave a Reply