Hanalei, Hawaii

Routing is probably the most confusing aspect of working with ASP.NET MVC. It’s hard to craft a groovy URL - even harder to link properly off to that groovy URL. Rails leans on Ruby’s forgiving and friendly nature to make this a bit more simple - C#4 allows to get close to this as well.

With Rails 3  you define a route in your config/routes.rb like this:

  match "order/receipt/:id" => "orders#receipt", :as => :receipt # receipt_url

You can access this route anywhere in your application using convention - simply reference the name of the route and append on “url” or “path” - depending what you want. “_path” returns the relative URL, “_url” gives you an absolute url:

 link_to "Your Receipt", receipt_path

Ruby can do this because you can handle the “method_missing” error thrown when an object doesn’t have the a method declared that you’re looking for.

In this case, Rails catches method_missing, parses the method call name (“root_url”) and figures out that you want the route named “root” and you want it to be absolute - pretty neat.

Contrast this with referencing a named route in C#/MVC:

  @Html.RouteLink("Home", "Root");

It’s not bad - it’s just a bit noisier than is needed. Moreover you can’t tell it to make the URL absolute, something that bugs me.

Most people just use “ActionLink” - sending in the action and controller name for their link (did *you* know there was a RouteLink() method?).

It’s far easier to name your routes in case you move actions/methods around (which you will as you refactor). Nailing actions/controller names to your view pages (and in your controllers as you redirect) can be a refactoring nightmare - using named routes helps with this.

Also - a bit of a nit - but there’s 11 overloads on this method and it’s really easy to trip yourself with them. 

Enough of that - let’s make this a bit more terse!

Dynamics! Hurrah!

It’s rather simple to create a dynamic route builder for ourselves - overriding “TryInvokeMember” and “TryGetMember”:

    public class DynamicRoute:DynamicObject {
        UrlHelper _helper;
        public DynamicRoute(UrlHelper helper) {
            _helper = helper;
        }
        public override bool TryGetMember(GetMemberBinder binder, out object result) {
            result = GetUrl(binder.Name, new object[0]);
            return true;
        }
        public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) {
            
            result = GetUrl(binder.Name,args);

            return true;

        }

        private object GetUrl(string methodName, object[] args) {
            object result;
            //split it for name of route and path/url
            var stems = methodName.Split('_');

            //look up the route by name
            var routeName = stems[0];
            var url = "";
            if (args.Length > 0) {
                //pass in the first argument...
                if (args[0].GetType() == typeof(string) || args[0].GetType().IsPrimitive) {
                    url = _helper.RouteUrl(routeName, new { id = args[0] });
                } else {
                    url = _helper.RouteUrl(routeName, args[0]);
                }
            } else {
                url = _helper.RouteUrl(routeName);
            }
            //url or path?
            if (stems.Last() == "url") {
                url = Root(false) + url;
            }

            //craft up the URL
            result = url;
            return result;
        }
        public string Root(bool includeAppPath = true) {
            var context = _helper.RequestContext.HttpContext;
            var Port = context.Request.ServerVariables["SERVER_PORT"];
            if (Port == null || Port == "80" || Port == "443")
                Port = "";
            else
                Port = ":" + Port;
            var Protocol = context.Request.ServerVariables["SERVER_PORT_SECURE"];
            if (Protocol == null || Protocol == "0")
                Protocol = "http://";
            else
                Protocol = "https://";

            var appPath = "";
            if (includeAppPath) {
                appPath = context.Request.ApplicationPath;
                if (appPath == "/")
                    appPath = "";
            }
            var sOut = Protocol + context.Request.ServerVariables["SERVER_NAME"] + Port + appPath;
            return sOut;
        }
    }

Exposing this to your ViewPage is also pretty simple. Phil explains here how to craft up your very own ViewPage, overriding the default one. Now we can simply drop in a property on the ViewPage and kick up our new DynamicRoute dealio:

 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace VidPub.Web.Infrastructure {
    public class RobsViewPage:WebViewPage {

        public dynamic Routes { get; set; }
        public override void InitHelpers() {
            base.InitHelpers();
            Routes = new DynamicRoute(Url);
        }

        public override void Execute() {
            base.ExecutePageHierarchy();
        }
    }

}

Buttery Funtime

Now we can simply call our routes by name:

 
  @Routes.root_url

Also - I added a sniffer for arguments send in - it will work just fine if you need to identify an “id” variable for the route:

 
  @Routes.root_url(new {id = "stuff"})

That incantation is still a bit noisy. If you read the code for GetUrl() above, you can see a test for “IsPrimitive” or the type being a string. If that’s the case, the AnonymousObject incantation is done for you - so you can just pass in your value:

 
  @Routes.root_url("stuff")

I worked this up as part of the Real-World ASP.NET MVC 3 series I’m about to release for Tekpub. Shameless plug - I’ve got more goodies where this came from :).

Blog comments powered by Disqus

My name is Rob Conery and I am the owner/smooth operator of Tekpub, creator of
This Developer's Life, and an avid Ruby/Rails/.NET developer.

Find Something