Sunday, September 23, 2012

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.

10 comments:

  1. Hi Max! I'm really excited on using MvcCodeRouting. I'm a newbie on MVC, but 8 yrs in Asp.Net (VB) and learning only based on what I saw from the examples.

    My concerns is where will I put the codes you mention on the examples? I'm a VB.Net user. I found MvcCodeRouting as I browse the web, learning new things to expand my folders.

    In my project, I have ~/Administrator/Registry/Products path. Should I:

    1. Create the folders according to this path?
    2. Do I need to add Controllers & Views folder to the Products folder?
    3. Am I going to add the CustomeRoute like this in the Products controller?




    I'm sorry if I am mixing it up. I just can't get figure it out. A simple basic example could lighten me up. Thanks a lot my friend.

    ReplyDelete
  2. I finally got it. So here's what I did:
    1. Install MvcCodeRouting from NuGet
    2. Register the routes
    3. Created controller inside ~/Administrator/Registry/ called ProductsController.vb
    4. Import MvcCodeRouting in the controller, specified CustomeRoute with the path
    5. Created View, created @Html.ActionLink("Products List","Products","Index") in the _Layout.vbhtml
    6. Run and it works! Now here lies my problem:

    - When I create a view, it goes into ~/Views/Products/Index.vbhtml

    Question: How can I make the view in ~/Views/Administrator/Registry/Products/Index.vbhtml? Is this possible?

    I tried to create the folder and move the Products view folder but it throw me an exception that the controller cannot find the view.

    I tried making it as Embedded Resource... But no luck... Any clue?

    ReplyDelete
    Replies
    1. When you call MapCodeRoutes pass the HomeController type, not ProductsController. If HomeController is in MyApp.Controllers namespace and ProductsController in MyApp.Controllers.Administrator.Registry namespace then it should work, no need for CustomRoute.

      Delete
    2. One detail I forgot, you must call EnableCodeRouting on the ViewEngineCollection, see http://mvccoderouting.codeplex.com/wikipage?title=Getting+Started for more info.

      Delete
  3. FINALLY! I made it working in VB! It's kinda mind breaking because I got used to VB not implementing Namespace but got it figured out since I remember, the MvcCodeRouting is written in C# (I guess that's the reason).

    So heres what I did.

    1. I opened up my Microsoft Visual Studio 2010 Ultimatum and created a new MVC 4 project in VB named MvcCodeRoutingProject, selected Internet Application with Razor, then click OK.

    2. I used NuGet and install the MvcCodeRouting package. This automatically configures the Web.Config and insert all the verbs and handlers.

    3. In the ~/App_Start/RouteConfig.vb, I registered the "routes.MapCodeRoutes" as stated in the 1. Convention over configuration of this article.

    4. In ~/Global.asax.vb, I registered the ViewEngines.Engines.EnableCodeRouting() inside the Sub Application_Start() procedure.

    5. I created the ~/Controllers/Administrator/Registry/ProductsController.vb, imported MvcCodeRouting class, declared Namespace Administrator.Registry, and created two (2) ActionResult: Index() and Details( pitems As String).

    6. I created the ~/Views/Administrator/Registry/Products/ folder and created two(2) files: Details.vbhtml & Index.vbhtml

    7. I modified the ~/Shared/_Layout.vbhtml file and added Html.ActionLink("Products", "Index", "~Administrator.Registry.Products") along with the menu, and modified all the ActionLinks pointing to Home Controller to "~Home". I found out that the tilde (~) character does the trick in jumping to other controllers, as stated in the Links and Controller Reference Syntax from the documentation of MvcCodeRouting.

    8. I also modified the ~/Shared/_LoginPartial.vbhtml partial view file's ActionLinks from "Account" to "~Account" (I added the tilde character) because it is rendered inside the _Layout.vbhtml which is responsible from jumping to their own respective controllers.

    9. I made sure that my .\SQLExpress is running and then I start debugging. Everything works!

    Key points to remember (from my points of view):

    - We need to define the namespace, excluding the project namespace. Only this: Namespace Administrator.Registry and not this: Namespace MvcCodeRoutingProject.Controllers.Administrator.Registry due to the routes I found when you visit /routes.axd from the browser's URL. It automatically adds the namespace. Adding it again will duplicate it: MvcCodeRoutingProject.MvcCodeRoutingProject.* and the routing will not work.

    - Always use the tilde (~) character on all the ActionLinks that points outside the currently displayed controller, so it will jump to the link, not inside the same controller.

    So there you go. I hope this could help my fellow VB.Net developers. You can download the working sample project from this link: http://abecomm.com/ais/uls/MvcCodeRoutingProject.zip

    Thank you Max for this wonderful module. I haven't implemented MvcCodeRouting but it rocks!

    ReplyDelete
  4. I tried to move my assemblies with embedded views into a subfolder of bin, like bin\Parts, but when I tried to navigate, I got not found. As soon as I moved the assemblies back to bin, it worked again.

    Do I need to tell the asp.net runtime to look in the subfolder somehow?

    ReplyDelete
    Replies
    1. You could try probing. MvcCodeRouting does not change how assemblies are loaded. Subfolders are usually used for localized resources.

      Delete
  5. Is there a way to apply a route format to ALL routes?

    Like the Pascal Hyphen example but rather than just the Home Controller ALL routes?

    ReplyDelete
    Replies
    1. It does apply it to all routes. In the example above HomeController is used as the root controller, the library also creates routes for other controllers in the same namespace or any sub-namespace, in the same assembly.

      Delete