For most of ASP.NET MVC's lifetime, routing has been accomplished via Convention Routing, which allows developers to specify a format or group of formats which can be used to parse incoming URLs and determine the appropriate actions, controllers, and data to use for that request.
In MVC 5, though, Microsoft introduced another scheme called Attribute Routing. Attribute Routing allows us to define routes in close proximity to their actions, giving us greater flexibility.
Let's dive into Routing as a whole, and then show how to implement Convention Routing and Attribute Routing and why they actually work together nicely.
What is Routing?
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.
"Hackable" URLs
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
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
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.
Matching by Convention
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 HomeController
and the Documents()
action therein, 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 the HomeController
and the 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()
of HomeController
(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 in the HomeController
and the value of 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
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.
Enable Attribute Routing
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
}
}
Simple Example
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.
[Route] Parameters
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 curly braces {}
:
[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.
Route Prefixes
We can use the [RoutePrefix]
attribute to specify a route prefix 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() { ... }
}
Default Routes
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();
}
}
Names and Order
The [Route]
attribute 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.
Using Both Convention and Attribute Routing
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.
Which is Better?
In this developer's opinion, Attribute Routing is a more flexible solution than Convention Routing, if only because it allows you quite a bit more flexibility and places the routes next to the actions that will actually use them. However there are certainly benefits to using both in tandem, particularly in situations when you know how some routes will look but aren't sure about others.
But don't take my opinion as gospel; try them both for yourself!
Happy Coding!