In this series of two posts I’ll cover routing on ASP.NET 5, starting from the core components and then going into how MVC fits on the routing system.
EDIT 07/2016: while most off this post still applies to the release of ASP.NET Core 1.0, there were some changes to keep in mind while reading it:
- AspNet -> AspNetCore namespace renames.
- The TemplateRoute class is now Route.
- An IRouter that can handle a request now sets the resulting RequestDelegate on RouteContext instead of setting the IsHandled flag as mentioned below. The invocation of the request delegate is now centralized on the RouterMiddleware.
On previous versions of ASP.NET, routing started as a feature of MVC and was later extracted to a dedicated assembly (System.Web.Routing) and integrated on .NET. When Web API was released it brought the need of hosting outside of IIS, which required a dedicated routing system (actually a facade that could delegate on ASP.NET routing when hosting on IIS).
Similar to how MVC and Web API are unified, there’s now a single unified routing system, which includes features previously found on both routing systems and an improved syntax for both convention- and attribute-based routes; constraints, optional parameters and default values can all be specified inline on the route template. In addition, a route handler can now return control to the routing system indicating that it didn’t handle the request, allowing the following routes/handlers to be attempted. This didn’t happen on previous versions: if, for instance, an MVC route template was matched but couldn’t actually be resolved (e.g. no controller with the attempted name) an HTTP not found was immediately returned.
As you might expect by now, ASP.NET 5 routing is implemented as a middleware that can be included on the request pipeline: the RouterMiddleware. To include it on the pipeline one can use the UseRouter extension method depicted below:
public static IApplicationBuilder UseRouter(this IApplicationBuilder builder, IRouter router) { return builder.UseMiddleware<RouterMiddleware>(router); }
This method takes an IRouter, whose definition is similar to the request delegate, since it takes a context and returns a task that represents the request completion:
public interface IRouter { Task RouteAsync(RouteContext context); // ... }
However, the RouteContext includes a bit more information, namely the RouteData and the IsHandled flag. The RouteData contains things such as the values extracted from the route template and the available routers; the IsHandled flag is used by the routers to signal if they were able to handle a request.
public class RouteContext { public HttpContext HttpContext { get; private set; } public bool IsHandled { get; set; } public RouteData RouteData { ... } }
These are the very basic elements of the routing system, which allow to build the RouterMiddleware:
public class RouterMiddleware { private readonly ILogger _logger; private readonly RequestDelegate _next; private readonly IRouter _router; // ... public async Task Invoke(HttpContext httpContext) { var context = new RouteContext(httpContext); context.RouteData.Routers.Add(_router); await _router.RouteAsync(context); if (!context.IsHandled) { _logger.LogVerbose("Request did not match any routes."); await _next.Invoke(httpContext); } }
ASP.NET includes some implementations of IRouter that are the base for application routes. One of them is TemplateRoute, which pairs a route template with another IRouter (the target); if the route template matches the current request, the route values are extracted from the template parameters and the request is forwarded to the target router.
public class TemplateRoute : INamedRouter { public TemplateRoute( IRouter target, string routeTemplate, IInlineConstraintResolver inlineConstraintResolver) // ... }
To illustrate the direct usage of this class, lets first define a custom hello world IRouter that extracts the name to greet from a route value named “name”.
public class SayHelloFromRouteDataRouter : IRouter { public async Task RouteAsync(RouteContext context) { object name; if(context.RouteData.Values.TryGetValue("name", out name)) { await context.HttpContext.Response.WriteAsync(String.Format("Hello {0} from ASP.NET 5 world!", name)); context.IsHandled = true; } } // ... }
If the name route value is present, the handler writes the response and marks the route context as handled; otherwise, it returns control to the parent router for further processing. We can then define a route template and include our handler on the pipeline by combining it with a TemplateRoute on the Startup class:
app.UseRouter( new TemplateRoute( new SayHelloFromRouteDataRouter(), "hello/{name:alpha}", app.ApplicationServices.GetService<IInlineConstraintResolver>() ) );
Note the usage of an inline constraint on the route parameter. If we run the app we can see the expected output on the browser.
If we use an URL that doesn’t match the route (e.g. use only numbers on the name segment) we’ll get an HTTP not found or the result of another middleware on the pipeline that handles the request.
Another built in router is RouteCollection, which takes an ordered list of routers and returns when one of them handles the request. The following code excerpt illustrates the usage of a route collection; I also added a default value for the name parameter as an example of default values.
var routes = new RouteCollection(); routes.Add(new TemplateRoute( new SayHelloFromRouteDataRouter(), "hello/{name:alpha=JohnDoe}", app.ApplicationServices.GetService<IInlineConstraintResolver>() )); routes.Add(new TemplateRoute( new SayGoodbyeRouter(), "bye", null )); app.UseRouter(routes);
And that’s it for this part. On the following post I’ll detail how MVC leverages the routing building blocks to route requests to controller/actions.