Understanding Routing in ASP.NET Core MVC

In ASP.NET Core MVC, a request URL is mapped to a controller’s action. This mapping happens through the routing middleware and you can do good amount of customization. There are two ways of adding routing information to your application: conventional routing and attribute routing. This article introduces you with both of these approaches with the help of an example.

To understand how conventional and attribute routing works, you will develop a simple Web application, as shown in Figure 1.

A routing form
Figure 1: A routing form

The Web page shown in Figure 1 displays a list of orders from the Orders table of the Northwind database. The launching page follows the default routing pattern: /controller/action. Every order row has Show Details links. This link, however, renders URLs of the following form:

/OrderHistory/CustomerID/order_year/order_month/order_day/OrderID

As you can see, this URL doesn’t follow the default routing pattern of ASP.NET Core MVC. These URLs are handled by a custom route that we specify in the application. Clicking the Show Details link takes the user to another page, where order details such as OrderDate, ShipDate, and ShipAddress are displayed (see Figure 2).

Details for a customer's order
Figure 2: Details for a customer’s order

Let’s develop this example and also discuss routing as we progress. The example uses Entity Framework Core for database access. Although we will discuss the EF Core code briefly, detailed discussion of EF Core is beyond the scope of this article. It is assumed that you are familiar with the basics of working with EF Core.

Okay. Create a new ASP.NET Core Web Application using Visual Studio. Because we want to use EF Core, add the NuGet package: Microsoft.EntityFrameworkCore.SqlServer. To add this package, right-click the Dependencies folder and select the “Manage NuGet Packages…” shortcut menu option. Then, search for the above-mentioned package and install it for your project.

Managing NuGet packages
Figure 3: Managing NuGet packages

Then, add a Models folder to your project root and add two class files into it: NorthwindDbContext.cs and Order.cs. Open the Order.cs file and add the following class definition to it.

[Table("Orders")]
public class Order
{
   [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
   [Required]
   public int OrderID { get; set; }
   [Required]
   public string CustomerID { get; set; }
   [Required]
   public DateTime OrderDate { get; set; }
   [Required]
   public string ShipName { get; set; }
   [Required]
   public string ShipAddress { get; set; }
   [Required]
   public string ShipCity { get; set; }
   [Required]
   public string ShipCountry { get; set; }
}

The Order class contains seven properties, namely: OrderID, CustomerID, OrderDate, ShipName, ShipAddress, ShipCity, and ShipCountry. The Order class is mapped to the Orders table by using the [Table] attribute.

Also, open the NorthwindDbContext class file and add the following code in it:

public class NorthwindDbContext : DbContext
{
   public DbSet<Order> Orders { get; set; }

   protected override void OnConfiguring
      (DbContextOptionsBuilder optionsBuilder)
   {
      optionsBuilder.UseSqlServer("data source=.;
         initial catalog = northwind;
         integrated security = true");
   }
}

The NorthwindDbContext inherits from the DbContext class. Inside, it declares Orders DbSet. The OnConfiguring() method is used to configure the database connection string. Here, for the sake of simplicity, we pass a hard-coded connection string to the UseSqlServer() method. You also could have used dependency injection. Make sure to change the database connection string as per your setup.

Conventional Routing

Now, open the Startup.cs file and go to the Configure() method. The Configure() method is used to configure middleware. Somewhere inside this method, you will find the following code:

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

});

The preceding code calls the UseMvc() method and also configures the default routing. Notice the MapRoute() call carefully. It defines a route named default and also specifies the URL template. The URL template consists of three parameters: {controller}, {action}, and {id}. The default value for the controller is Home, the default value foe action is Index, and the id is marked as an optional parameter.

If all you need is this default route, you also can use the following line of code to accomplish the same task:

app.UseMvcWithDefaultRoute();

This is the conventional way of defining routes and you can add your custom route definitions easily. Let’s add another route that meets our requirement:

app.UseMvc(routes =>
{
   routes.MapRoute(
      name: "OrderRoute",
      template: "OrderHistory/{customerid}/{year}/
         {month}/{day}/{orderid}",
      defaults: new { controller = "Order",
         action = "ShowOrderDetails" });

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

});

Notice the code marked in the bold letters. We added another route, named OrderRoute. This time, the URL template begins with static segment—OrderHistory— that won’t change with each URL. Further, the URL template contains five parameters: {customerid}/{year}/{month}/{day}/{orderid}. The default parameter sets the default controller to Order and the default action to ShowOrderDetails.

Now that you have the desired route definition in place, it’s time to create the HomeController and OrderController.

Add HomeController and OrderController to the Controllers folder by using the Add New Item dialog. The HomeController’s Index() action is shown below:

public IActionResult Index()
{
   using (NorthwindDbContext db = new
      NorthwindDbContext())
   {
      return View(db.Orders.ToList());
   }
}

The Index() action simply passes a list of all the orders to the Index view for the sake of displaying in a table.

Next, add an Index view under Views > Home folder and write the following markup in it.

@model List<RoutingInAspNetCore.Models.Order>

<html>
   <head>
      <title>List of Orders</title>
   </head>
   <body>
      <h2>List of Orders</h2>

      <table cellpadding="10" border="1">
         <tr>
            <th>Order ID</th>
            <th>Customer ID</th>
            <th>Action</th>
         </tr>
         @foreach (var item in Model)
         {
            <tr>
               <td>@item.OrderID</td>
               <td>@item.CustomerID</td>
               <td>@Html.RouteLink("Show Details",
                     "OrderRoute",new {
                  customerid=item.CustomerID,
                  year=item.OrderDate.Year,
                  month=item.OrderDate.Month,
                  day=item.OrderDate.Day,
                  orderid=item.OrderID }) </td>
            </tr>
         }
      </table>
   </body>
</html>

The Index view renders a table that lists OrderID and CustomerID of all the orders. Each order row has a link that points to the order details page. Notice how the link is rendered. We use the Html.RouteLink() helper to render this hyperlink. The first parameter of RouteLink() is the text of the hyperlink (Show Details, in this case). The second parameter is the name of the route as specified in the route definition (OrderRoute, in this case). The third parameter is an anonymous object holding all the values for the route parameters. In this case, there are five parameters: customerid, year, month, day, and orderid. The values of these parameters are picked from the corresponding Order object.

Okay. Now, open OrderController and add the ShowOrderDetails() action, as shown below:

public IActionResult ShowOrderDetails(string customerid,
   int orderid,int year,int month,int day)
{
   ViewBag.Message = $"Order #{orderid} for customer
      {customerid} on {day}/{month}/{year}";

   using (NorthwindDbContext db = new NorthwindDbContext())
   {
      Order order = db.Orders.Find(orderid);
      return View(order);
   }
}

The ShowOrderDetails() action takes five parameters corresponding to the route parameters. Inside, a message is stored in the ViewBag that indicates the OrderID, CustomerID, and OrderDate of the given order. Moreover, the code fetches the Order object from the Orders DbSet matching the OrderID. The Order object then is passed to the ShowOrderDetails view.

Now, add the ShowOrderDetails view to the Views > Order folder and add the following markup to it.

@model RoutingInAspNetCore.Models.Order

<html>
   <head>
      <title>Orders Details</title>
   </head>
   <body>
      <h2>@ViewBag.Message</h2>
      <h2>Order Details</h2>
      @Html.DisplayForModel()
   </body>
</html>

The ShowOrderDetails view simply outputs the Message ViewBag variable. All the properties of Order model object are outputted by using the DisplayForModel() helper.

This completes the application. Run the application, navigate to /Home/Index, and see whether the order listing is displayed. Then, click the Show Details link for an order and check whether order details are being shown as expected.

Attribute Routing

The previous example uses conventional routing. Now, let’s use attribute routing to accomplish the same task.

First of all, comment out the existing UseMvc() call so that conventional routing is no longer enabled. Then, add the following call at that place:

app.UseMvc();

As you can see, the UseMvc() call no longer contains routing information. Then, open HomeController and add the [Route] attribute on top of it as shown next:

[Route("[controller]/[action]/{id?}")]
public class HomeController : Controller
{
   ....
}

Now, the HomeController has been decorated with the [Route] attribute. The [Route] attribute defines the URL pattern using special tokens: [controller] and [action]. An optional id parameter also has been specified. This is equivalent to {controller}/{action}/{id} of the conventional routing.

If you want to set HomeController as the default controller and Index() as the default action, you would have done this:

public class HomeController : Controller
{
   [Route("")]
   [Route("Home")]
   [Route("Home/Index")]
   public IActionResult Index()
   {
      ....
   }
}

Here, the [Route] attribute has been added on top of the Index() action and sets the defaults as needed.

Okay. Then, open OrderController and add the [Route] attribute, as shown below:

[Route("OrderHistory/{customerid}/{year}/{month}/{day}/
   {orderid}",Name ="OrderRoute")]
public IActionResult ShowOrderDetails(string customerid,
   int orderid,int year,int month,int day)
{
   ....
}

Here, you added the [Route] attribute on top of the ShowOrderDetails() action. The [Route] attribute contains the same URL pattern as before. The name of the route also is specified by setting the Name property.

If you run the application, it should work as expected.

Route Constraints

You also can put constraints on the route parameters. Route constraints allow you to ensure that a route value meets certain criteria. For example, you may want to ensure that the CustomerID route value is a string with five characters. Or, the month value is between 1 and 12. You can add route constraints to both conventional routes as well as attribute routes. Let’s see how.

Consider the following piece of code:

routes.MapRoute(
   name: "OrderRoute",
   template: "OrderHistory/{customerid:alpha:length(5)}/
      {year:int:length(4)}/{month:int:range(1,12)}/
      {day:int:range(1,31)}/{orderid:int}",
   defaults: new { controller = "Order",
      action = "ShowOrderDetails" });
[Route("{customerid:alpha:length(5)}/{year:int:length(4)}/
   {month:int:range(1,12)}/{day:int:range(1,31)}/
   {orderid:int}",Name ="OrderRoute")]

The preceding code shows how route constraints can be applied. The above example uses the following constraints:

  • The alpha constraint is used to ensure that the value contains only alphabetical characters.
  • The length(n) constraint is used to ensure that a value must have a length equal to the specified number.
  • The int constraint is used to ensure that the value is an integer.
  • The range(a,b) constraint is used to ensure that a value falls within certain a minimum and maximum range.

There are many more constraints available. A parameter can have more than one constraint attached with it. You can read more about route constraints here.

After adding these constraints, run the application, navigate to the order details page, and then change the CustomerID in the browser’s address bar to some string more than 5 characters. Or, try changing the month to some number higher than 12. You will find that the request is rejected and doesn’t reach the ShowOrderDetails() action.

Conclusion

This article examined how conventional and attribute routing of ASP.NET Core MVC work. You may learn more about routing here. The complete source code of the example we discussed in this article is also available for download.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read