Strongly Typed Routes for ASP.NET MVC

OK, I know there are already a number of these, but since I didn’t actually know any, I wrote my own library.

I was trying to achieve something like this in a simple way:

   1: RouteTable.Routes
   2:     .ActionWithDefaultParameters<BlogController>(x => x.PostsByDate(DateTime.Today.Year, DateTime.Today.Month, DateTime.Today.Day))
   3:     .Map();

What we have is:

  • The controller is specified as a template parameter;
  • The action method is obtained from a strongly typed expression;
  • The default values for the action method parameters are specified in the expression.

I knew I had to start from the RouteCollection class, so I wrote a couple of extension methods:

   1: public static class RouteCollectionExtensions
   2: {
   3:     public static _Route Action<TController>(this RouteCollection routes, Expression<Func<TController, Object>> action, String url = "") where TController : IController, new()
   4:     {
   5:         MethodCallExpression method = action.Body as MethodCallExpression;
   6:  
   7:         _Route route = new _Route(routes, typeof(TController), method.Method);
   8:         route.SetUrl(url);
   9:  
  10:         return (route);
  11:     }
  12:  
  13:     public static _Route ActionWithDefaultParameters<TController>(this RouteCollection routes, Expression<Func<TController, Object>> action, String url = "") where TController : IController, new()
  14:     {
  15:         _Route route = Action<TController>(routes, action, url);
  16:         MethodCallExpression method = action.Body as MethodCallExpression;
  17:         ParameterInfo [] parameters = method.Method.GetParameters();
  18:         Expression [] arguments = method.Arguments.ToArray();
  19:  
  20:         for (Int32 i = 0; i < parameters.Length; ++i)
  21:         {
  22:             if (arguments[i] is ConstantExpression)
  23:             {
  24:                 route.AddDefaultParameterValue(parameters[i].Name, (arguments[i] as ConstantExpression).Value);
  25:             }
  26:             else if (arguments[i] is MemberExpression)
  27:             {
  28:                 route.AddDefaultParameterValue(parameters[i].Name, Expression.Lambda(arguments[i]).Compile().DynamicInvoke());
  29:             }
  30:         }
  31:  
  32:         return (route);
  33:     }
  34: }

The Action method lets you create a _Route instance with no default parameter values, while the ActionWithDefaultParameters will take any parameters in the expression (constants and simple properties only) and set them as defaults.

