Now that we've discussed some basics of routing in ASP.NET Core 3.0, we can move on to talking about convention-based routing in MVC.

"Convention-based" routing is a form of routing in which a small group of routes are defined and evaluated against URLs for matches. We define these routes in our Startup.cs file, and any request URL is evaluated to see if it matches any defined route. If such a match is found, the request is directed to the correct controller and action.

Pointing As You
I want YOU... to map to that route over there. Photo by Adi Goldstein / Unsplash

Let's see how we can use convention-based routing in our ASP.NET Core 3.0 MVC apps!

Sample Project

As will all my code-based posts, there is a sample project on GitHub. Check it out!

exceptionnotfound/RoutingAspNetCoreDemo
Contribute to exceptionnotfound/RoutingAspNetCoreDemo development by creating an account on GitHub.

You can use it to follow along with my examples, though it isn't absolutely necessary as I have included the needed code in this post.  

Setup

In order to use convention-based routing, we must do two things in our Startup.cs class.

First, we must enable the service layer calls for controllers and views, like so:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
}

The AddControllersWithViews() method is new in ASP.NET Core 3.0, and adds MVC controller and views to the service layer so that we might use Dependency Injection (DI) for them. Note that it does not add services for Razor Pages or SignalR.

Second, as mentioned in the previous post in this series, we need to call UseEndpoints():

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...
    app.UseEndpoints(endpoints =>
    {
        //Route definitions go here
    });
}

Within the call to UseEndpoints() we will establish the convention-based routes that our app will answer to.

An Example Route Definition

Now that convention-based routing has been enabled, let's take a look at an example route and show how that route might be matched.  Here's a route definition from the sample project:

endpoints.MapControllerRoute("account", 
                             "account/{id?}",
                             defaults: new { controller = "Account", action = "Index" });

This route has three parameters.

First is the route name, which is "account" in this case. This is a shorthand way of referring to this particular route, which we will use later when creating links for this route.

Second is the route pattern, which is the expected pattern of a URL that will match this route. In this case, the URL will be <root>/account/{id}, where the ID parameter is optional (as designated by the ? symbol).

Finally, we have the defaults. These are the controller and action that this route will map incoming requests to. In our case, the controller is Account and the action is Index.

Speaking of, let's look at that action:

public class AccountController : Controller
{
    public IActionResult Index(int id = 0)
    {
        AccountVM model = new AccountVM()
        {
            ID = id
        };
        return View(model);
    }
}

This action that takes the optional ID parameter (which will be 0 if another value is not supplied by the URL) and returns a ViewModel.  

So, if the following URL is read:

<root>/account/7

