ASP.NET Core MVC has introduced quite a few concepts that new (or new-to-ASP.NET) web developers might have some difficulty getting caught up with. My ASP.NET Core Demystified series is designed to help these developers get started building their own custom, full-fledged, working ASP.NET Core applications.

In this part of the series, we'll take a look at the concept of Routing and how we can use it to match URLs with actions. As always with my tutorials, there's a sample project over on GitHub, so go take a look at that code and come along with me as we explore routing in ASP.NET Core!

A superposition of helicopter routes over New York City.

Basics

Routing is a general term used in ASP.NET Core for a system which takes URLs and maps them to controller actions, files, or other items. Said system involves the use of classes called route handlers, which do the actual mapping. Most of the time, you won't be creating routes at such a low level (e.g. creating route handlers),. rather you will be defining routes and telling ASP.NET Core where those routes map to.

There are two main ways to define routes:

  • Convention-based routing creates routes based on a series of conventions, defined in the ASP.NET Core Startup.cs file.
  • Attribute routing creates routes based on attributes placed on controller actions.

The two routing systems can co-exist in the same system. Let's first look at Convention-based routing.

Convention-Based Routing

In convention-based routing, you define a series of route conventions that are meant to represent all the possible routes in your system. Said definitions are located in the Startup.cs file of you ASP.NET Core project. For example, let's look what might be the simplest possible convention-based route for an ASP.NET Core MVC application:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    ...
    app.UseMvc(routes =>
    {
        routes.MapRoute(
            template: "{controller}/{action}");
    });
}

This route expects, and will map, URLs like the following:

However, what if we wanted to have more specific routes? Say, like these:

In order to get our actions to match those URLs, we need to add a few features to our convention-based routes.

Route Constraints

Let's say we want to match the following URL:

One simple way of doing this might be to define the following route:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    ...
    app.UseMvc(routes =>
    {
        routes.MapRoute(
            template: "{controller}/{action}/{id}");
    });
}

This will, in fact, match the desired route, but it will also match the following:

This probably isn't what we want. In the above URLs, the values "all", "first", and "many" will be mapped to an action's id parameter.

If we want the id to only be an integer, we could introduce a route constraint, like so:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    ...
    app.UseMvc(routes =>
    {
        routes.MapRoute(
            template: "{controller}/{action}/{id:int}");
    });
}

The {id:int} specifies that whatever is in this part of the URL must be an integer, otherwise the URL does not map to this route.

There are tons of potential route constraints you can use, including:

  • :int
  • :bool
  • :datetime
  • :decimal
  • :guid
  • :length(min,max)
  • :alpha
  • :range(min,max)

There's also several others; you can find them all on the ASP.NET Core Routing docs page.

Optional Parameters

It's also possible that we want to map the following URLs to the same action:

To do this, we can use optional parameters in our convention-based routes by adding a ? to the optional parameter's constraint, like so:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    ...
    app.UseMvc(routes =>
    {
        routes.MapRoute(
            template: "{controller}/{action}/{id:int?}");
    });
}

NOTE: You may only have one optional parameter per route, and that optional parameter must be the last parameter.

Default Values

In addition to route constraints and optional parameters, we can also specify what happens if parts of the route are not provided. Say we encounter this URL:

We want to map this URL to the controller home and action index, so we introduce default route values by defining the following route:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    ...
    app.UseMvc(routes =>
    {
        routes.MapRoute(
            template: "{controller=Home}/{action=Index}/{id:int?}");
    });
}

This will now properly route the URL to our Home/Index action. We could also map default values by using a defaults property:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    ...
    app.UseMvc(routes =>
    {
        routes.MapRoute(
            template: "{controller}/{action}/{id:int?}"),
            defaults: new { controller = "Home", action = "Index" }
    });
}

Named Routes

We can also provide a name for any given route. Naming a route allows us to have multiple routes with similar parameters but that can be used differently. The names must be unique across the project.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    ...
    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller}/{action}/{id:int?}"),
            defaults: new { controller = "Home", action = "Index" }
    });
}

Sample Routes

As always with my tutorials posts, I've included a sample project over on GitHub. In that project, we define the following convention-based routes:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    ...
    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "blogPosts",
            template: "convention/{action}/{blogID:int}/{postID:int}",
            defaults: new { controller = "Convention", action = "BlogPost" }
            );

        routes.MapRoute(
            name: "allBlogs",
            template: "convention/{action}/{blogID:int?}",
            defaults: new { controller = "Convention", action = "AllBlogsAndIndex" }
            );

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

Attribute Routing

In contrast to convention-based routing, attribute routing allows us to define which route goes to each action by using attributes on said actions. If you are just using convention-based routing, there's no need to employ attribute routing at all. However, many people (myself included) believe that attribute routing provides a better relationship between actions and routes and more fine-grained control over routes.

Here's the simplest possible use of attribute routing:

public class HomeController : Controller
{
    [HttpGet("home/index")] //We generally want to use the more specific Http[Verb] attributes over the generic Route attribute
    public IActionResult Index()
    {
        return View();
    }
}

NOTE: In ASP.NET Core MVC, the attributes [HttpGet], [HttpPost] and similar attributes can be used to assign routes.

Token Replacement

One of the things ASP.NET Core MVC does to make routing a bit more flexible is provide tokens for [area], [controller], and [action]. These tokens get replaced by their action values in the route table. For example, if we have the following controller:

[Route("[controller]")]
public class UsersController : Controller { }

If there are two actions, index and view, in this controller, their routes are now:

  • /users/index
  • /users/view

Thus, token replacement provides a short way of including the controller, area, and action names into the route.

Multiple Routes

Attribute routing, because it focuses on the individual actions, allows for multiple routes to be mapped to the same action or controller. One of the most common ways of doing this is to define default routes, like so:

public class UsersController : Controller
{
    [HttpGet("~/")]
    [HttpGet("")]
    [HttpGet("index")]
    public IActionResult Index()
    {
        return View();
    }
}

The three [HttpGet] attributes define that the action matches the following routes:

You can define as many routes as you like for each action or controller.

Mixed Routing

It is perfectly valid to use convention-based routing for some controllers and actions and attribute routing for others. However, ASP.NET Core MVC does not allow for convention-based routes and attribute routing to exist on the same action. If an action uses attribute routing, no convention-based routes can map to that action. See the ASP.NET Core docs for more info.

Opinion

If you don't need my blithe little opinion on routing, skip to the next section.

Are they gone? Good.

Attribute route all the things! Seriously, attribute routing is much more powerful than convention-based routing. Convention-based routing is good for static files and the like, but if you need to map an action to a route, use attribute routing!

Summary

In ASP.NET Core MVC, Routing is the system by which URLs get mapped to controller actions and other resources. There are two primary methods of creating routes: convention-based routing, which defines a few routes in the Startup.cs file, and attribute routing, which defines a route per action. You can mix the two styles, but if an action has an attribute route, it cannot be reached by using a convention-based route.

As always, there's a sample project over on GitHub that demonstrates many of the routing features talked about in this post. Check it out! Also, please feel free to share in the comments anything useful from the Routing features of ASP.NET Core that I didn't already cover.

This post is meant to be an introductory-level demo to the concept of Routing, while eliminating the cruft in the official Microsoft documentation. It's meant to show the most common uses, not all of them. That said, if you think I missed something that should be here, please let me know in the comments!

Happy Coding!