Why I Love Attribute Based Routing
Posted: 2011/07/25 Filed under: code 8 CommentsOver at Stack Exchange we make use of this wonderful little piece of code, the RouteAttribute (an old version can be found in our Data Explorer; a distinct, somewhat hardened, version can also be found as part of StackID, and I link the current version toward the bottom of this post). Thought up by Jarrod “The M is for Money” Dixon sometime around April 2009, this is basically the only thing I really miss in a vanilla MVC project.
Here’s what it looks like in action:
public class UsersController : ControllerBase { [Route("users/{id:INT}/{name?}")] public ActionResult Show(int? id, string name, /* and some more */){ // Action implementation goes here // } }
Nothing awe-inspiring, all that says is “any request starting with /users/ followed by a number of 9 or fewer digits (tossing some valid integers out for simplicity’s sake), and optionally by / and any string should be routed to the Show action”.
Compare this to the standard way to do routing in MVC:
public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { // Other stuff routes.MapRoute( "Default", "{controller}/{action}/{id}", /* defaults go here */ ); } }
This isn’t exactly 1-to-1 as we end up with /users/show/123 instead of /users/123/some-guy, but now let’s call them equivalent. There are good reasons for why you’d want the /users/{id}/{name} route, which are discussed below.
Where’s the gain in using the RouteAttribute?
Ctrl-Shift-F (search in files, in Visual Studio) is way up there. With the RouteAttribute, the code behind a route is sitting right next to the route registration; trivial to search for. You may prefer to think of it as code locality, all the relevant bits of an Action are right there alongside its implementation.
Some might scoff at the utility of this, but remember that UsersController? That’s split across 14 files. The assumption that enough information to identify the location, in code, of an Action can be shoved in its URL falls apart unless you’re ready to live with really ugly urls.
Action method name flexibility. The RouteAttribute decouples the Action method and Controller names from the route entirely. In the above example, “Show” doesn’t appear anywhere, and the site’s urls are better for it.
Granted, most routes will start out resembling (if not matching) their corresponding method names. But with the RouteAttribute, permalinks remain valid in the face of future method renaming.
You’re also able to be pragmatic with Action method locations in code, while presenting a pristine conceptual interface. An administrative route in, for example, the PostsController to take advantage of existing code would still be reached at “/admin/whatever.”
A minor nicety, with the RouteAttribute it’s easy to map two routes to the same Action. This is a bit ugly with routing rules that include method/controller names, for obvious reasons.
Metadata locality. Our RouteAttribute extends ActionMethodSelectorAttribute, which lets us impose additional checks after route registration. This lets you put acceptable HTTP methods, permitted user types, registration priorities (in MVC, the order routes are registered matters), and the like all right there alongside the url pattern.
A (slightly contrived) example:
[Route("posts/{id:INT}/rollback/{revisionGuid?}", HttpVerbs.Post, EnsureXSRFSafe = true, Priority=RoutePriority.High)]
The strength here is, again, grouping all the pertinent bits of information about a route together. MVC already has enough of this approach, with attributes like HttpPost, that you’ll be decorating Actions with attributes anyway.
No need for [NonAction]. The NonActionAttribute lets you suppress a method on a controller that would otherwise be an Action. I’ll admit, there aren’t a lot of public methods in my code that return ActionResults that aren’t meant to be routable, but there are a number that return strings. Yes, if you weren’t aware, a public method returning a string is a valid Action in MVC.
It seems that back in the before times (in the original MVC beta), you had to mark methods as being Actions rather than not being actions. I think the current behavior (opting out of being an Action) makes sense for smaller projects, but as a project grows you run the risk of accidentally creating routes.
You (probably) want unconventional routing. One argument that has arisen internally against using the RouteAttribute is that it deviates from MVC conventions. While I broadly agree that adhering to conventions is Good Thing™, I believe that the argument doesn’t hold water in this particular case.
The MVC default routing convention of “/{controller}/{action}/{id}” is fine as a demonstration of the routing engine, and for internal or hobby projects it’s perfectly serviceable… but not so much for publicly facing websites.
Here are the two most commonly linked URLs on any Stack Exchange site.
/questions/{id}/{title} as in http://stackoverflow.com/questions/487258/plain-english-explanation-of-big-o
/users/{id}/{name} as in http://stackoverflow.com/users/59711/arec-barrwin
In both cases the last slug ({name} and {title}) are optional, although whenever we generate a link we do our best to include them. Our urls are of this form for the dual purposes of making them user-readable/friendly, and as SEO. SEO can be further divided into hints to Google algorithms (which is basically black magic, I have no confirmation that it actually does anything) and the more practical benefit of presenting the title of a question twice on the search result page.
Closing Statement
Unlike the WMD editor, Booksleeve, or the MVC MiniProfiler we don’t have an open source “drop in and use it” version of the RouteAttribute out there. The versions released incidentally are either out-dated (as in the Data Explorer) or a cut down and a tad paranoid (as in StackID). To rectify this slightly, I’ve thrown a trivial demonstration of our current RouteAttribute up on Google Code. It’s still not a simple drop in (in particular XSRF token checking had to be commented out, as it’s very tightly coupled to our notion of a user), but I think it adequately demonstrates the idea. There are definitely some quirks in the code, but in practice it works quite well.
While I’m real bullish on the RouteAttribute I’m not trying to say that MVC routing is horribly flawed, nor that anyone using it has made a grave error. If it’s working for you, great! If not, you should give attribute based routing a gander. If you’re starting something new I’d strongly recommend playing with it, you just might like. It’d be nice if a more general version of this were shipping as part of MVC in the not-horribly-distant future.
Why don’t u suggest this to Phill Haack . He might add it to MVC 4.
🙂
We actually have talked to Phil about our RouteAttribute, but we always sort of forget to follow up with actual code.
This post is in part to rectify that (as now our implementation is basically out there for all to see), and also to lay down the reasons we (or at least, I) like it.
A few things of note, as a user and non-user of the RouteAttribute:
1) Ctrl + Shift + F is a serious benefit, locating code being executed can be difficult, particularly in large codebases or for code you didn’t write. In fact, if the guys from the MVC team would implement a “search route” feature that dropped you off at the action to be executed, that would be awesome. This is a deeper problem than it appears due to context (http verbs, subdomains in multi-tenancy apps, etc) but a simple implementation would easily match the benefit of the route attribute.
2) Action-method name flexibility. I disagree this is a benefit. There is a built in ActionName attribute which you can apply to any action. Sure this is only beneficial when your route maps directly to the action name, but any route you can define in the route attribute can also be defined using the existing mechanism. The benefit here is defining the route right next to where it is being used. The major drawback to that is that it can make identifying route conflicts less obvious. Defining two routes to the same action (a no-no for purists) is a moot point if the project is willing to define more than just “God” routes (meaning only a few abstract routes to handle all routing for your application).
3) Metadata locality is another major benefit. Especially when a project repeatedly requires adding a filter to actions, and instead extends this attribute to include it.
4) No need for NonAction. This makes the assumption the consumer of the RouteAttribute has gone whole hog with it’s usage, eliminating all “God” routes, with allowably the exception of the {*url} route for 404. With the route attribute, for the non use of NonAction to apply, mix and match isn’t allowed. In any case, if the user wants a function to not act as an action, they are better off in the long run using the NonAction attribute, because eventually somebody will add a route that could hit it.
Another assumption made here is that you are writing your own links. The built in route matching algorithm has n squared run time (i.e. it gets slower pretty quickly after you define ~200 routes and are generating 10 or more links per page). If you are writing your links by hand, or calling routes by name (which arguably should be standard practice), you avoid the issue altogether. In any case, usually mo routes = mo problems.
There are plenty of ways to attack the “find a routes implementation” problem, I just happen to be a big fan of the low-tech “find in files” solution. For example, you can do a simple search in explorer for “Route(users type:cs” [note the lack of inner quotation mark] in Explorer to find all files that declare a route under /users. It’s nice to not be utterly dependent on Visual Studio, although in practice this is a really minor point.
I feel by the time you’re using the ActionNameAttribute (http://msdn.microsoft.com/en-us/library/system.web.mvc.actionnameattribute.aspx) you’ve decided to tweak your routing rules with an Attribute… why not go whole hog and use a RouteAttribute?
You’re point about [NonAction] is completely correct, you have to have gone all in on the RouteAttribute to get it’s benefit (through lack of use). Existing code-bases make this difficult, though starting from scratch it’s eminently doable.
ActionLink (http://msdn.microsoft.com/en-us/library/dd493018.aspx) and similar are very slow as the # of routes increases, which is unfortunate. Perhaps you should ping Phil Haack with that (since the Careers team knows much more than Core about that one)? That said, I don’t consider building your own links bad; a dynamic look-up doesn’t buy you much, since you general don’t want to break links anyway.
As Nick mentions above, this approach makes ActionLink a problem instead of a convenience. I happen to think that ActionLink is a core feature of MVC — it gets us out of the “remembering URLs” business. (With T4MVC, you can also know the correctness of ActionLinks at compile time.)
If 90% of links use a conventional route with a handful of prettier ones sprinkled in, one ends up with routes in the low dozens instead of hundreds.
Conveniences such as shift-ctrl-F become unnecessary when one can assume a route follows {controller}/{action}/{id} with known exceptions. Though I will admit to using said shortcut. 🙂
RouteAttribute is very smart and well-implemented, but I think it solves a non-problem for most sites.
I personally feel that ActionLink gets you out of the remembering URL business and into the remembering Action/Controller name business (there’s a blog post somewhere in my horribly failed attempt to “do it better” in StackID; I need to stake out some time to look at how T4MVC does it though). It’s one string for another, but the URLs are user & tester facing. However if you’ve made your peace with that, their degradation with the RouteAttribute does have to be worked around; at least until MVC ships with a more performant implementation.
If you’re willing to stick with the convention, yes Ctrl-Shift-F isn’t terribly useful (well, until you get into splitting controllers; then you’d best hope your action names are pretty unique 🙂 ). I contend that you give up quite a bit in Action naming and URL flexibility by sticking to the MVC conventions, but it’s certainly not a make or break for a website’s code-base.
Like I said, sticking to the default routing (that is, not using the RouteAttribute) isn’t some grave error. It really is quite useful in my experience though, so I’d encourage anyone working in MVC to give it a go.
I actually implemented my own version of this awhile ago in PHP. I found the feature it in FatFree MVC and thought it was great.
My only issue is with a language like PHP, you end up having to put all of the routes in a single file, or keep the route in the class file and include all the files manually, which makes Autoloading useless.
I try to stick to the standard conventions for routes, however I would always have a few custom routes in every project for SEO or for shortcuts. I’ve always hated the fact that these were defined in the global.asax or bootstrapper away from the controllers/actions themselves.
The RouteAttribute is working perfectly for my scenario – I use standard conventions for most routes, and RouteAttributes for any additional/custom routes.
The current version of RouteAttribute doesn’t support Areas, however it only took two lines of code to fix this (my solution might be a bit naive but works for me) –
At the end of MapDecoratedRoutes, before the route is added, it just needed –
if (controllerNamespace.Contains(“.Areas.”))
route.DataTokens[“area”] = controllerNamespace.Split(‘.’).SkipWhile(n => n != “Areas”).ElementAt(1);
I love how this works now – thanks again for making this available!