Easier ASP.NET MVC Routing
Posted
by Steve Wilkes
on Geeks with Blogs
See other posts from Geeks with Blogs
or by Steve Wilkes
Published on Wed, 30 Nov 2011 19:46:00 GMT
Indexed on
2011/12/01
1:56 UTC
Read the original article
Hit count: 698
I've recently refactored the way Routes are declared in an ASP.NET MVC application I'm working on, and I wanted to share part of the system I came up with; a really easy way to declare and keep track of ASP.NET MVC Routes, which then allows you to find the name of the Route which has been selected for the current request.
Traditional MVC Route Declaration
Traditionally, ASP.NET MVC Routes are added to the application's RouteCollection using overloads of the RouteCollection.MapRoute() method; for example, this is the standard way the default Route which matches /controller/action URLs is created:
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional });
The first argument declares that this Route is to be named 'Default', the second specifies the Route's URL pattern, and the third contains the URL pattern segments' default values. To then write a link to a URL which matches the default Route in a View, you can use the HtmlHelper.RouteLink() method, like this:
@ this.Html.RouteLink("Default", new { controller = "Orders", action = "Index" })
...that substitutes 'Orders' into the {controller} segment of the default Route's URL pattern, and 'Index' into the {action} segment. The {Id} segment was declared optional and isn't specified here.
That's about the most basic thing you can do with MVC routing, and I already have reservations:
- I've duplicated the magic string
"Default"between theRoutedeclaration and the use ofRouteLink(). This isn't likely to cause a problem for the defaultRoute, but once you get to dozens ofRoutes the duplication is a pain. - There's no easy way to get from the
RouteLink()method call to the declaration of theRouteitself, so getting the names of theRoute's URL parameters correct requires some effort. - The call to
MapRoute()is quite verbose; with dozens ofRoutes this gets pretty ugly. - If at some point during a request I want to find out the name of the
Routehas been matched.... and I can't.
To get around these issues, I wanted to achieve the following:
- Make declaring a
Routevery easy, using as little code as possible. - Introduce a direct link between where a
Routeis declared, where theRouteis defined and where theRoute's name is used, so I can use Visual Studio's Go To Definition to get from a call toRouteLink()to the declaration of theRouteI'm using, making it easier to make sure I use the correct URL parameters. - Create a way to access the currently-selected
Route's name during the execution of a request.
My first step was to come up with a quick and easy syntax for declaring Routes.
1 . An Easy Route Declaration Syntax
I figured the easiest way of declaring a route was to put all the information in a single string with a special syntax. For example, the default MVC route would be declared like this:
"{controller:Home}/{action:Index}/{Id}*"
This contains the same information as the regular way of defining a Route, but is far more compact:
- The default values for each URL segment are specified in a colon-separated section after the segment name
- The {Id} segment is declared as optional simply by placing a * after it
That's the default route - a pretty simple example - so how about this?
routes.MapRoute(
"CustomerOrderList",
"Orders/{customerRef}/{pageNo}",
new { controller = "Orders", action = "List", pageNo = UrlParameter.Optional },
new { customerRef = "^[a-zA-Z0-9]+$", pageNo = "^[0-9]+$" });
This maps to the List action on the Orders controller URLs which:
- Start with the string Orders/
- Then have a {customerRef} set of characters and numbers
- Then optionally a numeric {pageNo}.
And again, it’s quite verbose. Here's my alternative:
"Orders/{customerRef:^[a-zA-Z0-9]+$}/{pageNo:^[0-9]+$}*->Orders/List"
Quite a bit more brief, and again, containing the same information as the regular way of declaring Routes:
- Regular expression constraints are declared after the colon separator, the same as default values
- The target controller and action are specified after the ->
- The {pageNo} is defined as optional by placing a * after it
With an appropriate parser that gave me a nice, compact and clear way to declare routes. Next I wanted to have a single place where Routes were declared and accessed.
2. A Central Place to Declare and Access Routes
I wanted all my Routes declared in one, dedicated place, which I would also use for Route names when calling RouteLink(). With this in mind I made a single class named Routes with a series of public, constant fields, each one relating to a particular Route. With this done, I figured a good place to actually declare each Route was in an attribute on the field defining the Route’s name; the attribute would parse the Route definition string and make the resulting Route object available as a property. I then made the Routes class examine its own fields during its static setup, and cache all the attribute-created Route objects in an internal Dictionary. Finally I made Routes use that cache to register the Routes when requested, and to access them later when required.
So the Routes class declares its named Routes like this:
public static class
Routes
{
[RouteDefinition("Orders/{customerName}->Orders/Index")]
public const string OrdersCustomerIndex = "OrdersCustomerIndex";
[RouteDefinition("Orders/{customerName}/{orderId:^([0-9]+)$}->Orders/Details")]
public const string OrdersDetails = "OrdersDetails";
[RouteDefinition("{controller:Home}*/{action:Index}*")]
public const string Default = "Default";
}
...which are then used like this:
@ this.Html.RouteLink(Routes.Default, new { controller = "Orders", action = "Index" })
Now that using Go To Definition on the Routes.Default constant takes me to where the Route is actually defined, it's nice and easy to quickly check on the parameter names when using RouteLink(). Finally, I wanted to be able to access the name of the current Route during a request.
3. Recovering the Route Name
The RouteDefinitionAttribute creates a NamedRoute class; a simple derivative of Route, but with a Name property. When the Routes class examines its fields and caches all the defined Routes, it has access to the name of the Route through the name of the field against which it is defined. It was therefore a pretty easy matter to have Routes give NamedRoute its name when it creates its cache of Routes. This means that the Route which is found in RequestContext.RouteData.Route is now a NamedRoute, and I can recover the Route's name during a request. For visibility, I made NamedRoute.ToString() return the Route name and URL pattern, like this:
The screenshot is from an example project I’ve made on bitbucket; it contains all the named route classes and an MVC 3 application which demonstrates their use. I’ve found this way of defining and using Routes much tidier than the default MVC system, and you find it useful too ![]()
© Geeks with Blogs or respective owner