ASP.net MVC Core Routing

Routing in ASP.net MVC solves the problem of mapping a request to a route handler that can return a file, web page, or data. Out of the box, configuring routing is straightforward, but as an application becomes more complex, routing can be challenging. Knowing how routing works, and how to use the different types of routes will help you solve the problems that can arise.

What exactly are routes?

Routes define how requests are should be handled in your application. Each route has three parts that you must configure for them to work.

• Name – Identifies the route.
• Template – A pattern that is used to match to URLs.
• Target – A handler to specify where the request should go.

There are two different ways to define a route with these pieces of information. With conventional routing, you generally establish a standard pattern for mapping a URL to actions in controllers.

Routes.MapRoute(“Default”,
                “{controller}/{action}/{id}”,
                New { controller = “Home”, action = “Index”, id = “” });

In this example, the route is named “Default”, the template is a pattern of the controller name, followed by the action method in the controller, and a variable being passed into the method. Defining a pattern like this allows you to add more controllers and actions to your application, without having to map a URL each time.
Routes can also be defined using the [Route] attribute. You can simply decorate a controller action with the attribute.

[Route(“products/{id}”)]
public IActionResult ProductDetail(int id) { … }

In this case, the name is going to be generated for us, the template will be “products” followed by a variable, and the target will be the action that is decorated. If you are unfamiliar with routing, the “{controller}” or “{id}” might throw you. It is a segment, or parameter, that represents a piece of data that will be parsed from the URL.

How do the requests get applied to the routes?
Each application has a collection of routes. When an incoming request needs to be matched with a route handler, the RouteAsync() method is sequentially called on each route handler in the collection. If the route sets the handler for the request, iteration through the collection is stopped, and the handler is invoked. If a handler is not found, it is passed on the next piece of middleware in the request pipeline. But that’s a story for another day.

Now let’s take a look at some interesting cases.
Let’s say we wanted to have a controller that could display a product when you gave the website the name of the product, or an id number.

GetProductByID(int id) // product/4
GetProductByName(string name) //product/silly-puddy

In this case, we need to see what type of data is being included in the URL. If it is an integer, we want the GetProductByID action to be invoked. If it is a string, we want the GetProductByName action to be invoked. One way to do that is to use a constrained route parameter.

[Route(“product/{productid:int}”)]
public IActionResult GetProductByID(int id)

[Route(“product/{name:string}”)]
public IActionResult GetProductByName(string name)

Microsoft has created several types of route constraints that can be used to evaluate parameters in a URL. A route constraint matcher will iterate through the list of constraints, and call a match function to see if a given URL should be handled by the route. Constraints that check for integers and strings are commonly used, as well as regular expressions for more complex requirements. If you want to see all the available constraints, check out the GitHub repo for ASP.NET.
https://github.com/aspnet/Routing/tree/dev/src/Microsoft.AspNetCore.Routing/Constraints

Sometimes you need a handler for URLs with many segments. One example you need a specific style of webpage for widgets with special features.

[Route(“product/widgets/{*feature}”)]
public IActionResult GetSpecialWidget(string feature)

A wild card parameter will act as a “catch-all” for URLs that may contain multiple segments.
/product/widgets/jumbo/green
/product/widgets/tiny
/product/widget/blue

The wild card parameter can be constrained in the same way normal parameters are.

[Route(“product/widgets/{*feature:string}")]

 

Don’t forget about order!

Remember back when I mentioned that for each application, there is a collection of routes that are checked one by one, until a matching route is found. That means that the order of the routes in the collection matters. If a route with a wildcard parameter was first in the collection, it could swallow up requests that were intended for different handlers. In ASP.net there is a TreeRouteBuilder that is responsible for putting the routes in order. Here is the order by route type.

1) Literal Routes /product/brand-new
2) Constrained Routes with Parameters /product/{productid:int}
3) Unconstrained Routes with Parameters /product/{productname}
4) Constrained Routes with Wildcards /product/{*widgetsize:int}
5) Unconstrained Routes with Wildcards /product/{*feature}

In the event you need to have a route placed higher in the list, you can add an Order parameter to the route definition. All routes have an Order with a default value of zero. Setting the route’s order to a value less than zero will cause it to be checked before those set to zero. Setting the value to 1 will push the route to the end of the collection, and thus be checked later in the matching process.

[Route(“product”, Order=1)]

Hope this helps shed some light on how routing works, and the ways you can use routing features to better control your web applications.