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!
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();
}
}
}
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 :).
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.