MVC Routing fun

by MikeHogg 22. October 2012 10:08

I had a very small landing page project request for implementing a few thousand personalized URLs.  From what I knew about MVC routing, I figured it would be an easy task, and it was, but also a nice sample of the power of the new routing engine.

 

public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
            routes.Add("purls", new CustomRoute(
                "{id}", 
                new RouteValueDictionary(new { controller = "Home", action = "getquerystring" }),  // the default will send it to get querystring, and the override GetRouteData will then send it to purl
                new { id = new ActionNameConstraint()} // only if Action not exist
                ));   
            
            routes.MapRoute(
                "NoHome", // set default controller to Home so no /Home/ in url
                "{action}",  
                new { controller = "Home", action = "Index" } 
            );
        }

 

This has two extensible features of the MVC routing system- a custom route, and a custom constraint class, rather than just a constraint object.   The custom route is to prepare the purl action, and the constraint is to skip to the next route if an action name already exists for that route (static route).  I wanted to load an array of action names on Application_Startup, but apparently you can no longer access RequestContext that soon in the lifecycle, so I just implemented a lazy readonly Getter for an Application variable like so:

 

public class GLOBAL
    {
     public static string[] HomeActions
        {
            get
            {
                if (_homeactions == null)
                {
                    var controller = ControllerBuilder.Current.GetControllerFactory().CreateController(HttpContext.Current.Request.RequestContext, "Home");
                    var controllerType = controller.GetType();
                    var controllerDescriptor = new ReflectedControllerDescriptor(controllerType);
                    _homeactions = controllerDescriptor.GetCanonicalActions().Select(a => a.ActionName).ToArray();
                }
                return _homeactions;
            }
        }
 

 

And then, my constraint just needs to check against the array-

 

 

 
public class ActionNameConstraint : IRouteConstraint
    {
        public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
        {
            if (values.ContainsKey(parameterName))
            {
                string stringValue = values[parameterName] as string;
                return !GLOBAL.HomeActions.Contains(stringValue);
            }
            return false;
        }
    }
 

 

And, if it passes the constraint, our first stop is an action called GetQueryString, to append some utm= keyvalue  pairs for our google analytics, which then redirects to the purl lookup action...

 

 
public ActionResult GetQueryString(string id)
        { 
            Dictionary<string,object> d = new Dictionary<string,object>();
            GLOBAL.QUERYSTRINGOBJECT.CopyTo(d);
            return RedirectToAction(id, new RouteValueDictionary(d));
            
        }
...
public class GLOBAL
    {
        public static System.Collections.Specialized.NameValueCollection QUERYSTRINGOBJECT = HttpUtility.ParseQueryString("utm_source=DM&utm_medium=something&utm_campaign=something"); 

... and that's where our Custom route comes in, simply to check for existance of a querystring, and if so, then changes the action to "purl"...

 

 public class CustomRoute : Route
    {
        public CustomRoute(string url, RouteValueDictionary routevalues, object constraints) : base(url, routevalues, new RouteValueDictionary( constraints ), new MvcRouteHandler()) {}
 
        public override RouteData GetRouteData(HttpContextBase httpContext)
        { 
            RouteData routeData = base.GetRouteData(httpContext);
            if (routeData == null) return null;
             
            if (httpContext.Request.QueryString.Keys.Count > 0) routeData.Values["action"] = "purl";
            return routeData;
        }         
    } 

 

The "purl" action is going to do the lookup against 10k urls for this user's viewname, all the while keeping the url that the user typed in their address bar intact, with the addition of the GA querystring.

 

 

 
public ActionResult Purl(string id)  
        {
            Models.PModel pmodel = lib.Repo.GetPModel(id);
 
            if (String.IsNullOrEmpty(pmodel.ViewName)) return RedirectToAction("NotFound");
 
            return View(pmodel.ViewName, pmodel);
        }
 

 

 

And that's it.

Tags:

MVC

About Mike Hogg

Mike Hogg is a c# developer in Brooklyn.

More Here

Favorite Books

This book had the most influence on my coding style. It drastically changed the way I write code and turned me on to test driven development even if I don't always use it. It made me write clearer, functional-style code using more principles such as DRY, encapsulation, single responsibility, and more. amazon.com

This book opened my eyes to a methodical and systematic approach to upgrading legacy codebases step by step. Incrementally transforming code blocks into testable code before making improvements. amazon.com

More Here