ASP.NET Core 1.0 - Routing - Under the hood

Routing was introduced to .NET with the release of ASP.NET MVC 1.0 back in 2009. Routing is the process of taking an input URL, and mapping it to a route handler. This integrated into the ASP.NET pipeline as an IHttpModule - the UrlRoutingModule.

Current ASP.Net Routing

Let's remind ourselves about how the ASP.NET 4.x pipeline works:

ASP.NET Pipeline

Routing integrated with the pipeline as an IHttpModule, and when a route was resolved, it would bypass the rest of the pipeline and delegate to the final IHttpHandler through a new factory-type interface, the IRouteHandler:

public interface IRouteHandler  
{
    IHttpHandler GetHttpHandler(RequestContext context);
}

It was through this IRouteHandler that MVC integrated with Routing, and this is important, because generally MVC-style URLs are extensionless, so the routing system enabled these types of URLs to be mapped to a specific IHttpHandler, and in the case of MVC, this means mapping to the MvcHandler which was the entry point for controller/action execution. This means we didn't need to express a whole host of <httpHandler> rules for each unique route in our web.config file.

MVC integration with Routing in ASP.NET Pipeline

Mapping Routes

The MVC integration provided the MapRoute methods as extensions for a RouteCollection. Each Route instance provides a Handler property - which by default it set to the MvcRouteHandler (through the IRouteHandler abstraction):

routes.MapRoute(  
    "Default", 
    "{controller}/{action}/{id}", 
    new { controller = "Home", action = "Index", Id = UrlParameter.Optional }); 

This method call creates a Route instance with the MvcRouteHandler set. You could always override that if you wanted to do something slightly more bespoke:

var route = routes.MapRoute(  
    "Default", 
    "{controller}/{action}/{id}", 
    new { controller = "Home", action = "Index", Id = UrlParameter.Optional }); 

route.Handler = new MyCustomHandler();  

The Routing table

In current Routing (System.Web.Routing) routes are registered into a RouteCollection which forms a linear collection of all possible routes. When routes are being processed against an incoming URL, they form a top-down queue, where the first Route that matches, wins. For ASP.NET 4.x there can only be one route collection for your application, so all of your routing requirements had to be fulfilled by this instance.

ASP.Net Core 1.0 Routing

How has routing changed for ASP.Net Core 1.0? Quite significantly really, but they had managed to maintain a very familiar shape of API, but it is now integrated into the ASP.NET Core middleware pipeline.

The new Routing framework is based around the concept of an IRouter:

public interface IRouter  
{
    Task RouteAsync(RouteContext context);

    VirtualPathData GetVirtualPath(VirtualPathContext context);
}

An instance of RouterMiddleware can be created using any instance of IRouter. You can think of this as the root of a routing tree. Routers can be implemented any which way, and can be plugged directly into the pipeline using this middleware. To handle the classical routing collection (or route table), we now have an IRouteCollection abstraction, itself extending IRouter. This means that any RouteCollection instance acts as a router and can be used directly with the middleware:

ASP.NET Core Routing Middleware

This is how MVC hooks into the pipeline. When you call app.UseMvc(r => { }) and configure your routes, you're actually using a new IRouteBuilder abstraction which is used to build a router instance:

public interface IRouteBuilder  
{
    IRouter DefaultHandler { get; set; }

    IServiceProvider ServiceProvider { get; }

    IList<IRouter> Routes { get; }

    IRouter Build();
}

For MVC, the DefaultHandler property is an instance of MvcRouteHandler, and this does the work of selecting an action to execute.

The MapRoute methods are now provided as extensions of IRouteBuilder and they work by creating new routes and adding them to the builder's Routes collection. When the final Build call is executed, the standard RouteBuilder creates an instance of RouteCollection, which acts as our router for our middleware instance.

Remembering this is important if you are migrating from an ASP.NET 4.x application to ASP.NET Core and you've invested heavily on tweaking the Routing framework to suit your needs.

A quick note on Attribute Routing

Attribute Routing is a feature of MVC and not directly tied to the Routing framework. Because MVC creates its own router, it can control at what point it integrates Attribute routing. It does this during the call to UseMvc(r => { }) by injecting a single instance of AttributeRoute at the start of the route collection after all other routes have been configured.

This single instance of AttributeRoute acts as the router for handling Controller and Action-level attribute routes, using the application model as the source of truth.

Making decisions

Using the standard MapRoute method you end up with an instance of a TemplateRoute which works on a route template string, such as {controller}/{action}/{id?}. When the RouteAsync method is evaluating for this type, it does so by checking to see if the incoming request path matches the route template. If this is true, it then checks against any applied constraints to determine if the route values lifted from the incoming request path are valid. If either of these steps returns false, then the route does not match and control is returned back to the route collection to test the next route. This is very similar to how conventional working currently operates for ASP.NET 4.x.

Finishing up

Hopefully you can appreciate this run through of the under-the-hood changes made to the Routing framework. This newer Routing framework offers up greater flexibility in composing our applications because of the integration with the middelware pipeline.

It's worth having a look at the GitHub repo code for yourself.

comments powered by Disqus