Type safe route registration in ASP.NET MVC

October 18, 2009 17:25

A few days ago my workmate Ian made a point that there should be a nice way to implement type safe route registration in ASP.NET MVC.

Type safe! Can anyone read these words without experiencing a tug of excitement, without being swayed by the view of departed bugs and newly arrived refactorings on controllers and actions - refactorings that don't break your app?

Indeed, type safe is possible. Here's how we register our routes now:

routes.MapRoute(
    "viewProduct",                                                        
    "{locale}/{product}/view/{mode}/",                                 
    new { controller = "Product", action = "Display", mode = "full" }  
);
 
and here's a type safe way:

routes.MapRoute(
    "{locale}/{product}/view/{mode}/")
     .On<ProductController>(x => x.Display("full"));

Fluent language for route registration.

But we can go even further. With a bit of expression trees and extension methods we can build a small language that would allow much more complicated registrations. Like this one:

routes.MapRoute("{locale}/{product}/view/{mode}/")
      .On<ProductController>(x => x.Display(Detalization.Full))
      .Default("locale=be-BY", "product=all")
      .Constrain("locale", To.Regex("[a-z]{2}-[A-Z]{2}"))
      .Constrain("mode", To.Enum<Detalization>());

So let's look how it's implemented inside. Here's the code for the fluent interface:


public static class MyExtensions
{
    public static MyRoute MapRoute(this RouteCollection routes, string url)
    {
        var myRoute = new MyRoute(url);
        routes.Add(myRoute.Route);
        return myRoute;
    }
 
    public static MyRoute On<T>(this MyRoute myRoute, Expression<Action<T>> action)
        where T : Controller
    {
        return myRoute.SetActionDefaults(action);
    }
 
    public static MyRoute Default(this MyRoute myRoute, params string[] param)
    {
        return myRoute.SetDefaults(param);
    }
 
    public static MyRoute Constrain(this MyRoute myRoute, string param, IRouteConstraint constraint)
    {
        return myRoute.SetConstraint(param, constraint);
    }
}


It relies on a bit more verbose MyRoute class:


public class MyRoute
{
    public Route Route { get; private set; }
 
    public MyRoute(string url)
    {
        Route = new Route(url, new RouteValueDictionary(), new MvcRouteHandler());
    }
 
    internal MyRoute SetActionDefaults<T>(Expression<Action<T>> action)
    {
        Route.Defaults = ParseDefaults(action.Body as MethodCallExpression);
        return this;
    }
 
    internal MyRoute SetDefaults(params string[] param)
    {
        foreach (var pair in ParseParameters(param)) {
            Route.Defaults.Add(pair.Key, pair.Value);
        }
        return this;
    }
 
    internal MyRoute SetConstraint(string param, IRouteConstraint constraint)
    {
        var constraints = Route.Constraints ?? new RouteValueDictionary();
        constraints.Add(param, constraint);
 
        Route.Constraints = constraints;
        return this;
    }
 
    //private ParseDefaults and ParseParameters omitted for brevity 
}

As for class To, it's a simple container with static properties like Regex or Enum which return custom route constraints.

Finishing off.

So - what do you think? Give it a try - download the code, play with it, and let me know if you can think of any pros, cons and improvements.

I also want to mention that while working on this type safe registration thing, I stumbled upon two amazing blog posts from Gino Heyman. He doesn't use fluent interfaces but our implementations of type safe routes are fairly similar. Gino was the first one - check out his ideas:


kick it on DotNetKicks.com

Comments

Comments are closed