ASP.NET MVC: friendlier action names and controllers

ASP.NET MVC’s default route is good for most simple case scenarios, allowing us to access actions through these type of URLs:

http://{server:port}/{controller}/{action}

Therefore, given a controller class with actions such as these:


public class CorporateInformationController : Controller
{
  public ActionResult AboutUs()
  {
    return View();
  }
  public ActionResult OurMission()
  {
    return View();
  }
  public ActionResult JoinUs()
  {
    return View();
  }
}

… we could access them using natural addresses such as /CorporateInformation/AboutUs, or /CorporateInformation/OurMission. Without doubt, this is great progress when offering our user friendly URLs, and improving our search engine optimization.

However,  wouldn’t it be better if we could access actions and controllers using hyphens to divide the different terms?  I mean, for example, uisng the /Corporate-Information/About-Us URL, or /Corporate-Information/Join-Us.

Obviously we can’t change the name of our controllers and actions, since our programming languages don’t allow the use of hyphens in identifier names.

Upon this scenario, a possibility could be assigning through the [ActionName] attribute an alternative name for each one of these actions. Nevertheless, besides the work this implies, there isn’t an easy way to achieve for the names of the controller classes either.  Another option would be to register in the route table a specific entry for each one of the actions. Even though it doesn’t have any contraindications and it would cover are needs, it’s way too much work!!!

Fortunately, there are several expansion points in the framework, were we can hook up our process logic to the requests, and use them to obtain the solution to these kinds of problems globally. And one of them is the routing system.

1. Conventions forever!

Next we are going to see how to achieve it globally using the routing system, a basing on a very simple convention that we are going to set: the identifiers of the controllers and the actions that contain an underscore (“_”) will be converted in hyphens in the routes.

Since the underscore is perfectly valid for class and method names, we only have to introduce this character in the place we want the hyphens to appear, and leave the routing system in charge of the converting them automatically. Of course, we will perform this transformation bidirectionally: both when the URL is analyzed to determine the controller and the action to be executed, as well as when the address is generated from route parameters.

2. The FriendlyRoute class

Next we are going to see a new class, that we’ll call FriendlyRoute, and which we will provide with the hyphen to underscore conversion logic, and viceversa. Nothing we can’t solve in 20 code lines:

public class FriendlyRoute : Route
{
  public FriendlyRoute(string url, object defaults) :
    base(url, new RouteValueDictionary(defaults), new MvcRouteHandler())
    { }

  public override RouteData GetRouteData(HttpContextBase httpContext)
  {
    var routeData = base.GetRouteData(httpContext);
    RouteValueDictionary values = routeData.Values;
    values["controller"] = (values["controller"] as string)
                           .Replace("-", "_");
    values["action"] = (values["action"] as string)
                       .Replace("-", "_");
    return routeData;
  }

  public override VirtualPathData GetVirtualPath(
                  RequestContext ctx, RouteValueDictionary values)
  {
    values["controller"] = (values["controller"] as string)
                           .Replace("_", "-").ToLower();
    values["action"] = (values["action"] as string)
                       .Replace("_", "-").ToLower();
    return base.GetVirtualPath(ctx, values);
  }
}

From the code above, let’s point out some things:

  • I have only created a constructor, sincerely, out of laziness. Actually, if we wanted to cover more cases, like restrictions or namespaces, that are allowed by the overloads of the Route class constructors, we should create all of them for our class. But in any case, it is very easy.
  • the GetRouteData() method is used by the framework to get the route data of an incoming request. As you can see iin the code above, we simply run the logic off the base class, and apply the corresponding transformations (from  hyphens to underscores).
  • the GetVirtualPath() method is used for inverse transformation, this is, to generate a URL starting from route paraneter values. In this case, we first retouch the controller and action name values slightly, converting the underscores into hyphens and putting them in small case, and next we invoke the base class logic so it generates the URL for us.

From this moment on we could add this class to the routing table with a code as follows:

  routes.Add("default",
     new FriendlyRoute(
         "{controller}/{action}/{id}", // URL with parameters
         new { controller = "home",
               action = "start_here",
               id = UrlParameter.Optional
         }
     )
  );

Notice that one of the advantages of using this technique is that at code level we will always use the real name of controllers and actions (with underscore), and the routing system performs these conversions. This enable us to keep using T4MVC to avoid “magic strings” in our code, among other advantages.

3. Hey… but I’m used to using routes.MapRoute()… O:-)

It’s ok, it is also very easy. The MapRoute() methods that we use in the RegisterRoutes methods of the global.asax are simply RouteCollection class extensions: Therefore, we can base on this same idea and create custom extensions that give better access to our friendly route mapping:

public static class RouteCollectionExtensions
{
  public static Route MapFriendlyRoute(this RouteCollection routes,
                                       string name, string url,
                                       object defaults)
  {
    var route = new FriendlyRoute(url, defaults);
    routes.Add(name, route);
    return route;
  }
}

This way now we just have to reference from the global.asax the space of names where we have defined the previous class and we can already fill the route table using sentences such as:

  routes.MapFriendlyRoute(
    "Default", // route name
    "{controller}/{action}/{id}", // URL with parameters
    new { controller = "home", action = "start_here", id = UrlParameter.Optional }
  );

This way once we enter the previous route in the place of the one that comes by default in ASP.NET MVC projects, our system is already ready to receive a request such as GET /Name-Controller/Name-Action and map it towards the Name_Action action of the Controller_NameController class.

Jose M. Aguilar

ASP.NET/IIS MVP

Jose M. is a well-known world expert in web technologies. He is the author of Microsoft Press SignalR Programming in Microsoft ASP.NET. He works as an independent consultant and developer, helping companies and institutions to reach their goals by using software. He also works with company development teams providing consultancy services and support in several fields. Follow Jose M on Twitter.

,

7 Responses to ASP.NET MVC: friendlier action names and controllers

  1. MC November 19, 2012 at 20:03 #

    Very helpful, thanks.

    NB: I had to put a check for routeData != null in GetRouteData().

  2. Jose M. Aguilar November 19, 2012 at 20:16 #

    Thanks for contributing!

  3. mcartur January 22, 2013 at 12:30 #

    Great article.

  4. Lelala May 18, 2013 at 15:58 #

    All that stuff to make from:
    http://www.mydomain.com/signUp
    the URL:
    http://www.mydomain.com/sign-Up
    ??
    (note the sign-up is the controller name, default action is “index”)

    Nope – there must be a simplie way?

    • Jose M. Aguilar May 18, 2013 at 20:14 #

      Hi, Lelala!

      The simplest way to do this is, for sure, to use the standard routing system to route the requests made to the url “/sign-up” to the controller class “SignUpController”.

      And of course, you could use more sophisticated approaches, such as replacing the ControllerFactory and other mechanisms to make it possible.

      Regards,
      Jose.

  5. G00N3R July 2, 2013 at 19:22 #

    Great post,

    I googled and this was by far the best I could find IMO. Going this route, does it effect load time or anything of that sort?

    Thanks, much appreciated post.

    • Jose M. Aguilar July 2, 2013 at 22:47 #

      Hi!

      The impact on performance should be minimal because they are very fast in-memory operations (character substitution). However, obviously, it’ll be slower than doing nothing ;)

      Thanks for commenting :)

Leave a Reply