This article will introduce you to the concept of routing in ASP.NET Core framework.

What is routing?

Simply said routing is a process of mapping URLs to controllers and actions. If you’ve ever wondered how does your app “know” that /Home/Index request should be handled by HomeController.Index method then routing is the answer.

You can make special routes, which allow you to provide additional parameters as a part of url. A common example is an “id”. In the “old days” URLs looked like this: http://localhost/page?id=1 now you can easily map it to more human friendly http://localhost/page/1 or even http://localhost/page/about-us.

Default routing

Routes are defined in Startup.Configure method. This is where the default routing sits. Here’s how it looks like when you create a new project:

1
2
3
4
5
6
app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Home}/{action=Index}/{id?}");
});

Routes have name. I’ll show you later why/how to make use of it, but the most important part is a template. In essence template is a “map” of url address. Let’s break down the default template piece by piece.

Notice that it has 3 segments, each separated by a /. We can also see that it has 3 parameters controller, action and id. Parameters are always between {} brackets. You probably already guessed that the controller token maps first segment of url to a controller, action to a method inside controller and id to id parameter of this method.

This is correct, but there is more to it. In the default route all parameters are optional. Question mark in {id?} means that parameter may or may not be present within url, {action=Index} means that when there is no action then it should look for Index method and {controller=Home} means that the default controller is HomeController. It’s worth noting that when you want to pass id you also have to pass controller and action, so /1 url won’t work, because routing will interpret it as a controller with name “1” (it will look for 1Controller and try to execute Index method). Similary, when you want to use different method/action you need to define controller parameter.

Default route structure

Routing parameters

We have already spoken about parameters in previous section. Yet there is much more we can do with it. As you’ve probably guessed parameters in route template are binded by name, which means that {id} in your route template will be binded to parameter named id (e.g. as in public IActionResult Index(int? id)).

Parameters may be obligatory or optional. If you want to make it optional then you add question mark at the end, just like we already did in {id?}.

You may define as much parameters as you want:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Startup.cs
routes.MapRoute(
    name: "default",
    template: "{controller=Home}/{action=Index}/{page}/{pageSize}");

// url /Home/Index/1/10
// HomeController.cs
public IActionResult Index(int page, int pageSize)
{
    // page == 1
    // pageSize == 10

    return View();
}

// as long as your action method has those parameter
// it will also work bind those values
// url /Users/List/2/5
// UsersController.cs
public IActionResult List(int page, int pageSize)
{
    // page == 2
    // pageSize == 5

    return View();
}

// url /
// default route
// controller == "Home"
// action == "Index"
// page == 0
// pageSize == 0

You can use a “catch-all” parameter which will basically grab the rest of the url by adding a start before name of the parameter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Startup.cs
routes.MapRoute(
    name: "default",
    template: "{controller=Home}/{action=Details}/{*name}");

// url /Home/Details/contact-page/about
// HomeController.cs
public IActionResult Details(string name)
{
    // name == "contact-page/about"

    return View();
}

So far we’ve seen default values for controller and action, but you should know that you can also set default values for other parameters. Let’s see how we can do this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// Startup.cs
routes.MapRoute(
    name: "default",
    template: "{controller=Articles}/{action=Index}/{*language=en}");

// url /Articles/Index
// ArticlesController.cs
public IActionResult Details(string language)
{
    // language == "en"
    return View();
}

// url /Articles/Index/pl
// ArticlesController.cs
public IActionResult Details(string language)
{
    // language == "pl"
    return View();
}

There is one more thing I want to show you. You can use static text inside route e.g. when you want to point to controller/action by entering specific url. However, when you define text base route you have to define defaults somewhere else, you cannot do that in the route template. Fortunately it’s possible to do that using defaults parameter of the MapRoute.

1
2
3
4
5
6
7
8
// Startup.cs
routes.MapRoute(
    name: "egg",
    template: "easter-egg",
    defaults: new { controller = "Home", action = "EasterEgg"});

// url /easter-egg
// points to HomeController.EasterEgg method

Multiple routes

You can create multiple routes, all you have to do is to add more MapRoute calls. Routing engine tries to find best matching route for given url, if it can’t it throws an exception. Routes are processed in order they’re added. It means that the first  route is going to be evaluated firstly. If it’s a match then it’s going to be delegated to controller, otherwise it will try to match next route. You have to take that into account, because it may match wrong route for given url. For this reason you should leave default route at the end.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Startup.cs

// url /easter-egg is caught by "egg" route
routes.MapRoute(
    name: "egg",
    template: "easter-egg",
    defaults: new { controller = "Home", action = "EasterEgg"});

// everything else is caught by "default" route
routes.MapRoute(
    name: "default",
    template: "{controller=Home}/{action=Index}/{id?}");

Routing constraints

Route constraints are are more advanced concept. They help us match correct route by filtering values. A good example for this is when we want to make sure that id parameter of a route is an int and it’s positive. ASP.NET Core routing already has some built-in constraint, but you can also create your own.

One way to use constraints is to define it “inline” as a part of route template. For example to make sure that default route catches all routes where id is an integer value we have to add int constraint after parameter name and a colon:

1
2
3
routes.MapRoute(
    name: "default",
    template: "{controller=Home}/{action=Index}/{id:int}");

Since this article is an introduction I won’t be explaining how to create custom constraints. Let me just tell you that you have to implement IRouteConstraint (like all the built-in constraints). Let our int constraint be an example for this. Basically int in route template is evaluated by IntRouteConstraint class. So the other way to put this constraint is to use constraints parameter of a MapRoute:

1
2
3
4
routes.MapRoute(
    name: "default",
    template: "{controller=Home}/{action=Index}/{id}",
    constraints: new { id = new IntRouteConstraint() });

You can find full list of built-in constraints here. Be aware that some of the constrains (like datetime or double) use invariant culture, which means that you have to pay attention to your date/number format.

Conventional routing vs attribute routing

So far we’ve talked only about conventional routing, which means that we are building routes via MapRoute method. There is another way to build routes, we can define them using attributes.

The main attribute here is Route which you place over controllers/actions. But you can also define routes within HttpGet,HttpPost (and others). Let’s see an example:

1
2
3
4
5
6
7
8
public class HomeController : Controller
{
   // matches /Home/Index
   [Route("Home/Index")]
   public IActionResult Index()
   {
      return View();
   }

As you see Route attribute says that this place (HomeController.Index) can be reached with /Home/Index. You can use multiple attributes on single action, so if you want to reach the same method with different urls you have to do something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class HomeController : Controller
{
   // mathes /
   [Route("")]
   // matches /Home
   [Route("Home")]
   // matches /Home/Index
   [Route("Home/Index")]
   public IActionResult Index()
   {
      return View();
   }

You can also use mixed routing. Some parts of your application can be reached by conventional and some by attribute routing. If you use Route attribute on controller/action then it can only be reached via attribute based routing, otherwise it can only be reached by means of conventional routing. It’s common to use coventional routing for regular browser apps and attribute routing for [REST] APIs.

I’m not going to dive deeper with it. I wanted to mention it just to make sure you’re aware that something like this exists.

URL generation

Let’s quickly get back to the name parameter of the routes. You can create urls via Url.Action method in your controller or via IUrlHelper (also Url.Action) in views. You can pass controller, action and all the required parameters e.g Url.Action("Show", "Orders", new { id = 1 }), but you can also create valid urls by passing route name like so @Url.RouteUrl("egg") (parameter value is the name of the route, so it will look for a route named “egg”). This way you won’t have to remember all the details of the the route. This may come in handy at times.