5 reasons why you should use MvcCodeRouting

MvcCodeRouting v1.0 is out, this post highlights the library’s most important features and why I believe is a must have for all ASP.NET MVC developers.

1. Convention over configuration

Getting started with MvcCodeRouting in a new project, or integrating with an existing codebase, requires very little configuration, and many times no configuration at all. This is because the library recognizes that most of the time you follow the {controller}/{action} or {controller}/{action}/{id} convention. For example, let’s take the MvcMusicStore application and use MvcCodeRouting on it:

public static void RegisterRoutes(RouteCollection routes)
{
   routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

   //routes.MapRoute(
   //    "Default", // Route name
   //    "{controller}/{action}/{id}", // URL with parameters
   //    new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
   //);

   routes.MapCodeRoutes(
      rootController: typeof(Controllers.HomeController),
      settings: new CodeRoutingSettings {
         UseImplicitIdToken = true
      }
   );
}

Calling MapCodeRoutes is the only change required. You can build and run the application as if nothing changed. If you visit ~/routes.axd you can see the routes that MvcCodeRouting created:

routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(null, "{action}", 
    new { controller = @"Home", action = @"Index" }, 
    new { action = @"Index" }, 
    new[] { "MvcMusicStore.Controllers" });

routes.MapRoute(null, "StoreManager/{action}/{id}", 
    new { controller = @"StoreManager" }, 
    new { action = @"Delete|Details|Edit", id = @"0|-?[1-9]\d*" }, 
    new[] { "MvcMusicStore.Controllers" });

routes.MapRoute(null, "Store/Details/{id}", 
    new { controller = @"Store", action = @"Details" }, 
    new { id = @"0|-?[1-9]\d*" }, 
    new[] { "MvcMusicStore.Controllers" });

routes.MapRoute(null, "Checkout/Complete/{id}", 
    new { controller = @"Checkout", action = @"Complete" }, 
    new { id = @"0|-?[1-9]\d*" }, 
    new[] { "MvcMusicStore.Controllers" });

routes.MapRoute(null, "ShoppingCart/{action}/{id}", 
    new { controller = @"ShoppingCart" }, 
    new { action = @"AddToCart|RemoveFromCart", id = @"0|-?[1-9]\d*" }, 
    new[] { "MvcMusicStore.Controllers" });

routes.MapRoute(null, "Account/{action}", 
    new { controller = @"Account" }, 
    new { action = @"LogOn|LogOff|Register|ChangePassword|ChangePasswordSuccess" }, 
    new[] { "MvcMusicStore.Controllers" });

routes.MapRoute(null, "StoreManager/{action}", 
    new { controller = @"StoreManager", action = @"Index" }, 
    new { action = @"Index|Create|Edit" }, 
    new[] { "MvcMusicStore.Controllers" });

routes.MapRoute(null, "Store/{action}", 
    new { controller = @"Store", action = @"Index" }, 
    new { action = @"Index|Browse|GenreMenu" }, 
    new[] { "MvcMusicStore.Controllers" });

routes.MapRoute(null, "Checkout/AddressAndPayment", 
    new { controller = @"Checkout", action = @"AddressAndPayment" }, 
    new[] { "MvcMusicStore.Controllers" });

routes.MapRoute(null, "ShoppingCart/{action}", 
    new { controller = @"ShoppingCart", action = @"Index" }, 
    new { action = @"Index|CartSummary" }, 
    new[] { "MvcMusicStore.Controllers" });

OK, so the application worked fine with 1 route and now we have 10, how is that better? Well, in some aspects these routes work better (more on that follows), but the point I was trying to make here is how easy it is to start using MvcCodeRouting.

MvcMusicStore is actually not a very good case study, because it only uses the default route ({controller}/{action}/{id}). Routing presents no challenges when you only have one route, but real-world applications use more than one route, and that’s when you start facing issues, since adding new routes can potentially break the existing ones.

2. Automatic route grouping, ordering and constraining

Instead of creating one route per action MvcCodeRouting groups similar actions to minimize the number of routes created. For instance, the routes for the StoreManager controller are:

routes.MapRoute(null, "StoreManager/{action}/{id}", 
    new { controller = @"StoreManager" }, 
    new { action = @"Delete|Details|Edit", id = @"0|-?[1-9]\d*" }, 
    new[] { "MvcMusicStore.Controllers" });

routes.MapRoute(null, "StoreManager/{action}", 
    new { controller = @"StoreManager", action = @"Index" }, 
    new { action = @"Index|Create|Edit" }, 
    new[] { "MvcMusicStore.Controllers" });

Since Delete, Details and Edit actions take an id parameter of the same type (Int32) only one route is created that can handle those actions. Keeping the number of routes to a minimum makes route matching more efficient.

Note that Edit is also listed in the second route, because there’s also an Edit action that takes no parameters. The order of these routes affects URL generation. For example,

Url.Action("Edit", "StoreManager", new { id = 1 }) returns "/StoreManager/Edit/1",

but if you switch the order

Url.Action("Edit", "StoreManager", new { id = 1 }) returns "/StoreManager/Edit?id=1" (note id in query string).

MvcCodeRouting knows how to order routes to avoid URL generation issues.

