21 Jul 2011, 12:08

Revisited: Strongly typed routes for ASP.NET MVC

As 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).

comments powered by Disqus