24 Jun 2011, 15:26

Strongly typed routes for ASP.NET MVC

####EDIT: I’ve revisted this here: Revisited: Strongly typed routes for ASP.NET MVC

The other day, a coworker had a problem where a route that I defined for a specific page was returning a 404.  I couldn’t reproduce the problem locally.  As it turns out his project was missing the entire controller class that this route depended on.  As I’m sure you’re well aware, routes are normally defined similarly to this:

routes.MapRoute(
    "BuildVersion",
    "build_version.html",
    new {
        controller    = "BuildVersion",
        action        = "DisplayBuildVersion"
    }
);

This presents a few points of failure, all stemming from the same issue; controllers and actions are specified as strings.  If the classes or methods that are specified here change, you won’t find out until run time, and you’ll only find out in the way of getting a 404 for your requested URL.  To help address this, I created a RouteCollectionExtensions class that provides a way to map routes with strong typing:

RouteCollection.MapRoute<IController>(
    "Route Name",
    "Route URL",
    new {
        action = new ControllerAction<IController>(c => c.ActionResult()).Action
        }
);

As an example:

routes.MapRoute<BuildVersionController>(
    "BuildVersionHtml",
    "build_version.html",
    new {
        action = new ControllerAction<BuildVersionController>(c => c.DisplayBuildVersion()).Action
    }
);

Using this method, if the class for the controller or the method for the action changes, you’ll get a compile time error.  The ControllerAction portion is a little more wordy than I’d like, but it works and is readable.

Sound interesting? Check out the implementation.

The implementation looks like this:

using System;
using System.Linq.Expressions;
using System.Web.Mvc;
using System.Web.Routing;

    public static class RouteCollectionExtensions
    {
        public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults)
            where TController : IController
        {
            return routes.MapRoute(name, url, defaults, null);
        }

        public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints)
            where TController : IController
        {
            Route route = new Route(url, new MvcRouteHandler())
                              {
                                  Defaults = new RouteValueDictionary(defaults),
                                  Constraints = new RouteValueDictionary(constraints)
                              };

            route.Defaults.Add("controller", typeof(TController).Name.RemoveLastInstanceOf("Controller"));
            routes.Add(name, route);
            return route;
        }

        private static string RemoveLastInstanceOf(this string text, string remove)
        {
            return text.Remove(text.LastIndexOf(remove));
        }
    }

    public class ControllerAction where TController : IController
    {
        public string Action { get; private set; }

        public override string ToString()
        {
            return Action;
        }

        public static implicit operator string(ControllerAction controllerAction)
        {
            return controllerAction.Action;
        }

        public ControllerAction(Expression> action)
        {
            MethodCallExpression m = (MethodCallExpression)action.Body;
            if (m.Method.ReturnType != typeof(ActionResult)) {
                throw new ArgumentException("ControllerAction method '" + m.Method.Name + "' does not return type ActionResult");
            }
            Action = m.Method.Name;
        }
    }

I’m sure this could be improved to be much more thorough, but as it stands it accomplishes my immediate goals.