21 Jul 2011, 12:08
Revisited: Strongly typed routes for ASP.NET MVCAs I mentioned here, I was a bit unimpressed with the way routes were defined in ASP.NET MVC3. They weren’t type safe, and as such, were prone to many errors. I implemented a solution that addressed that well enough for my particular desires, but it was still (as I mentioned at the end of that article) a bit too wordy for my liking.
Well, that kept getting on my nerves, so I reworked a lot of what I had previously done.
RouteCollectionExtensions.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Web.Mvc;
using System.Reflection;
using System.Web;
using System.Web.Routing;
namespace Sdbarker.Extensions.RouteCollectionExtensions {
public static class RouteCollectionExtensions {
/// <summary>
/// Maps a URL route and sets the default values.
/// </summary>
/// <typeparam name="TController"></typeparam>
/// <param name="action">The method that this route will call on the specified controller.</param>
/// <param name="name">Optional: A string that specifies the name of the route (automatically generated as ControllerMethod if null or empty)</param>
/// <param name="url">Optional: A string that specifies the URL for the route (automatically generated as Controller/Method if null or empty)</param>
/// <param name="defaults">Optional: An object that contains default route values.</param>
/// <param name="constraints">Optional: An object that specifies the contraints of the route.</param>
/// <returns>The generated Route object.</returns>
public static Route MapDefaultRoute(this RouteCollection routes) {
return routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
public static Route MapRoute<TController>(this RouteCollection routes, Expression<Func<TController, ActionResult>> action, string name = null, string url = null, object defaults = null, object constraints = null) where TController : IController {
MethodCallExpression m = (MethodCallExpression)action.Body;
if (m.Method.ReturnType != typeof(ActionResult)) {
throw new ArgumentException("ControllerAction method '" + m.Method.Name + "' does not return type ActionResult");
}
if (string.IsNullOrEmpty(name)) {
name = typeof(TController).Name + m.Method.Name;
}
if (string.IsNullOrEmpty(url)) {
url = string.Format("{0}/{1}", typeof(TController).Name.RemoveLastInstanceOf("Controller"), m.Method.Name);
}
if (defaults == null) {
defaults = new { action = m.Method.Name };
}
else {
defaults = new RouteValueDictionary(defaults);
(defaults as RouteValueDictionary).Add("action", m.Method.Name);
}
return routes.MapRoute<TController>(name, url, defaults, constraints);
}
private static Route MapRoute<TController>(this RouteCollection routes, string name, string url, object defaults, object constraints) where TController : IController {
Route route = new Route(url, new MvcRouteHandler()) {
Defaults = (defaults is RouteValueDictionary) ? (defaults as RouteValueDictionary) : new RouteValueDictionary(defaults),
Constraints = new RouteValueDictionary(constraints)
};
// If a controller property was specified in the defaults, argument exception; someone is doing something that they're not aware of
string controller = typeof(TController).Name.RemoveLastInstanceOf("Controller");
if (route.Defaults.ContainsKey("controller")) {
throw new ArgumentException("Defaults contains key 'controller', but using a strongly typed route.");
// route.Defaults["controller"] = controller;
}
else {
route.Defaults.Add("controller", controller);
}
// Move the original action (which is really a ControllerAction) to the controllerAction key
// and then specify our own action value, otherwise routing will flip out
object action = null;
if (route.Defaults.TryGetValue("action", out action)) {
if (action.GetType().IsGenericType && action.GetType().GetGenericTypeDefinition() == typeof(ControllerAction<>)) {
route.Defaults.Add("controllerAction", route.Defaults["action"]);
route.Defaults["action"] = route.Defaults["controllerAction"].ToString();
}
}
routes.Add(name, route);
return route;
}
private static string RemoveLastInstanceOf(this string text, string remove) {
return text.Remove(text.LastIndexOf(remove));
}
}
public class ControllerAction<TController> where TController : IController {
public string Action { get; private set; }
public override string ToString() {
return Action;
}
public static implicit operator string(ControllerAction<TController> controllerAction) {
return controllerAction.Action;
}
public ControllerAction(Expression<Func<TController, ActionResult>> 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;
}
}
}
This long bit of neatness now lets you declare in a much cleaner syntax. Like so:
routes.MapRoute<HomeController>(controller => controller.Index());
routes.MapRoute<HomeController>(controller => controller.TestPost(new Models.HomeModel()));
routes.MapRoute<HomeController>(controller => controller.WithParams(0, ""), url: "Home/WithParams/{id}/{val}", defaults: new { id = 7, val = "foo" });
routes.MapDefaultRoute();
That’s much, much prettier (to me anyway).