ASP.NET vNext Routing Overview

ASP.NET vNext Routing Overview

Rate This
  • Comments 8

Introduction

The ASP.NET Routing system is primarily responsible for two operations:

  1. It maps incoming HTTP requests to a route handler given a collection of routes.
  2. It generates URLs (links) from these routes. 

A typical route definition is a string that contains a URL template to match, such as:

"blog/{year}-{month}-{day}/{slug}"

With the URL template, the route definition can also contain default values and constraints for parts of the URL. This helps define exactly which URLs the route can match, for example:

routes.MapRoute(
    name: "Product", 
    template: "Product/{productId}", 
    defaults: new { controller = "Product", action = "Details" }, 
    constraints: new { productId = @"\d+" });

The first argument "Product", is the name of the route. The second argument "Product/{productId}", is the URL template for this route. The URL must be of the form “Products/n”, where n is the productID. The 3rd argument contains the default route values. In this example, the default controller is ProductController. 

When a route matches a request, route data is generated and passed to the route handler. The route data contains a set of key value pairs typically representing the matched segments and default data. In an MVC application the matched segments are used to select a controller and action and execute it.

The ASP.NET Routing system can use the same templates to generate URLs as well. 

History

Originally, ASP.NET Routing was written as integrated feature of ASP.NET MVC (in pre-v1.0-beta days of ASP.NET MVC). Later it was extracted in its own assembly (System.Web.Routing) and it was made part of the .NET Framework. This more general routing system supports not only MVC, but also WebForms with page routes.

When ASP.NET Web API was introduced there was a need for it to be hostable outside of the ASP.NET pipeline in IIS. In that scenario Web API cannot use ASP.NET routing. As a result, Web API was built using a routing façade, which can delegate to its own version of routing. When Web API is hosted on ASP.NET in IIS with the WebHost NuGet package the Web API routing façade delegates to ASP.NET’s built-in routing system.

What’s new in vNext

In ASP.NET vNext the routing systems have been unified to a single implementation that is used when hosting in IIS, self-hosting, and for in-memory local requests for unit testing. Routing in ASP.NET vNext resides in the Microsoft.AspNet.Routing NuGet package. The source repository is located here: https://github.com/aspnet/Routing. Similarly MVC and Web API have been unified into a new MVC framework.

There are several major improvements to routing in ASP.NET vNext.

Rerouting the request

To understand this new feature, it’s important to understand that routing and action selection are two separate systems.

  • The routing system’s responsibility is to find a matching route, create route data, and dispatch the request to a handler.
  • Action selection is an implementation detail of the MVC’s handler. It uses route data and other information from the incoming request to select the action to execute. In an MVC application, MvcRouteHandler is the handler the request gets dispatched to.

In MVC 5 and Web API 2 there is no way for the handler to return control to the routing system if no action could be matched. In this scenario MVC and Web API will typically return an “HTTP 404 - Not Found” response. That meant that a developer had to be careful about the order of the routes, and constraints had to be crafted carefully to make sure incoming URLs reached the desired action.

In ASP.NET vNext there are two improvements to this behavior:

  1. The handler can return control to the routing system indicating that it cannot handle the request. In that case the routing system will try and match the next route in the route collection. This allows for a more flexible ordering of routes and reduces the need to add constraints to each route.
  2. Before dispatching the request to the handler, the routing system can use dependency injected constraints, so it can in effect look into what actions are available down stream at the time of matching.

Routing as middleware

Since ASP.NET MVC and WebAPI’s release, the ASP.NET framework is a family of pluggable components rather than a single framework.

In ASP.NET vNext, Routing is built as a middleware component that can be added to the request pipeline. It can be composed along with any other middleware components, such as the static file handler, error page, or the SignalR server.

Of course just like in MVC 5, a developer can hook up handlers other than MVC’s MvcRouteHandler.

The request flow through the routing pipeline

