ASP.NET 5 Routing – Part II

On the previous post of this series I covered the basics of ASP.NET 5 routing. On this post I’ll explore how MVC fits on the routing system.

When configuring an application, the UseMvc method is invoked to add MVC to the pipeline. This method allows us to configure convention-based routes, such as:

app.UseMvc(routes =>
{
routes.MapRoute(
null,
"Hello/{name:alpha}",
new { controller = "HelloWorld", action = "Index" });
});

The method takes an action of IRouteBuilder which is just a means of aggregating routes and creating a RouteCollection. The MapRoute extension method is a simplified way of creating a TemplateRoute.

The same routing configuration could be achieved using attribute routing by annotating  the controller/action:

public class HelloWorldController : Controller
{
[Route("Hello/{name:alpha}")]
public object Index(string name)
{
return Content("Hello " + name + " from the ASP.NET 5 world!");
}
}

The UseMvc method is responsible for wiring up all the routes and it’s pretty straight forward:

public static IApplicationBuilder UseMvc(
[NotNull] this IApplicationBuilder app,
[NotNull] Action<IRouteBuilder> configureRoutes)
{
// ...

var routes = new RouteBuilder
{
DefaultHandler = new MvcRouteHandler(),
ServiceProvider = app.ApplicationServices
};

configureRoutes(routes);

routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(
routes.DefaultHandler,
app.ApplicationServices));

return app.UseRouter(routes.Build());
}

A few things to highlight:

  1. The route builder is created using MvcRouteHandler as the default handler. This is MVC’s top-most IRouter, which is used as the inner router on all the routes being created.
  2. Attribute routes are added on the beginning of the route list, which means they have precedence over convention-based routes, as expected. The AttributeRouting class is used to build an IRouter that composes all the attribute routes. This class lives in the MVC namespace, i.e. it’s not part of the base Microsoft.AspNet.Routing package.
  3. After building the application route collection, the basic routing middleware is registered, as described on the previous post.

Even if we didn’t go further, it’s already clear how MVC fits on the routing system by leveraging IRouter and the routing middleware (UseRouter). The next step is to take a look on how the attribute-based routes are created.

The AttributeRouting class just creates an instance of AttributeRoute which is an IRouter responsible for building the attribute routes. This is done on the first time AttributeRoute is invoked:

public Task RouteAsync(RouteContext context)
{
var route = GetInnerRoute();
return route.RouteAsync(context);
}

private InnerAttributeRoute GetInnerRoute()
{
var actions = _actionDescriptorsCollectionProvider.ActionDescriptors;
if (_inner == null || _inner.Version != actions.Version)
{
_inner = BuildRoute(actions);
}

return _inner;
}

The BuildRoute method is where the routes are actually processed; it executes the following steps:

  1. Extract route information from the available action descriptors (more on this further on).
  2. Create data structures for link generation based on route values (note that the routing system is bidirectional).
  3. Create data structures for URL matching.
  4. Create an instance of InnerAttributeRoute, which is the actual IRouter for attribute routing (more on this further on).

The first step is based on ActionDescriptor which represents an action on your application that was discovered by the framework. Building this model is a whole new subject, but for this post suffices to say that it contains things like the action name, metadata of the action parameters and attribute routing metadata, if applicable:

public class ActionDescriptor
{
public virtual string Name { get; set; }
public IList<ParameterDescriptor> Parameters { get; set; }
public AttributeRouteInfo AttributeRouteInfo { get; set; }

// ...
}

For each action that has attribute route info, an intermediate representation (RouteInfo) is created. This is done on the GetRouteInfo method, which contains some interesting stuff, highlighted bellow:

