ASP.NET MVC: Fun With Routes

For the purposes of this post, let’s say that we are building a community blogging site.  We want multiple users to be able to maintain separate blogs on the site.  We have an area in our MVC application that is called Blogs.  We have a controller in the Blogs area called BlogController and this controller will return views that display the blog posts.

Our URL path to a blog post could be something like this: /Blogs/Blog/ViewPost/jeremiah.clark/2011/01/15/SomePostTitle
The route for this path would be:

    1: routes.MapRoute(
    2:     "Blogs_display", 
    3:     "Blogs/Blog/ViewPost/{username}/{year}/{month}/{day}/{postTitle}"
    4: );

And our BlogController would look something like this:

    1: public class BlogController : Controller
    2: {
    3:     private readonly IBlogRepository _blogRepository;
    4:  
    5:     public BlogController(IBlogRepository blogRepository)
    6:     {
    7:         this._blogRepository = blogRepository;
    8:     }
    9:  
   10:     public ActionResult ViewPost(string username, int year, 
   11:         int month, int day, string postTitle)
   12:     {
   13:         // validate arguments
   14:         ...
   15:  
   16:         var blogModel = blogRepository.GetPost(username, year, 
   17:             month, day, postTitle);
   18:  
   19:         return View(blogModel);
   20:     }
   21: }

Instead of this, we want the path to the post to be:
/Blogs/jeremiah.clark/2011/01/15/SomePostTitle
The difference is that we don’t want to specify a controller or action method in the path, but we still want to use the common BlogController to handle the requests.  It also makes for a cleaner and more readable URL for our blog posts.

Our new route looks like this:

    1: routes.MapRoute(
    2:     "Blogs_display",
    3:     "Blogs/{username}/{action}/{year}/{month}/{day}/{postTitle}",
    4:     new { controller = "Blog", action = "ViewBlog" }
    5: );

And our new BlogController looks something like this:

    1: public class BlogController : Controller
    2: {
    3:     private readonly IBlogRepository _blogRepository;
    4:     private string _blogOwner;
    5:  
    6:     public BlogController(IBlogRepository blogRepository)
    7:     {
    8:         this._blogRepository = blogRepository;
    9:     }
   10:  
   11:     protected override void Initialize(RequestContext requestContext)
   12:     {
   13:         base.Initialize(requestContext);
   14:         this.blogOwner = this.ControllerContext.RouteData
   15:             .Values["username"].ToString();
   16:     }
   17:  
   18:     public ActionResult ViewPost(int year, int month, 
   19:         int day, string postTitle)
   20:     {
   21:         // validate arguments
   22:         ...
   23:  
   24:         var blogModel = blogRepository.GetPost(this.BlogOwner, year, 
   25:             month, day, postTitle);
   26:  
   27:         return View(blogModel);
   28:     }
   29: }

So what is happening with this new route?

The controller and the action are being set to Blog and ViewBlog for every matching request.  Every route that matches will be sent to the BlogController and it will invoke the ViewBlog action method. 

This solves part of our problem, but not the entire thing. 

  • We still need to validate that the username part of a path is a valid user.
  • This breaks the route to any other controllers that exist in the Blogs area.
  • We don’t want a path available to the BlogController, it must specify a username.

Here is a good spot to use an IRouteConstraint.  We can create a class that implements this interface and apply it to the username route value.  If the constraint passes, then we have a match and a valid user.  If not, then the route does not match and the routing system searches for a different matching route.  Here is an implementation of IRouteConstraint that checks to see if the supplied username value exists in our repository:

    1: public class BlogOwnerRouteConstraint : IRouteConstraint
    2: {
    3:     private readonly IUserRepository userRepository;
    4:  
    5:     public BlogOwnerRouteConstraint(IUserRepository userRepository)
    6:     {
    7:         this.userRepository = userRepository;
    8:     }
    9:  
   10:     public bool Match(HttpContextBase httpContext, Route route, 
   11:         string parameterName, RouteValueDictionary values, 
   12:         RouteDirection routeDirection)
   13:     {
   14:         string username = values["username"].ToString();
   15:         return (userRepository.Exists(username));
   16:     }
   17: }

Now we can update our route to use the constraint.  We can also add additional routes to fix our other two problems that were listed above.  Here are the new routes:

    1: // This will block any direct access to the BlogController
    2: routes.IgnoreRoute("Blogs/Blog/{*pathInfo}");
    3:  
    4: // This provides our dynamic routing based on username
    5: routes.MapRoute(
    6:     "Blogs_display",
    7:     "Blogs/{username}/{year}/{month}/{day}/{postTitle}",
    8:     new { controller = "Blog", action = "ViewBlog" },
    9:     new { username = new BlogOwnerRouteConstraint(
   10:             DependencyResolver.Current.GetService<IUserRepository>()
   11:         ) }
   12: );
   13:  
   14: // This provides the default route for any path that does not match above
   15: routes.MapRoute(
   16:     "Blogs_default",
   17:     "Blogs/{controller}/{action}/{id}",
   18:     new { controller = "Home", action = "Index", id = UrlParameter.Optional }
   19: );

This example just shows some of the cool things that can be done with ASP.NET Routing.  For more on ASP.NET Routing, check out the documentation on MSDN: https://msdn.microsoft.com/en-us/library/cc668201.aspx.