Understanding Routing
Index
Introduction
One of the important feature of ASP.NET MVC is Routing. The Routing infrastructure helps us to map the incoming requests to controllers and actions. The routing module ships with a separate assembly called System.Web.Routing and that helps us to use the routing infrastructure outside ASP.NET MVC applications, like in Webforms.
In this article we are going to see about the important details of routing infrastructure. First we start from basics and slowly move to the advanced concepts and at-last we see how we can simplify creating routes by using our own extension methods. For people who are already familiar with the basic things they can jump to the last section where we discuss about creating cool extension methods to simplify routing.
Basics
All the routes used in an MVC application has to be defined in the Application_Start event of the Global.asax.cs. When we create a brand new MVC application using one of the built-in templates available in VS we notice a method named RegisterRoutes in Global.asax.cs and inside that method we define all our routes consumed by our application.
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults ); }
Listing 1. RegisterRoutes method and default routes
The RegisterRoutes method is called from the Application_Start event,
RegisterRoutes(RouteTable.Routes);
Listing 2. Calling RegisterRoutes from Application_Start
What is this RouteTable? let see that next.
RouteTable
The RouteTable is the one that stores all the routes defined in the application. The RouteTable contains property named Routes which is of type RouteCollection and it is where we add all our routes. From the Application_Start we pass this RouteCollection to the RegisterRoutes method.
So back in the RegisterRoutes method we'll see couple of methods called in the RouteCollection. Let's leave the IgnoreRoute for the time being and discuss about the MapRoute method.
MapRoute
As you guessed, MapRoute is the method used to define new routes in our application. The MapRoute method does not belongs to System.Web.Routing assembly but they are the extension methods defined in the System.Web.Mvc assembly.
The following are the different overloaded extension methods of MapRoute.
public static Route MapRoute(this RouteCollection routes, string name, string url) public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults) public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints) public static Route MapRoute(this RouteCollection routes, string name, string url, string[] namespaces) public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, string[] namespaces) public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces)
Listing 3. MapRoute extension methods
The default route is little complex to explain so let's start with a very simple basic route.
routes.MapRoute( "MyRoute", "{controller}/{action}");
Listing 4. Simple route
The first parameter is the name for the route and it's completely optional (but I recommend it). The second parameter is the interesting one and that's what confuses people, it's a pattern but not regular expression that contains segments which will be filled by the parameters in the url. So the above pattern contains only two segments: {controller} and {action}. Both of these segments are wrapped in curly braces and that means they can be matched by more than one values.
Let see what kind of urls matches this simple route. Any url that contains only two segments leaving the domain name will match the route. For ex. the below url contains party/index after leaving the domain name and so party is matched to the {controller} and index is matched to the {action}.
http://www.partyplanner.com/party/index
The above route also matches these urls.
http://www.partyplanner.com/comment/post
http://www.mysite.com/home/index
What about these urls?
http://www.partyplanner.com/home
http://www.partyplanner.com/party/get/2
The above two urls doesn't matches the route. The first one contains only one segment that is home and the next one contains three segments party, get and an integer 2 so this also don't matches.
Default values
While defining routes we can assign default values for segments. Specifying default values in the routes are really helpful. For ex. when you want to match a url http://www.partyplanner.com that doesn't contains controller and action segments to an action, which will be possible only by specifying defaults while defining routes.
If you look at the default route shown in Listing 1. you can see the third parameter passed to the MapRoute method which is an anonymous object. The anonymous object contains details about the default values of segments like controller, action and other parameters in the url.
Let's take the same simple route defined in Listing 4. but now with some default values passed in.
routes.MapRoute( "MyRoute", "{controller}/{action}", new { controller = "party", action = "index" } );
Listing 5. Passing default values in route
Every route should have the controller and action parameters but if that information is not available then the ones specified in the defaults are used. For ex. if the user doesn't pass the action name then the above route takes the action as index, likewise if the user doesn't pass both the controller and action (ex. http://www.partyplanner.com) then the route takes the controller as party and action as index.
Having a little basic let's re-look at the default route defined in the Global.asax.cs.
routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional } );
Listing 6. Default route
The default route contains three segments: controller, action and id with default values for all of them.
Hence the route matches a wide range of url patterns. Some examples are,
http://www.partyplanner.com/party/get/1
http://www.partyplanner.com/party/get
http://www.partyplanner.com/party
http://www.partyplanner.com
Mapping complex URL patterns
Through routing we can typically map any complex route quite easily to actions. Here are some complex routes.
Route 1
routes.MapRoute( "Search", "{controller}/search/query={query}?p={p}", new { controller = "Home", action = "Search", query = UrlParameter.Optional, p = 1 } );
Listing 7. Complex route 1
Matches..
http://www.prideparrot.com/home/search
http://www.prideparrot.com/home/search/query=sql
http://www.prideparrot.com/home/search/query=sql?p=12
Route 2
routes.MapRoute( "Archive", "{controller}/{action}/{year}/{month}/{day}/{filename}", new { controller = "Blog", action = "Archive" } );
Listing 8. Complex route 2
Matches http://www.prideparrot.com/blog/archive/2012/12/1/routing_in_aspnetmvc
Catch-all segment
routes.MapRoute( "CustomRoute", "product/{*param}", new { controller = "Product", action = "Index" } );
Matches..
http://mysite.com/product/hello
http://mysite.com/product/hello/a/b/c
Listing 8. Defining catch-all segment in route
The interesting thing in the above route is the second segment of the url pattern (*param} which starts with an "*". The "*" says that any no of segments after that even though they are separated by "/" will be matched by this route.
Catch-all route
routes.MapRoute( "CatchAllRoute", {*url}", new { controller = "Home", action = "Index", url = UrlParameter.Optional } );
Listing 9. Catch-all route
This route is little modified version of the former one which matches any url and it's simply called as catch-all route.
Matches anything, examples..
http://mysite.com
http://mysite.com/product/hello
http://mysite.com/home/index/hello/text/1
Route with different separator
routes.MapRoute( "MyRoute", "{controller}~{action}~{id}" );
Listing 10. Using different separator in route
Instead of "/" we can even separate the segments by "~". Ex. http://mysite.com/product~list~1
Route constraints
Constraints are used to restrict a matched url to be handled by a route or not. Let's take the route we saw in the Listing 11. There is a problem in that route that is even for urls that doesn't have valid values for year, month or day still be matched by that route.
If someone send a request like http://www.prideparrot.com/blog/archive/w/x/y/z still the route matches the request and the problem is we will endup handling lot of invalid requests unnecessarily. One great way to stop handling those invalid requests is using constraints.
routes.MapRoute( "Archive", "{controller}/{action}/{year}/{month}/{day}/{filename}", new { controller = "Blog", action = "Archive" }, new { year = @"\d{4}", month = @"\d{2}", day = @"\d{2}" } );
Listing 11. Passing constraints to route
One of the MapRoute method takes an anonymous object as the fourth parameter. In the anonymous object we can pass one or many constraints. We can supply constraints in two ways: either as a string or an object that implements IRouteConstraint interface.
In the Listing 11. we have passed the route constraint as a string and when we do that it is taken as regular expression. When the passed value matches the regular expression the request succeeds else fails with 404. The other option is IRouteConstraint implementation, here is the definition of the interface.
public interface IRouteConstraint { bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection); }
Listing 12. IRouteConstraint interface
The interface contains a single method that returns a boolean. Here is an example of how we can constrain a route using this way,
routes.MapRoute( "Product", "Product/Insert", new { controller = "Product", action = "Insert"}, new { httpMethod = new HttpMethodConstraint("POST") } );
Listing 13. Passing IRouteConstraint implementation in constraints
HttpMethodConstraint is a built-in implementation of IRouteConstraint and it checks whether the request http method satisfies the passed value and if not returns false. I have a post here that explains more about route constraints.
Ignoring routes
Like mapping routes we can say the routing infrastructure not to handle requests by ignoring routes. As default the routing infrastructure doesn't handles the requests for static resources and this can be controlled by the RouteExistingFiles property. The ignore route statements should appear above the other route definitions.
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
Listing 14. IgnoreRoute example
The above route ignore all the requests that contains the extension ".axd".
The IgnoreRoute method is overloaded we can even pass constraints as a second argument to the method.
routes.IgnoreRoute("{resource}.axd/{*pathInfo}", new { resource = new CustomConstraint() });
Listing 15. Passing constraints to IgnoreRoute method
RouteExistingFiles property
As default the routing infrastructure doesn't handles requests send for static resources like images, css or js files. This is controlled by the RouteExistingFiles property and the default value is false. There are situations you may have to handle the requests for static resources by actions and that can be done by setting the RouteExistingFiles property to true.
There is a good reason we need to set this property as true. For ex. if you have images in a file system and the users are allowed to view the images based upon security level it is good to handle those requests and return the images through controller actions instead of delegating that to IIS. I've written a good article on this subject here.
The important thing is the RouteExistingFiles property is at global level not at route level, means by setting this property the requests to all the static resources has to be handled through actions which is probably not a good idea. But there is a way to solve this issue by ignoring the static resources that doesn't need to be handled before setting the property to true.
routes.IgnoreRoute("Content/{*relpath}"); routes.RouteExistingFiles = true; routes.MapRoute( "photo", "premium/photos/{accountNo}/{image}", new { controller = "Photo", action = "Index" } ); // other route mappings
Listing 16. RouteExistingFiles property
In the above case other than the static resources located in the Content folder the remaining requests are handled by the routing infrastructure.
Passing controllers namespaces
When creating routes we can even pass an array of namespaces where the controller can be searched for and this helps to avoid duplicate exception when you have controllers with same name in different namespaces.
routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }, new[] { "MvcApp.Controllers" } );
Listing 17. Passing controllers namespaces when creating routes
The namespaces we passed as an array is stored in the DataTokens property of routes. We can use DataTokens to store our custom information that can be used for different purposes.
Route
Still now we haven't seen the core class that represents a route, it's nothing other than the class called Route. Whenever you are calling MapRoute or IgnoreRoute you are adding a new instance of the Route class to the routes collection. The below listing shows the important properties and methods of this Route class.
public class Route: RouteBase { public Route(string url, IRouteHandler routeHandler); // other ctors public RouteValueDictionary Constraints { get; set; } public RouteValueDictionary DataTokens { get; set; } public RouteValueDictionary Defaults { get; set; } public IRouteHandler RouteHandler { get; set; } public string Url { get; set; } public override RouteData GetRouteData(HttpContextBase httpContext); public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values); protected virtual bool ProcessConstraint(HttpContextBase httpContext, object constraint, string parameterName, RouteValueDictionary values, RouteDirection routeDirection); }
Listing 18. Route class
This Route class is part of the System.Web.Routing assembly. Let see about each property. In the MapRoute method the anonymous object we pass for defaults is converted into RouteValueDictionary and stored in the Defaults property. Like that, the Constraints property contains the collection of constraints set for the route. The DataTokens property contains the custom values that is passed to the route handler on creating routes. The Url property contains the url pattern that we passed to the MapRoute or IgnoreRoute methods.
Route handler
Every created route is linked to a route handler. The default route handler that is set for created route is the MvcRouteHandler. A route handler is something that implements IRouteHandler.
Below is the definition of IRouteHandler.
public interface IRouteHandler { IHttpHandler GetHttpHandler(RequestContext requestContext); }
Listing 19. IRouteHandler interface
The MvcRouteHandler instantiates and returns an instance of MvcHandler that handles the request from there.
Creating custom extension methods for simplifying routing
Routing sometimes confuses people. How about mapping routes like this?
// creating a default route routes.Create("Default") .If("{controller}/{action}/{id}") .Controller("Home") .Action("Index) .Set("id", UrlParameter.Optional); // ignoring route routes.If("product/index").Reject(); // redirecting routes permanently routes.If("/party").To("/premium");
Listing 20. Using custom extension methods for creating routes
The above extension methods makes mapping routes much easy and the code is very easy to maintain and extensible.
The following are the extension methods we are going to create.
Method | Extension of | Purpose |
---|---|---|
Create | RouteCollection | Used for mapping routes. Starts the chain by creating an instance of Route class and add to the routes collection. |
If | RouteCollection | Used to set the url pattern. |
If | Route | Used to set the url pattern. |
Controller | Route | Used to set the controller that's going to handle the request for the route. |
Action | Route | Used to set the action for the route. |
Set | Route | Used to set default value for a parameter. |
Set (overloaded) | Route | Used to set default values for many parameters as an anonymous object. |
Rule | Route | Used to add an constraint to a route. |
Rules | Route | Used to add multiple constraints. |
Reject | CustomRoute | Used to reject a route similar to the IgnoreRoute method. |
To | CustomRoute | Used to set permanent redirect for a route. |
Here are the implementations,
Create(string name)
public static Route Create(this RouteCollection routes, string name = "") { var route = new Route("", new MvcRouteHandler()); routes.Add(name, route); return route; }
Listing 21. Create() extension method
If(string pattern)
public static Route If(this Route route, string pattern) { route.Url = pattern; return route; }
Listing 22. If() extension method
Controller(string controller)
public static Route Controller(this Route route, string controller) { return Set(route, "controller", controller); }
Listing 23. Controller() extension method
Action(string action)
public static Route Action(this Route route, string action) { return Set(route, "action", action); }
Listing 24. Action() extension method
Set(string param, object @default)
public static Route Set(this Route route, string param, object @default) { if (route.Defaults == null) { route.Defaults = new RouteValueDictionary(); } route.Defaults[param] = @default; return route; }
Listing 25. Set() extension method
Set(object defaults)
public static Route Set(this Route route, object defaults) { route.Defaults = new RouteValueDictionary(defaults); return route; }
Listing 26. Set() overloaded extension method
Rule(string param, object constraint)
public static Route Rule(this Route route, string param, object constraint) { if (route.Constraints == null) { route.Constraints = new RouteValueDictionary(); } route.Constraints[param] = constraint; return route; }
Listing 27. Rule() extension method
Rules(object constraints)
public static Route Rules(this Route route, object constraints) { route.Constraints = new RouteValueDictionary(constraints); return route; }
Listing 28. Rules() extension method
The implementations are simple and straight. The Create method starts the chain for mapping routes and the following methods helps to set url pattern, default values and constraints to the created route.
The extension methods for ignoring and redirecting routes are little tricky. We can't directly add extension methods to Route class to achieve that. Because for ignoring routes we need a custom route implementation and for redirecting we need a custom route handler. To simplify the things I've created a custom route class as below,
public class CustomRoute : RouteBase { public string Pattern { get; set; } public CustomRoute(string pattern) { Pattern = pattern; } public RouteBase Route { get; set; } public override RouteData GetRouteData(HttpContextBase httpContext) { return Route.GetRouteData(httpContext); } public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) { return Route.GetVirtualPath(requestContext, values); } }
Listing 29. CustomRoute class for ignoring/redirecting routes
The CustomRoute class implements and encapsulates RouteBase. It delegates the calls to the encapsulated RouteBase instance.
The IgnoreRoute class shown below is used to ignore the routes using the StopRoutingHandler and we will passing instance of the class to CustomRoute for ignoring routes.
public class IgnoreRoute : Route { public IgnoreRoute(string url) : base(url, new StopRoutingHandler()) { } public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary routeValues) { return null; } }
Listing 30. IgnoreRoute class
Here are the extension methods that helps for ignoring routes.
public static CustomRoute If(this RouteCollection routes, string pattern) { var route = new CustomRoute(pattern); routes.Add(route); return route; } public static CustomRoute Reject(this CustomRoute route) { route.Route = new IgnoreRoute(route.Pattern); return route; }
Listing 31. If() and Reject() extension methods
We have to do little more work for redirecting routes. Unlike the StopRoutingHandler we don't have any built-in route handler for redirecting so we have to create a custom one.
public class RedirectRoute : Route { public RedirectRoute(string url, string target) : base(url, new RedirectRouteHandler(target)) { } public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) { return null; } } public class RedirectRouteHandler : IRouteHandler { private string targetUrl; public RedirectRouteHandler(string targetUrl) { this.targetUrl = targetUrl; } public IHttpHandler GetHttpHandler(RequestContext requestContext) { if (targetUrl.StartsWith("~/")) { string virtualPath = targetUrl.Substring(2); Route route = new Route(virtualPath, null); var vpd = route.GetVirtualPath(requestContext, requestContext.RouteData.Values); if (vpd != null) { targetUrl = "~/" + vpd.VirtualPath; } } return new RedirectHandler(targetUrl, false); } } public class RedirectHandler : IHttpHandler { private string targetUrl; private bool isReusable; public RedirectHandler(string targetUrl, bool isReusable) { this.targetUrl = targetUrl; this.isReusable = isReusable; } public bool IsReusable { get { return isReusable; } } public void ProcessRequest(HttpContext context) { context.Response.Status = "301 Moved Permanently"; context.Response.StatusCode = 301; context.Response.AddHeader("Location", targetUrl); } }
Listing 32. RedirectRoute with handlers
And the extension method for redirecting route is,
To(string redirectUrl)
public static CustomRoute To(this CustomRoute route, string target) { route.Route = new RedirectRoute(route.Pattern, target); return route; }
Listing 33. To() extension method
That's it!
We can easily add more extension methods to the routing infrastructure to do complex things easily.