Sunday, February 1, 2009

ASP.NET MVC strong typed routing

One of the more questionable practices in the ASP.NET MVC framework is that all routing to controllers (and views) is based on names; plain text strings. I'm assuming this is because the MVC framework is heavily influenced by existing MVC frameworks like Ruby on Rails, but I'm not so sure whether it's a good idea.

One of the first things I learned back in the days when I got started in software development is never to rely on strings; use strong typing wherever possible because the compiler can check types. It can't check strings.

ASP.NET MVC seems to violate this rule and instead promotes unit testing to validate routing decisions. While I'm convinced that unit testing is a good thing, I'm not at all comfortable with writing a unit test for something that a compiler can check. For example, I have a controller class named My.VeryCoolController. If I want to route requests to that controller I could set that up like this:
RouteTable.Routes.MapRoute( "coolstuff", "docoolstuff.aspx", new { controller = "VeryCool", action ="HelloWorld" } );
So the controller parameter refers to the VeryCoolController class by it’s name. Where I come from this is a very bad practice! In fact, pretty much all of the coding standards I've ever seen will explicitly forbid this. So, why not do this:
RouteTable.Routes.mapRoute<my.verycoolcontroller>( "coolstuff", "docoolstuff.aspx", new { action = "helloworld" );

In my opinion this is much more robust; the code won't even compile if I break it somehow.

Since the ASP.NET MVC framework is completely setup to work with names in stead of type I've created some classes that do support routing based on types. This requires the following functionality:

  1. Extension methods to define a route using the controller’s type
  2. A route handler that can spawn a strong typed HttpHandler for the controller
  3. An HttpHandler that invokes the specified controller
An extension method for mapping a route using a controller type would look like this :
public static Route MapRoute<T>( this RouteCollection routes, string name, string url, object defaults, object constraints ) 
  where T : Controller, new() 
     Route route = new Route( url, new MvcRouteHandler<t>() ) 
         Defaults = new RouteValueDictionary( defaults ), 
         Constraints = new RouteValueDictionary( constraints ) 

     // For compatibility with MvcHandler 
     route.Defaults.Add( "controller", typeof( T ).Name.Replace( "Controller", "" ) ); 
     routes.Add( name, route ); 
     return route; 

Note that all error handling has been removed from this snippet. Nothing spectacular here; this is pretty much what the stock extension methods shipped with ASP.NET MVC do. One important thing to note here is that setting a value for controller is required; otherwise this like RedirectToRouteResult will not work as you'd expect. Next up, implementing a custom route handler. Not too dificult either:
internal class MvcRouteHandler<TController> : System.Web.Routing.IRouteHandler 
  where TController : System.Web.Mvc.Controller, new() 
    public IHttpHandler GetHttpHandler( RequestContext requestContext ) 
      return new MvcHttpHandler( 
        new TController(), 
	requestContext ); 
The code above instantiates a new controller of the required type and hands it over to an MvcHttpHandler which we'll create next. Before we do though, note that creating the controller like this requires it to have default (parameterless) constructor. If you need some kind of dependency injection you could delegate controller creation to an Inversion-of-Control container. Finally, the MvcHttpHandler mentioned above:
internal class MvcHttpHandler : IHttpHandler, 
  public MvcHttpHandler( System.Web.Mvc.IController controller, System.Web.Routing.RequestContext context ) 
    _controller = controller; 
    _context = context; 

  public bool IsReusable 
    get { return false; } 

  public void ProcessRequest( HttpContext context ) 
    _controller.Execute( _context ); 

  System.Web.Mvc.IController _controller; 
  System.Web.Routing.RequestContext _context; 

A couple of things to point out here. First off, to support session state the handler must inherit from IRequiresSessionState. Secondly, notice that the Execute method does not use the HttpContext instance passed into the ProcessRequest method. Instead the RequestContext required by the controller is passed in to the constructor by the route handler and stored as a field. This also means that IsReusable must return false; the handler has state specific to a request and is therefore not reusable.

And then what?
That's it. The controller has all the mechanisms needed to dynamically invoke the method associated with the request. All of MVC's routing magic will work with this setup. I'll post a zip with the full code shortly.

Sources are available as part of the Alanta.Mvc project.


Karel Frajták said...

Nice article, I have one comment - you don't have to use Activator.CreateInstance to create new instance of your controller. You can simply call new TColntroller() since Controller class itself has a parameterless constructor.

Marnix said...

Thanks for the feedback Karel. I've updated the source code in the post as well as on GitHub.