private static RouteInfo GetRouteInfo(
IInlineConstraintResolver constraintResolver,
Dictionary<string, RouteTemplate> templateCache,
ActionDescriptor action)
{
// ...

var routeInfo = new RouteInfo()
{
ActionDescriptor = action,
RouteTemplate = action.AttributeRouteInfo.Template,
// ...
};


RouteTemplate parsedTemplate;
if (!templateCache.TryGetValue(action.AttributeRouteInfo.Template, out parsedTemplate))
{
parsedTemplate = TemplateParser.Parse(action.AttributeRouteInfo.Template); // (1)
templateCache.Add(action.AttributeRouteInfo.Template, parsedTemplate);
}

routeInfo.ParsedTemplate = parsedTemplate;
routeInfo.Name = action.AttributeRouteInfo.Name;
// ...

routeInfo.Defaults = routeInfo.ParsedTemplate.Parameters // (2)
.Where(p => p.DefaultValue != null)
.ToDictionary(p => p.Name, p => p.DefaultValue, StringComparer.OrdinalIgnoreCase);

return routeInfo;
}

As highlighted above this is where the route template is actually parsed (1) and where the default route parameter values are prepared (2). The data structures mentioned on steps 2 and 3 are created from this info. Actually, for URL matching entries the parsed route template is given to a TemplateMatcher which be using when matching an URL. It’s also worth mentioning that all the URL matching entries are associated to a target IRouter which by default is MvcRouteHandler, already mentioned. This means that when one of this entries matches, it computes the route values as appropriate and delegates execution to its target.

On the last step, InnerAttributeRoute is created with all the gathered info. Since it is an IRouter, we should take a look at its RouteAsync method:

public async Task RouteAsync([NotNull] RouteContext context)
{
foreach(var matchingEntry in _matchingEntries)
{
var requestPath = context.HttpContext.Request.Path.Value;

var values = matchingEntry.TemplateMatcher.Match(requestPath); // (1)
if (values == null)
{
// If we got back a null value set, that means the URI did not match
continue;
}

var oldRouteData = context.RouteData;

var newRouteData = new RouteData(oldRouteData);
newRouteData.Routers.Add(matchingEntry.Target);
MergeValues(newRouteData.Values, values); // (2)


if (!RouteConstraintMatcher.Match( // (3)
matchingEntry.Constraints,
newRouteData.Values,
// ...))
{
continue;
}


context.RouteData = newRouteData;
await matchingEntry.Target.RouteAsync(context); // (4)

// ...

if (context.IsHandled) // (5)
{
break;
}
}
}

As expected, the URL matching entries are iterated and each entry’s template matcher is invoked (1). If the template matches, the current route values are merged with the ones extracted from the template (2). Next, the route constraints are checked (3) and if they match the target IRouter is invoked (4). An important aspect is that if the target doesn’t handle the request (5), other URL matching entries and eventually other routers will be attempted. This is different from previous versions of MVC, on which we’d get an HTTP not found.

As a side note: this method has a lot in common with the corresponding method on TemplateRoute (covered on the previous post). Both use template and constraint matchers and delegate on a target router.

Note that at (4) the route data contains important tokens extracted from the route template and/or defaults and/or constraints, such as “action” and “controller”. These route values are used by MvcRouteHandler to actually select an action.

public async Task RouteAsync([NotNull] RouteContext context)
{
// ...
var actionSelector = services.GetRequiredService<IActionSelector>();
var actionDescriptor = await actionSelector.SelectAsync(context);

if (actionDescriptor == null)
{
return;
}

// ...

await InvokeActionAsync(context, actionDescriptor);
context.IsHandled = true;

// ...
}

This might seem like we’re taking a step back. We had a template that was originally extracted from an action descriptor and now we’re going to select an action again? Keep in mind that applications can also define convention-based routes, where a single template may match multiple controllers/actions. All the framework knows at this point is that a route template matched, but it still needs to determine if the corresponding action exists, if action constraints match (e.g. HTTP method) and so on. All this common logic is triggered at this point.

An that’s it for MVC routing. This has been a long post, but hopefully it can provide some guidance when following ASP.NET source code.

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s