The route middleware will map that URL to the AccountController`` Index()``` action, with an ID value of 7.

Route Evaluation Order

When a request URL is intercepted by the routing middleware, it evaluates the URL against the routes that have been defined. Said evaluation occurs against the routes in the order they were defined.  

For example, say we have the following routes:

endpoints.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

endpoints.MapControllerRoute(
    name: "user",
    pattern: "User/Index/{id?}",
    defaults: new {controller = "User", action = "Details"} );

Say we also have this incoming URL:

/user/index/7

With this URL, the second route will never be matched, because this URL matches the first route and that one will be evaluated first. Consequently, a URL of "/user/index/7" will never be redirected to the appropriate controller and action.

Because of this, you should define your routes in an order from most-specific to least-specific, possibly ending with a "catchall" route that is the basic route for your application. In this way, you can avoid situations like the (admittedly contrived) one above.

Multiple Parameters in Routes

Let's say we need multiple parameters passed to an action. In fact, let's say we have the following action:

public class HomeController : Controller
{
    public IActionResult Parameters(int level, string type, int id)
    {
        ParametersVM model = new ParametersVM()
        {
            Level = level,
            Type = type,
            ID = id
        };

        return View(model);
    }
}

We could use the a default route for this action, where the parameters are found in the query string of the URL. Said default route would look like this:

endpoints.MapControllerRoute(
    name: "default",
    pattern: "{controller}/{action}");

Which means that our URLs would look like this:

/home/parameters?level=2&type=default&id=81

This works perfectly fine! But what if we expect our action parameters to exist as part of the URL? Further, let's shorten the expected URL by removing the "/home" part. The URL we now expect looks like this:

/parameters/2/default/81

This is much cleaner, and depending on your usage may be much easier for your users to read and understand. To match this URL, we need a new route, and that route looks like the following:

endpoints.MapControllerRoute("parameters", 
                             "parameters/{level}/{type}/{id}",
                             defaults: new { controller = "Home", action = "Parameters" });

The pattern for this route, "parameters/{level}/{type}/{id}" is looking for the value "parameters" in the first segment followed by three more segments, which be mapped to level, type, and ID respectively. If only two segments exist, this route will not be matched.  

Mapping Routes to Areas

Areas are a feature of ASP.NET Core 3.0 that allow for "splitting" your apps in to different, dedicated sections, each with their own controllers, views, or Razor Pages. For example, you might have a "Blog" area, which would be located in the folder /Areas/Blog in your project, and that area would have its own controllers and views.  

Say we have the following controller in the Blog area:

[Area("blog")]
public class PostController : Controller
{
    public IActionResult Index(int id)
    {
        return View();
    }

    public IActionResult Article(string title)
    {
        return View();
    }
}

We want to specify routes that will map to these actions, including their parameters.

In ASP.NET Core 3.0, we need to specifically call out routes that map to areas. Taking the "blog" area, we can make a sample route by calling MapAreaControllerRoute()

endpoints.MapAreaControllerRoute(name: "blog",
                                 areaName: "blog",
                                 pattern: "blog/{controller=Post}/{action=Index}/{id?}");

This route will match the Index() action in the controller. Note that this method wants us to explicitly specify the area name, because the route middleware needs to know what area to place matched URLs into.

Now let's also define a route for the Article() action.

endpoints.MapAreaControllerRoute(name: "article",
                                 areaName: "blog",
                                 pattern: "blog/{controller=Post}/{action=Article}/{**title}");

Note the "**" syntax on the title segment. This syntax says that anything can be in that segment and will be mapped to the title parameter.  

Ambiguous Routes

It is possible to have a single URL match multiple defined routes. Let's now say we have the following controller:

public class AccountController : Controller
{
    public IActionResult IndexAmbiguous(int id = 0)
    {
        AccountVM model = new AccountVM()
        {
            ID = id
        };
        return View("Index", model);
    }

    public IActionResult IndexAmbiguous(string id = "0")
    {
        AccountVM model = new AccountVM()
        {
            ID = int.Parse(id)
        };
        return View("Index", model);
    }
}

These actions are distinct actions in C# because they have different parameters. However, it will be difficult to match them using the standard convention route.  

Say our system encounters the following URL:

/account/indexambiguous/5

We know from reading the URL that we most likely mean to match the first action, the one with the integer parameter. However, the routing system in ASP.NET Core 3.0 cannot make that determination, and will throw an AmbiguousActionException when attempting to map this URL.

We can solve this problem in several ways, including renaming the actions, using Route Constraints, or specifying different, designated routes for each of these actions.

Example Route Matching

For our final example, let's see all the routes we defined in the sample project and show how many URLs will map to them. Here's all the routes defined:

endpoints.MapAreaControllerRoute(name: "blog",
                                 areaName: "blog",
                                 pattern: "blog/{controller=Post}/{action=Index}/{id?}");

endpoints.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

endpoints.MapAreaControllerRoute(name: "article",
                                 areaName: "blog",
                                 pattern: "blog/{controller=Post}/{action=Article}/{**title}");

endpoints.MapControllerRoute("account", "account/{id?}",
                             defaults: new { controller = "Account", action = "Index" });

endpoints.MapControllerRoute("user", "user/{id?}",
                             defaults: new { controller = "User", action = "Index" });

endpoints.MapControllerRoute("parameters", "parameters/{level}/{type}/{id}",
                             defaults: new { controller = "Home", action = "Parameters" });

Given these routes and in this order, let's see how some URLs would be matched.

Example #1: Simple Area

/blog/post/index/7

Matched. This route matches the first convention route exactly, meaning it would be mapped to the Blog area, Post controller, Index action, with ID 7.

Example #2: User

/user/19

Matched. This matches the "user" route, so it would be mapped to User controller, Index action, with ID 19.

Example #3: Blog Index

/blog/post/this-is-a-sample-post

Not matched. This URL does not match any of the defined routes, and so ASP.NET Core 3.0 will return 404 Not Found.

Example #4: Parameter Mismatch

/parameters/final/7/18

Matched, but with an issue. This is a tricky one. This will, in fact, match the "parameters" route, because it has the specified three segments.  But ASP.NET Core will not be able to map "final" to an integer value for the level parameter, so instead it will ignore that segment and map the default value of the type (0 for integers) to the parameter.

Note that "7" is a valid string value, and will be correctly mapped to the type parameter.

Example #5: Blog Post without Title

/blog/post/article

Matched, and that is a problem. Which article are we referring to? Unless this is some kind of index page, we have no way to specify what article to display to the user. We would need to do some refactoring (either including the article identifier in the URL or having another way to access it, or changing the route entirely) to achieve what we intend.

Summary

Convention-based routing in ASP.NET Core 3.0 allows us to define a set of routes that "match" request URLs and their segments to appropriate controllers, actions, and parameters. This system is flexible, allowing for many uses and many routes.

Remember that:

  • We must enable routes in Startup.cs by using AddControllersWithViews() and UseEndpoints().
  • Routes are evaluated in the order they are defined. Define your routes in a most-specific to least-specific order.
  • You can define routes with default actions, controllers, and parameters.
  • You must explicitly call out routes to areas.

Don't forget to check out the sample project on GitHub!

Happy Routing!