ASP.NET 5 Routing – Part I

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.

image

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.

Advertisement

Custom error & not found pages on ASP.NET MVC & IIS

Some (bad) things in ASP.NET come back from time to time and custom error pages are one of them. A lot has been written on this topic – specifically when using MVC – but I found myself again struggling to get it right. However, I finally got a solution that is a good trade-off. My requirements are similar to everyone’s:

  • Preserve request URL.
  • Return proper status code (namely 404 and 500) and content-type.
  • Cover IIS errors, since it’s likely that unmanaged stuff is is place (e.g. static file handler).
Before going into the details, here’s the solution:
<system.webServer>
<httpErrors defaultResponseMode="File" existingResponse="Replace" errorMode="Custom">
<remove statusCode="404"/>
<error statusCode="404" path="404.html" />
<remove statusCode="500"/>
<error statusCode="500" path="500.html" />
</httpErrors>
</system.webServer>

You might need to unlock some attributes on the httpErrors section at machine level. Also, I set errorMode to Custom to always get the custom error pages when testing. In production you’d likely set it to DetailedLocalOnly.

How does it work and what is the trade-off?

The trade-off is between the amount of code/configuration and the flexibility of the resulting error pages. In my solution there’s nothing else to configure or code, but you must use static error pages. This may not give you all the flexibility and duplicates a bit of HTML (some of the site’s layout, assuming that the error pages will look the same), but covers all the error cases. How? The trick is on two of the attributes:

  • defaultResponseMode=”File” – Ensures that the response content-type is correct and also preserves the status code.
  • existingResponse=”Replace” – Ensures that any existing response is always replaced by the one configured on httpErrors. This is important because sometimes ASP.NET already produces a response (e.g. not found pages). This option is not usually mentioned on community answers.

So, it doesn’t matter what happened on the server, this configuration will always kick in.

Other solutions

This post is a commonly referenced resource (even by MSFT). It uses a static-ish page approach which configures both ASP.NET custom errors and IIS errors. This works fine, but has more configuration and more code and duplicated HTML.

There’s also a NuGet package to handle 404 on MVC. It wires-up on different MVC extensibility points to catch all the not found situations. You could complement this with the built in HandleErrorAttribute, but it leaves out errors than happen on ASP.NET prior to MVC and IIS errors (e.g. file not found).

If you need to generate the error pages dynamically (using a controller/view), this question on Stackoverflow discusses different alternatives. Note that when you do this, you might get errors when trying to generate the custom error page. Inception! Also, depending on which error configuration section you use, some errors may be left out.

Conclusion

IIS error pages seem to be the less cumbersome solution for custom error pages. If you’re willing to have static HTML pages, it’s probably as good as it gets. I wasn’t able to find any other solution that covers as many cases and is worth the effort.

ASP.NET 5 beta 5 and other news

Microsoft recently released beta 5 of ASP.NET 5 and it includes a lot of bug fixes, API reviews and some package refactoring, namely on MVC. It’s nice to see this effort on tidy and coherent APIs. Here are some things I find worth mentioning:

As previously mentioned there’s a visible effort on API/packages organization which is visible if we browse on the change logs for the individual components. Nevertheless, I get the feeling that “it is almost there” for most of the components.

In addition to the beta 5 release, there are some other news worth mentioning:

  • There’s a new Announcements repository where the major changes on ASP.NET 5 are announced. Worth subscribing!
  • Visual Studio 2015 and ASP.NET 4.6 will ship on the July 20! ASP.NET 5 stuff will be beta and updated later.
  • ASP.NET 5 RC is tentative scheduled for late fall 2015. Meanwhile, monthly beta releases should occur.

EDIT: Mid-November is the target date for ASP.NET 5 RC

EDIT: Roadmap is now public.

More info: