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.

blog comments powered by Disqus