ASP.NET Routing is the ability to have URLs represent abstract actions rather than concrete, physical files.
In "traditional" websites, every URL represents a physical file, whether it is an HTML or ASPX page, or a script file, or some other content. If I see a URL of www.example.com/articles/post.aspx?id=65, I'm going to assume that that URL represents a folder called articles at the root of the site, and within that folder a page called post.aspx.
In MVC, no such physical folders and pages exist, and so the MVC architecture allows us to map routes to controllers and actions which may represent many kinds of results. Routing is a layer of abstraction on top of regular URLs that allows programmers greater control over what those URLs mean and how they are formatted.
One of the things Routing allows us to do is to create "hackable" URLs; that is, URLs whose meaning is easily read, understood, and extended upon by human beings. We can use Routing to turn this messy URL:
www.example.com/article.aspx?id=69&title=my-favorite-podcasts
into a much cleaner one:
www.example.com/articles/69/my-favorite-podcasts
The concept of "hackable" URLs goes a bit further, too. If I was a user looking at the clean URL, I might append "/comments" on the end of it:
www.example.com/articles/69/my-favorite-podcasts/comments
"Hackable" URLs implies that this should display the comments for that article. If it does, then I (the user) have just discovered how I can view the comments for any given article, without needing to scroll through the page and hunt down the appropriate link.
So how do we actually implement Routing in MVC? It all starts with something called the Route Table.
The Route Table is a collection of all possible routes that MVC can use to match submitted URLs. Items in the Route Table specify their format and where certain values are in the route structure. These values can then be mapped to controllers, actions, areas, etc. depending on their placement within the route.
Any URL that is submitted to the application will be compared to the routes in the Route Table, and the system will redirect to the first matching route found in that table. In versions of MVC up to version 5, we added routes to this table at a specific place, usually in RouteConfig. With the introduction of Attribute Routing, this method of adding routes has been retroactively termed Convention Routing.
Convention Routing approaches the routing problem general-case-first; by default, you are given a route that will probably match most if not all of your routes, and are asked to define if there are any more specific routes you would also like to handle.
A call to set up convention-based routes might look like this:
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Special",
url: "Special/{id}",
defaults: new { controller = "Home", action = "Special", id = UrlParameter.Optional }
); //Route: /Special/12
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
); //Route: /Home/Index/12
}
Let's break down some of the pieces here:
- routes is the Route Table of type RouteCollection, and stores all of the possible routes we can match for a given URL.
- A call to IgnoreRoute allows us to tell ASP.NET to ignore any URL that matches that tokenized structure and process it normally.
- A call to MapRoute allows us to add a route to the route table.
MapRoute includes a few parameters: a name of the route that must be unique to each route, a tokenized URL structure, and default values that will be applied if alternate values are not supplied by the route. The tokenized URL structure is then used to match supplied values in the URL.
Say we have this route:
routes.MapRoute(
name: "PersonDefault",
url: "{controller}/{person}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Now imagine we have this controller:
public class HomeController : Controller
{
public ActionResult Index(string person) { ... }
public ActionResult Documents(string person, int id) { ... }
}
If we submit a URL of /Home/Dude-Person/Documents/17, we will be directed to the Home controller's Documents action, and the person and id parameters will have values of "Dude-Person" and 17 respectively.
If we submit a URL of /Home/Dude-Person/Documents?id=17, we will again be directed to Home controller and Documents action with the same values as before, because MVC will look at query string values if no route values exist that match the expected parameters.
If we submit a URL of /Home/Dude-Person, we will be directed to the Index action (because that's what was specified in the defaults) with parameter person having the value "Dude-Person".
If we submit a URL of /Home we will be redirected to the Index action and person will be an empty string. If no matching value is found for a given parameter, the default value for that parameter's type is used.
One thing to keep in mind when designing your routes is that the order in which the routes are added to the table matters. The routing engine will take the first route that matches the supplied URL and attempt to use the route values in that route. Therefore, less common or more specialized routes should be added to the table first, while more general routes should be added later on.
For example, for the routes above, if we submit a URL of /Home/Documents we will be redirected to the Index action with parameter person having the value "Documents", which is probably not the desired behavior.
In short, Convention Routing approaches Routing from the general case; you generally add some routes that will match all or most of your URLs, then add more specific routes for more specialized cases. The other way to approach this problem is via Attribute Routing.
Attribute Routing (introduced in MVC 5) is the ability to add routes to the Route Table via attributes so that the route definitions are in close proximity to their corresponding actions. We will still populate the Route Table, but we will do it in a different manner.
Before we can start using Attribute Routing, though, we must first enable it.
If you want to use Attribute Routing, you have to enable it by calling MapMvcAttributeRoutes on the RouteCollection for your app (usually this is done in RouteConfig):
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapMvcAttributeRoutes(); //Enables Attribute Routing
}
}
A simple example of Attribute Routing might look like this:
public class HomeController : Controller
{
[Route("Users/Index")] //Route: /Users/Index
public ActionResult Index() { ... }
}
What that [Route] attribute does is specify a route to be added to the Route Table which maps to this action. The parameters to [Route]'s constructor are where the real functionality happens.
For example, what if we need a way to specify that we want to include parameter data in the routes? We can do so with the {} syntax:
[Route("Users/{id}")] //Route: /Users/12
public ActionResult Details(int id) { ... }
Notice that the name in the curly braces matches the name of one of the inputs to the action. By doing this, that value of that parameter will appear in the route rather than in the query string.
We can also specify if a parameter is optional by using ?:
[Route("Users/{id}/{name?}")] //Route: /Users/12/Matthew-Jones or /Users/12
public ActionResult Details(int id, string name) { ... }
If we need a given parameter to be of a certain type, we can specify a constraint:
[Route("Users/{id:int}")] //Route: /Users/12
public ActionResult Details(int id) { ... }
[Route("{id:alpha}/Documents")] //Route: /product/Documents
public ActionResult Documents(string id) { ... }
There are quite a few different constraints we can use; Attribute Routing even includes support for regular expressions and string lengths. Check this article from MSDN for full details.
We can specify a RoutePrefix that applies to every action in a controller:
[RoutePrefix("Users")]
public class HomeController : Controller
{
[Route("{id}")] //Route: /Users/12
public ActionResult Details(int id) { ... }
}
If we need to have an action that overrides the Route Prefix, we can do so using the absolute-path prefix ~/:
[RoutePrefix("Users")]
public class HomeController : Controller
{
[HttpGet]
[Route("~/special")] //Route: /special
public ActionResult Special() { ... }
}
Specifying the default route for the application also uses the absolute-path prefix ~/. We can also specify a default route for a given route prefix by passing an empty string:
[RoutePrefix("Users")]
public class HomeController : Controller
{
[Route("~/")] //Specifies that this is the default action for the entire application. Route: /
[Route("")] //Specifies that this is the default action for this route prefix. Route: /Users
public ActionResult Index() { ... }
}
We can also specify default routes another way: by capturing them as inputs.
[RoutePrefix("Users")]
[Route("{action=index}")] //Specifies the Index action as default for this route prefix
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
}
[Route] will also accept names and order values for the routes:
[RoutePrefix("Users")]
public class HomeController : Controller
{
[Route("Index", Name = "UsersIndex", Order = 2)]
public ActionResult Index() { ... }
[Route("{id}", Name = "UserDetails", Order = 1)]
public ActionResult Details(int id) { ... }
}
Order is still very important! In the above example, if we give a route of /Users/Index, we will get an exception because that matches the UserDetails route, which has a higher order. If no order is specified, the routes are inserted into the Route Table in the order they are listed.
We can solve the above conflict by either adding a constraint on UserDetails that ID must be an integer or reordering the routes and placing UsersIndex at a higher order.
Because each of the defined routes is close to their respective action, it is my recommendation that each route be as specific as possible. In other words, when using Attribute Routing, define the routes to be specific to the decorated action and not any other action.
You can also implement both Attribute and Convention routing at the same time:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
...
routes.MapMvcAttributeRoutes(); //Attribute routing
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
Notice that the setup above gives all the Order weight to the Attribute routes, since they were added to the Route Table first.