This section brings together everything that you’ve read so far:

  1. When a request is processed by ASP.NET vNext, the routing middleware will try to match the request with routes in the route collection.
  2. If one of the routes matches the request, it looks for the handler for the route.
  3. The RouteAsync method of the handler is called.
  4. The RoutingContext has a flag called IsHandled. If that is set to true, it means the request was successfully handled by the handler. If it is set to false, it means the route wasn’t able to handle the request, and that the next route should be tried.

Inline constraints

Route constraints let developers restrict how the parameters in the route template are matched. For example, a regex constraint requires that a regular expression match the value in order for the route to match. In MVC 5, the constraint syntax looks like this:

routes.MapRoute(
    "Product", 
    "Product/{productId}", 
    defaults: new { controller = "Product", action = "Details" }, 
    constraints: new { productId = @"\d+" });

In this code sample the productId parameter has a regex constraint that matches an integer of one or more digits (0-9). This style of defining routes and constraints is called “convention-based routing”. In MVC 5 and Web API 2 we introduced a new style of constraints and defaults for attribute routing scenarios, called inline constraints. In ASP.NET vNext, we are introducing a similar syntax for defining route constraints for convention-based routing. It simplifies specifying the constraints with an inline syntax in the form of: 

"{parameter:constraint}"

A developer can now apply a constraint in a more succinct way as part of the URL template, Here are a few approaches. The full list of constraints that can be applied will be identical to the Web API 2 attribute routing constraints.

Constraint

Description

Example template

alpha

Matches uppercase or lowercase Latin alphabet characters (a-z, A-Z)

"Product/{ProductName:alpha}"

int

Matches a Signed 32-bit integer value.

"Product/{ProductId:int}"

long

Matches a Signed 64-bit integer value.

"Product/{ProductId:long}"

minlength

Matches a string with a minimum length.

"Product/{ProductName:minlength(10)}"

Regex

Matches a regular expression.

"Product/{productId:regex(^\\d{4}$)}"

Optional URI Parameters and Default Values

A URI parameter can be made optional by adding a question mark to the route parameter. The example specifies that the productId parameter needs to satisfy the built-in “long” constraint.  This example also shows that the parameter is optional, as denoted by the “?” token.

routes.MapRoute(
    "Product", 
    "Product/{productId:long?}", 
    new { controller = "Product", action = "Details" });

You can specify a default value inside the route template as follows. This example shows that the productId is optional and default value is 1.

routes.MapRoute(
    "Product", 
    "Product/{productId:long=1}", 
    new { controller = "Product", action = "Details" });

Looking forward

For the beta release of ASP.NET vNext the following features are planned. These features should be available in the nightly builds as they become available.

  1. Inline constraints for convention-based routing, as described in the details above.
  2. Attribute based routing with inline constraints
    The Web API 2 already has this feature, and it will soon be available for ASP.NET vNext. 

