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… ). 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!