Next is the _Route class (sorry, I couldn’t think of a better name… Winking smile). This class implements a fluent interface for configuring the routes:

   1: public sealed class _Route
   2: {
   3:     private readonly RouteCollection routes;
   4:  
   5:     public _Route(RouteCollection routes, Type controllerType, MethodInfo actionMethod)
   6:     {
   7:         this.routes = routes;
   8:         this.ControllerType = controllerType;
   9:         this.Action = actionMethod;
  10:         this.Defaults = new RouteValueDictionary();
  11:         this.Constraints = new RouteValueDictionary();
  12:         this.Name = String.Concat(controllerType.Name.Remove(controllerType.Name.LastIndexOf("Controller")), "_", actionMethod.Name);
  13:         this.Url = String.Join("/", actionMethod.GetParameters().Select(x => String.Concat("{", x.Name, "}")));
  14:     }
  15:  
  16:     public String Url
  17:     {
  18:         get;
  19:         private set;
  20:     }
  21:  
  22:     public String Name
  23:     {
  24:         get;
  25:         private set;
  26:     }
  27:  
  28:     public RouteValueDictionary Defaults
  29:     {
  30:         get;
  31:         private set;
  32:     }
  33:  
  34:     public RouteValueDictionary Constraints
  35:     {
  36:         get;
  37:         private set;
  38:     }
  39:  
  40:     public Type ControllerType
  41:     {
  42:         get;
  43:         private set;
  44:     }
  45:  
  46:     public MethodInfo Action
  47:     {
  48:         get;
  49:         private set;
  50:     }
  51:  
  52:     public _Route SetUrl(String url)
  53:     {
  54:         if (String.IsNullOrWhiteSpace(url) == false)
  55:         {
  56:             this.Url = url;
  57:         }
  58:  
  59:         return (this);
  60:     }
  61:  
  62:     public _Route SetName(String name)
  63:     {
  64:         this.Name = name;
  65:  
  66:         return (this);
  67:     }
  68:  
  69:     public _Route AddDefaultParameterValue(String parameterName, Object value)
  70:     {
  71:         this.Defaults[parameterName] = value;
  72:  
  73:         return (this);
  74:     }
  75:  
  76:     public _Route SetOptionalParameter(String parameterName)
  77:     {
  78:         this.Defaults[parameterName] = UrlParameter.Optional;
  79:  
  80:         return (this);
  81:     }
  82:  
  83:     public void Map()
  84:     {
  85:         String url = String.Concat(this.ControllerType.Name.Remove(this.ControllerType.Name.LastIndexOf("Controller")), "/", this.Url);            
  86:         Route route = this.routes.MapRoute(this.Name, url, null, null);
  87:  
  88:         foreach (KeyValuePair<String, Object> @default in this.Defaults)
  89:         {
  90:             route.Defaults[@default.Key] = @default.Value;
  91:         }
  92:  
  93:         foreach (KeyValuePair<String, Object> constraint in this.Constraints)
  94:         {
  95:             route.Constraints[constraint.Key] = constraint.Value;
  96:         }
  97:  
  98:         route.Defaults["Controller"] = this.ControllerType.Name.Remove(this.ControllerType.Name.LastIndexOf("Controller"));
  99:         route.Defaults["Action"] = this.Action.Name;
 100:     }
 101:  
 102:     /*
 103:     
 104:     */
 105:  
 106:     public _Route AddConstraints(Object constraints)
 107:     {
 108:         PropertyDescriptorCollection props = TypeDescriptor.GetProperties(constraints);
 109:  
 110:         foreach (PropertyDescriptor prop in props)
 111:         {
 112:             this.AddDefaultParameterValue(prop.Name, prop.GetValue(constraints));
 113:         }
 114:  
 115:         return (this);
 116:     }
 117:  
 118:     public _Route AddConstraint<TRouteConstraing>() where TRouteConstraing : IRouteConstraint, new()
 119:     {
 120:         this.Constraints[Guid.NewGuid().ToString()] = new TRouteConstraing();
 121:  
 122:         return (this);
 123:     }
 124:  
 125:     public _Route AddConstraint(IRouteConstraint constraint)
 126:     {
 127:         this.Constraints[Guid.NewGuid().ToString()] = constraint;
 128:  
 129:         return (this);
 130:     }
 131:  
 132:     public _Route AddConstraint(String parameterName, String regularExpression)
 133:     {
 134:         this.Constraints[parameterName] = regularExpression;
 135:  
 136:         return (this);
 137:     }
 138:  
 139:     public _Route AddConstraint(String parameterName, IRouteConstraint constraint)
 140:     {
 141:         this.Constraints[parameterName] = constraint;
 142:  
 143:         return (this);
 144:     }
 145: }

It has the following methods:

  • AddConstraint: for adding route constraints (several overloads);
  • AddDefaultParameterValue/SetOptionalParameter: for setting default parameter values;
  • SetName: for setting a name for the route;
  • SetUrl: for setting the URL;
  • Map: for actually registering the route.

SetName and SetUrl are optional, because it will try to create a URL from the action method parameters and will name the route like Controller_Action.

Some more usages:

   1: RouteTable.Routes
   2:     .ActionWithDefaultParameters<BlogController>(x => x.GetPostsForDate(DateTime.Today.Year, DateTime.Today.Month, DateTime.Today.Day))
   3:     .AddConstraint<DateRouteConstraint>()
   4:     .Map();
   5:  
   6: RouteTable.Routes
   7:     .Action<AnotherController>(x => x.SomeAction(null))
   8:     .SetOptionalParameter("someParameter")
   9:     .Map();
  10:  
  11: RouteTable.Routes
  12:     .Action<HomeController>(x => x.Index())
  13:     .SetName("Default")
  14:     .Map();

Hope you enjoy!

                             

No Comments