There are a lot of improvements done and planned for Routing for ASP.NET vNext. Your feedback will be very valuable. Please provide your feedback on these features and feel free to let us know what you’d like to see that you don’t see here – we’ll definitely consider it! You can provide feedback in comments on this blog or on GitHub (https://github.com/aspnet/Routing). If you ask a question in Stack Overflow, use the asp.net-vnext tag.

Leave a Comment
  • Please add 7 and 4 and type the answer here:
  • Post
  • Looks similar to hapi.js

  • Hi Mugdha, thanks for the detailed explanation. I have a question that might also be a feature request.

    When generating a URL based on a route, there seems to be no way to change the built-in behavior. Let's say for example that I want to add a query string parameter to *every* URL that is generated. So Url.Action("ProductsController", "List") with a route "{controller}/{action}"  would result in /products/list?extra=42. Or let's say I wanted to add a language tag in front of alle URLs, the the result would be /en/products/list.

    When we consider this type of URL modification a cross-cutting concern (so we don't want to add it to every route and then have to add route data to every call to Url.Action()) then there is no extension point in the current routing infrastructure that I am aware of. Is that correct?

    If so, could this be considered for ASP.NET vNext?

    Thanks!

  • Thanks for the write-up.

    The largest difficulty I have with the current routing infrastructure is to properly support a RESTful interface based on HTTP verbs. If I have have - for example - a product number 17, there is no easy way to route GET /products/17 differently from DELETE /products/17, POST /products/17 or PUT /products/17. From my perspective the HTTP verb is part of the route. While it could be argued the 17 should be part of the entity for PUT or POST requests - or omitted entirely for PUT requests - it's still all but impossible to handle POST and PUT differently, or handle GET and DELETE requests differently.

    To a certain extend the same goes for accept headers (do I return Json or HTML?), though it can easily be argued that the responsibility for this lies with the controller, and improvements should be made there (i.e. rather than have an "action" return an "ActionResult", have it return an IDictionary<MimeType, Task<ActionResult>>, and have the framework complete the request with the ActionResult with the highest q value. But that's something for another team I suppose.

    I haven't yet looked in to replacing the MvcRouteHandler by something else, or injecting my own IControllerFactory, partially because it greatly increases the complexity of a project. In this way I feel MVC routing encourages me to do the wrong thing through the default implementation

  • Nice post

  • @Martijn I'm not sure I'm following your issue.

    It is completely possible to handle POST and PUT requests separately you can simply decorate your actions with [HttpPost] and [HttpPut] and the request will be routed accordingly. You don't need to do anything at the routing level. In this release you can even separate them to different controllers by separating the namespace (not recommended :) but completely doable). This is also completely supported today (MVC5 & Web API 2) using attribute routing, and attribute routing is again coming shortly to the framework.

    In regards to the accept header scenario, we are in the process of adding this feature (content negotiation), that will allow you to return an object (and have it content negotiated), or return an ObjectResult (name not final) that basically does something similar to to your suggestion.

  • @Mike: Sorry for the delay in response. To do this in vNext, you need to implement IUrlHelper. You can then implement your own URL modification. If you want to keep most UrlHelper's (product implementation of IUrlHelper) functionality, you can wrap an instance of it, and just change what you need. Here is an example.

    public class MyUrlHelper : IUrlHelper

      {

          private readonly UrlHelper _helper;

          public MyUrlHelper(IContextAccessor<ActionContext> contextAccessor, IActionSelector actionSelector)

          {

              _helper = new UrlHelper(contextAccessor, actionSelector);

          }

          public string Content(string contentPath)

          {

               return UpdateUrl(_helper.Content(contentPath));

          }

          public bool IsLocalUrl(string url)

          {

               return _helper.IsLocalUrl(url);

          }

          public string RouteUrl(string routeName, object values, string protocol, string host, string fragment)

          {

              return UpdateUrl(_helper.RouteUrl(routeName, values, protocol, host, fragment));

          }

          string IUrlHelper.Action(string action, string controller, object values, string protocol, string host, string fragment)

          {

              return UpdateUrl(_helper.Action(action, controller, values, protocol, host, fragment));

          }

          public string UpdateUrl(string url)

          {

              // Put your changes here.

              return "/" + url;

          }

      }

    Alternatively, you can extend UrlHelper which is not very clean right now as the methods are not virtual. You need to use Explicit Interface Implementation. We will look into making those virtual if this turns out to be common enough scenario.

  • @Yishai G

    If I want a different action for a put and post action with the same argument, I can't have a controller class with two methods with the same name and the same argument(s), but different filter attributes, it simply doesn't compile. So if I want to distinguish between the two, but want to have them to have the same URL, I have to make the distinction in the route. But I can't make the distinction in the route, because I can't differentiate on the request method in the route. Am I making sense yet?

    It's great to hear that automatic content negotiation is in the works by the way! If something like this would land, I think it would be important to make it fairly easy to provide custom marshallers for the negotiation to to use if you need/want something custom.

  • Good work! ;)

Page 1 of 1 (8 items)