Using constraints for the action token avoids route conflicts. MvcCodeRouting also uses constraints for action parameters, which you can override on a per-parameter or per-site basis. Constraining also helps keeping bad URLs out, e.g. /StoreManager/Edit/foo doesn’t match any route.

3. Custom routes

Now that we have conventional routes automatically created for us let’s see how we can configure more customized routes. Let’s change Store/Details/{id} to a more SEO friendly format, like p/{id}/{slug}.

This is the current action code:

public ActionResult Details(int id)
{
   var album = storeDB.Albums.Find(id);

   return View(album);
}

We’ll change it to this:

[CustomRoute("~/p/{id}/{slug}")]
public ActionResult Details([FromRoute]int id, [FromRoute]string slug = null)
{
   var album = storeDB.Albums.Find(id);

   if (album != null
      && album.Title != slug) {
      return RedirectToAction(null, new { id, slug = album.Title });
   }

   return View(album);
}

The use of the CustomRoute attribute should be self explanatory. The FromRoute attribute must be used on route parameters. Optional parameters are used to specify if the corresponding route parameter should be optional. Since we don’t have a slug property in our model I’m using Title for now.

This is the resulting route definition:

routes.MapRoute(null, "p/{id}/{slug}", 
    new { controller = @"Store", action = @"Details", slug = UrlParameter.Optional }, 
    new { id = @"0|-?[1-9]\d*" }, 
    new[] { "MvcMusicStore.Controllers" });

As you can see, custom routes are easy.

4. Route formatting

Now that we have SEO friendly routes let’s see if we can make our conventional routes look prettier. A very common URL formatting convention is lower case hyphenated, this is how we can an implement it:

routes.MapCodeRoutes(
   rootController: typeof(Controllers.HomeController),
   settings: new CodeRoutingSettings { 
      UseImplicitIdToken = true,
      RouteFormatter = args =>
         Regex.Replace(args.OriginalSegment, @"([a-z])([A-Z])", "$1-$2")
            .ToLowerInvariant()
   }
);

RouteFormatter is a delegate that takes information about a route segment and returns a modified segment. Routes now look like this one:

routes.MapRoute(null, "shopping-cart/{action}/{id}", 
    new { controller = @"ShoppingCart" }, 
    new { action = @"add-to-cart|remove-from-cart", id = @"0|-?[1-9]\d*" }, 
    new[] { "MvcMusicStore.Controllers" });

The beauty of route formatting is that it doesn’t affect URL generation. For instance, Url.Action("AddToCart", "ShoppingCart") continues to work, even though the route uses add-to-cart and not AddToCart. Route formatting is easy.

5. Namespaces

Currently, the StoreManager controller only manages Albums, what about the other entities in our application, like Artist and Genre? Let’s add actions for those too, but adding them to the StoreManager controller would make it grow too much and make the code hard to maintain, so instead let’s create separate controllers in a sub-namespace:

namespace MvcMusicStore.Controllers.StoreManager // <-- Note the sub-namespace
{
   [Authorize(Roles = "Administrator")]
   public class ArtistController : Controller 
   {
      public ActionResult Index() 
      {
         throw new NotImplementedException();
      }

      public ViewResult Details(int id) 
      {
         throw new NotImplementedException();         
      }

      public ViewResult Create() 
      {
         throw new NotImplementedException();
      }
   }

   [Authorize(Roles = "Administrator")]
   public class GenreController : Controller 
   {
      public ActionResult Index() 
      {
         throw new NotImplementedException();
      }

      public ViewResult Details(int id) 
      {
         throw new NotImplementedException();
      }

      public ViewResult Create() 
      {
         throw new NotImplementedException();
      }
   }
}

The routes created for these controllers are:

routes.MapRoute(null, "store-manager/artist/details/{id}", 
    new { controller = @"Artist", action = @"Details" }, 
    new { id = @"0|-?[1-9]\d*" }, 
    new[] { "MvcMusicStore.Controllers.StoreManager" });

routes.MapRoute(null, "store-manager/genre/details/{id}", 
    new { controller = @"Genre", action = @"Details" }, 
    new { id = @"0|-?[1-9]\d*" }, 
    new[] { "MvcMusicStore.Controllers.StoreManager" });

routes.MapRoute(null, "store-manager/artist/{action}", 
    new { controller = @"Artist", action = @"index" }, 
    new { action = @"index|create" }, 
    new[] { "MvcMusicStore.Controllers.StoreManager" });

routes.MapRoute(null, "store-manager/genre/{action}", 
    new { controller = @"Genre", action = @"index" }, 
    new { action = @"index|create" }, 
    new[] { "MvcMusicStore.Controllers.StoreManager" });

Unlike areas, there’s no limit to the depth of namespaces you can use with MvcCodeRouting. There’s also support for namespace-aware views location. For example, views for the MvcMusicStore.Controllers.StoreManager.Artist controller can be located in ~/Views/StoreManager/Artist/.

Conclusions

MvcCodeRouting is an attempt to make you almost completely forget about route management and routing issues, a routing-based URL to code mapping solution. Give it a try!

For more information read the documentation, if you have questions please use the project forum.

Posted by at
Tags: asp.net mvc, mvccoderouting, routing
comments powered by Disqus