Integrated Solutions, Inc.

MVC Routes and Magic Strings == “No Good”

I live and breathe with ASP.NET MVC, but from the very beginning I’ve been very uncomfortable with the magic string nature of routes, controllers, and actions. I long ago stopped using the various “action” extensions because they are way too prone to fat finger screw ups and moved nearly 100% to using route extensions and putting my routes names into constants so as not to have any chance of fat finger issues, but recently, after reviewing the source code for MVC2, specifically the “LabelFor” extension, I realized there was even another way to limit fat finger screw ups via routing extensions and lambdas.

Note: After some remarks from readers, I have modified the original post

a. Add extensions for enabling the new MapRoute extension

public static class RouteCollectionExtensions
{
  public static System.Web.Routing.Route MapRoute<TController>(this System.Web.Routing.RouteCollection routes, string name, string url, Expression<Func<TController, System.Web.Mvc.ActionResult>> action) where TController : System.Web.Mvc.Controller
  {
    return routes.MapRoute<TController>(name, url, action, (object)null /* defaults */);
  }

  public static System.Web.Routing.Route MapRoute<TController>(this System.Web.Routing.RouteCollection routes, string name, string url, Expression<Func<TController, System.Web.Mvc.ActionResult>> action, object defaults) where TController : System.Web.Mvc.Controller
  {
    return routes.MapRoute<TController>(name, url, action, defaults, (object)null /* constraints */);
  }

  public static System.Web.Routing.Route MapRoute<TController>(this System.Web.Routing.RouteCollection routes, string name, string url, Expression<Func<TController, System.Web.Mvc.ActionResult>> action, object defaults, object constraints) where TController : System.Web.Mvc.Controller
  {
    if (routes == null) throw new ArgumentNullException("routes");
    if (url == null) throw new ArgumentNullException("url");

    System.Web.Routing.RouteValueDictionary defaultValues = new System.Web.Routing.RouteValueDictionary(defaults);

    Type type = typeof(TController);

    #region controllerName
    string controllerName = type.Name;

    if (controllerName.EndsWith("Controller", StringComparison.InvariantCultureIgnoreCase)) controllerName = controllerName.Substring(0, controllerName.Length - "Controller".Length);

    defaultValues["controller"] = controllerName;
    #endregion

    #region actionName
    System.Reflection.MethodInfo methodInfo = ((MethodCallExpression)action.Body).Method;

    string actionName = methodInfo.Name;

    ActionNameAttribute[] actionNameAttributes = (ActionNameAttribute[])methodInfo.GetCustomAttributes(typeof(ActionNameAttribute), false);

    if ((actionNameAttributes != null) && (actionNameAttributes.Length > 0)) actionName = actionNameAttributes[0].Name;

    defaultValues["action"] = actionName;
    #endregion

    System.Web.Routing.Route route = new System.Web.Routing.Route(url, new MvcRouteHandler())
    {
      Defaults = defaultValues,
      Constraints = new System.Web.Routing.RouteValueDictionary(constraints),
      DataTokens = new System.Web.Routing.RouteValueDictionary()
    };

    #region controllerNamespace
    string controllerNamespace = type.FullName;

    controllerNamespace = controllerNamespace.Substring(0, controllerNamespace.Length - (type.Name.Length + 1));

    route.DataTokens["Namespaces"] = new string[] { controllerNamespace };
    #endregion

    routes.Add(name, route);

    return route;
  }
}

b. Create a routes class:

public partial class Routes
{
  public static void RegisterRoutes(RouteCollection routes)
  {
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    Account.RegisterRoutes(routes);
    Home.RegisterRoutes(routes);
  }
}

c. Create a routes class for the HomeController

public partial class Routes
{
  public partial class Home
  {
    public class RouteNames
    {
      public static readonly string Index = Guid.NewGuid().ToString("D");
      public static readonly string About = Guid.NewGuid().ToString("D");
    }

    protected static readonly string URLRoot = "";

    public static void RegisterRoutes(RouteCollection routes)
    {
      routes.MapRoute<Controllers.HomeController>(RouteNames.About, URLRoot + "About", controller => controller.About());
      routes.MapRoute<Controllers.HomeController>(RouteNames.Index, URLRoot, controller => controller.Index());
    }
  }
}

Note: The route names are totally random because they are just meant to be unique keys pointing to specific routes

d. Create a routes class for the AccountController

public partial class Routes
{
  public partial class Account
  {
    public class RouteNames
    {
      public static readonly string LogOn = Guid.NewGuid().ToString("D");
      public static readonly string LogOff = Guid.NewGuid().ToString("D");
      public static readonly string Register = Guid.NewGuid().ToString("D");
      public static readonly string ChangePassword = Guid.NewGuid().ToString("D");
      public static readonly string ChangePasswordSuccess = Guid.NewGuid().ToString("D");
    }

    protected static readonly string URLRoot = "Account";

    public static void RegisterRoutes(RouteCollection routes)
    {
      routes.MapRoute<Controllers.AccountController>(RouteNames.LogOn, URLRoot + "LogOn", c => c.LogOn());
      routes.MapRoute<Controllers.AccountController>(RouteNames.LogOff, URLRoot + "LogOff", c => c.LogOff());
      routes.MapRoute<Controllers.AccountController>(RouteNames.Register, URLRoot + "Register", c => c.Register());
      routes.MapRoute<Controllers.AccountController>(RouteNames.ChangePassword, URLRoot + "ChangePassword", c => c.ChangePassword());
      routes.MapRoute<Controllers.AccountController>(RouteNames.ChangePasswordSuccess, URLRoot + "ChangePasswordSuccess", c => c.ChangePasswordSuccess());
    }
  }
}

e. Replace the register routes in Global.asax.cs:

public class MvcApplication : System.Web.HttpApplication
{
  protected void Application_Start()
  {
    AreaRegistration.RegisterAllAreas();

    Routes.RegisterRoutes(RouteTable.Routes);
  }
}

f. Change all usages of “Action” extensions to “Route” extensions
    In LogOnUserControl.ascx change:
       Html.ActionLink("Log Off", "LogOff", "Account")
    To
      Html.RouteLink("Log Off", Routes.Account.RouteNames.LogOff)

    In AccountController.cs change:
       return RedirectToAction("Index", "Home");
    To
       return RedirectToRoute(Routes.Home.RouteNames.Index);

Published 03/01/2010 by Ron Muth