Home .NET A simple way to add/key/value routing to ASP .NET MVC

A simple way to add/key/value routing to ASP .NET MVC

by admin

The other day I had a chance to try ASP .NET MVC 1.0, which I’ve read about inthe blogs and here on Habra. At first sight, I liked the simplicity of the concept used and the logical connection to the ASP .NET architecture (the familiar aspx, ascx, masterpages, Global.asax – only now used in a slightly different way). However, for all its convenience, the way of setting routing and passing parameters as {Controller}/key1/value1/key2/value2 I couldn’t find any. You can set them asmuch as you want, but unfortunately they have to be in the right place, and thisissometimes very inconvenient, especially when passing a large number of values. Some arguments might have default values, so forcing them into a URL isnot the best solution. Of course, you could use the standard one, ?key1=value1key2=value2 way, but personally, for some reason I wanted to be able to set parameters in this, "MVC-style", ifI may say so 🙂
Turned out quite well, and without much effort. For starters, I flipped through a couple of articles on ASP .NET MVC architecture, found a description of the basic steps in the request lifecycle : www.asp.net/learn/mvc/tutorial-22-cs.aspx From the description, in order to implement your own routing handler, you need to replace or modify the standard UrlRoutingModule so that ifthere is no suitable route, additional checks are made to match "special" routings that will identify the Action-methods of some controller with an undefined number of parameters. Having looked with Reflector into the code UrlRoutingModule , you can see that the main jobthisclassdoes is handling the events of Application.PostMapRequestHandler and Application.PostResolveRequestCache The code of the method to which the event handling is delegated suggests that the existing modification code is safe to work :

public virtual void PostResolveRequestCache(HttpContextBase context)
{
RouteData routeData = this RouteCollection.GetRouteData(context);
//And ifrouteData == null, then our handler is activated
if (routeData != null )
{
IRouteHandler routeHandler = routeData.RouteHandler;
if (routeHandler == null )
{
throw new InvalidOperationException( string Format(...));
}
if (!(routeHandler is StopRoutingHandler))
{
RequestContext requestContext = new RequestContext(context, routeData);
IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext);

}
}
}
* This source code was highlighted with Source Code Highlighter

Next, if we are going to addspecial logic to this method, we need to distinguish our, "special" routers in the RouteTable table from the regular ones. For that I used the signature {…}, which means there can be any number of parameters in this place which will be organized by /key/value pairs, i.e. for the pattern MyExtendedController/{…}would be a valid URL, like this : /MyExtendedController/searchBy/name/page/3/pageSize/15
So, the task is to add code which will take Request from context, check it against one of the "special" routers, and replace legitimate nullwith fake routeData, which will pass control to the right controller and its Action-method. Then you need to attach the modified code to the application. Here we could go several ways, the first – modification of the System.Web.Routing assembly – rejected immediately for unnecessary complexity and inconvenience, the second – inheritance from UrlRoutingModule and redefining of appropriate virtual method – ok, but I chose the third way, namely, rip code UrlRoutingModule from System.Web.Routing assembly by reflector (because it does not carry external dependencies) and simply modify the desired methods. Everything was successful, the added code looks like this :

// Try to find an extended routing from routes table
if (routeData == null ) {
foreach (RouteBase routeBase in this RouteCollection) {
if (routeBase is Route) {
Route route = routeBase as Route;
RouteGhost routeGhost = new RouteGhost(route.Url, route.Defaults, route.Constraints, route.DataTokens, route.RouteHandler);
if ((routeData = routeGhost.GetRouteData(context)) != null ) {
break ;
}
}
}
}
* This source code was highlighted with Source Code Highlighter

here RouteGhost is a dummy class, inherited from Route, where we can copy all the data from the Route being checked and see if it’s a match for us. RouteGhost overrides the method

RouteData GetRouteData(HttpContextBase httpContext)
* This source code was highlighted with Source Code Highlighter

which just checks if the request from context.Request matches the Route.Url pattern, taking into account {…}.
So, our module analogue of UrlRoutingModule has been created, and now in order to register it in the application, in config web.config it is enough to correct one line :

< httpModules >
< add name ="ScriptModule" type ="System.Web.Handlers.ScriptModule, System.Web.Extensions,
Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
/>
< add name ="UrlRoutingModule" type ="System.Web.Routing.UrlRoutingModule, System.Web.Routing,
Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
/>
< httpModules >
* This source code was highlighted with Source Code Highlighter

and a similar line in the section in the configuration <system.webServer>, replacing the type with your own and pointing to your assembly. After that, everything should work the same way as before, except now we have our own advanced mechanism in addition to the standard way to resolve routes, which we can customize as we want.
For the demonstration I sketched a micro-project based on the standard heloworld template from the ASP .NET MVC delivery and created a TestController:

public class TestController : Controller
{
public ActionResult Index( int ?param1, string param2, string param3)
{
ViewData[ "param1" ] = (param1 == null ) ? "null" : param1.ToString();
ViewData[ "param2" ] = param2 ?? "null" ;
ViewData[ "param3" ] = param3 ?? "null" ;
//
return View( "Test" );
}
}
* This source code was highlighted with Source Code Highlighter

and View

< asp:Content ID ="Content2" ContentPlaceHolderID ="MainContent" runat ="server" >
< h2 > Test </ h2 >
< b > param1 = andlt;%. = ViewData[ "param1" ] %> </ b >
< br />
< b > param2 = <% = ViewData[ "param2" ] %> </ b >
< br />
< b > param3 = <% = ViewData[ "param3" ] %> </ b >
</ asp:Content >
* This source code was highlighted with Source Code Highlighter

Due to the fact that our advanced routing only works if there is no match with normal, "normal" routes, we had to change the standard

routes.MapRoute(
"Default" ,
"{controller}/{action}/{id}" ,
new { controller = "Home" action = "Index" id = "" }
);
* This source code was highlighted with Source Code Highlighter

to the sequence

routes.MapRoute(
Account_Default
",
Account/{action}/{id}"
,
new { controller = "Account" action = "Index" id = "" }
);
routes.MapRoute(
Home_Default" ,
"Home/{action}/{id}" ,
new { controller = "Home" action = "Index" id = "" }
);
routes.MapRoute(
"Default" ,
"{controller}" ,
new { controller = "Home" action = "Index" id = "" }
);
* This source code was highlighted with Source Code Highlighter

Otherwise, when asked for /Test/. or /Test/param1/value1/ would trigger the standard {controller}/{action}/{method}
And at the end we add

routes.MapRoute(
"Test_Extended" ,
"Test/{...}" ,
new { controller = "Test" action = "Index" id = "" }
);
* This source code was highlighted with Source Code Highlighter

Run it, everything works!
A simple way to add/key/value routing to ASP .NET MVC
The test project itself can be downloaded here

You may also like