Building a strongly typed route provider for ASP.NET Web API

Β· 1534 words Β· 8 minutes to read

ASP.NET Web API 2.2 was released last week, and one of the key new features is the ability to extend and plug in your own custom logic into the attribute routing engine.

Commonly known as “attribute routing”, it’s actually officially called “direct routing”, because, as we are about to show here, it’s not necessary to use it with attributes at all, and you can plug in any route provider into it.

More after the jump.

Introducing IDirectRouteProvider πŸ”—

The new extensibility point is called IDirectRouteProvider and is shown below. It allows you to plug in any logic that would be responsible for registering direct routes. So with the right provider, the routes can easily be registered centrally too, they don’t have to be coupled to your controllers/actions inline, like it is the case with regular attribute routing.

public interface IDirectRouteProvider  
{  
IReadOnlyList<RouteEntry> GetDirectRoutes(  
HttpControllerDescriptor controllerDescriptor,  
IReadOnlyList<HttpActionDescriptor> actionDescriptors,  
IInlineConstraintResolver constraintResolver);  
}  

The framework ships with the default implementation called DefaultDirectRouteProvider which is the one being used when you call the MapAttributeRoutes extension method against your HttpConfiguration to instruct Web API to use attribute routing.

All of the methods on the default implementation are virtual which makes it really easy to customize its behavior, without having to reimplement the interface from scratch.

That default implementation will simply walk through all of the available controllers and harvest all routes declared through the use of RouteAttribute and register them. Of course, it is all not surprising - after all, this is the typical attribute routing behavior.

Finally, the route in direct routing is represented by IDirectRouteFactory, which is exactly what the RouteAttribute is implementing. It’s a simple interface, shown below, and is responsible for producing an instance of a route - a RouteEntry.

public interface IDirectRouteFactory  
{  
RouteEntry CreateRoute(DirectRouteFactoryContext context);  
}  

The important message here is that as long as we implement that interface, we are free to invent any class we wish to represent routes in our Web API service - and be able to plug them into Web API pipeline. It doesn’t have to be an Attribute.

Strongly typed routing πŸ”—

One interesting thing you can build through this extensibility point is a strongly typed route provider. Wouldn’t you want to declare your routes in strongly typed way - that is explicitly pointing a route template to a controller/action, without the need to use magic strings to resolve controller or action name? This could sure be very handy when it comes to future maintenance and potential refactoring.

Consider the following TypedRoute class, implementing the aforementioned IDirectRouteFactory. It is a simple wrapper for the information required by the route, with a couple of methods returning this hanging off it, but that’s just syntactic sugar so that we could build the routes using a fluent approach.

public class TypedRoute : IDirectRouteFactory  
{  
public TypedRoute(string template)  
{  
Template = template;  
}

public Type ControllerType { get; private set; }

public string RouteName { get; private set; }

public string Template { get; private set; }

public string ControllerName  
{  
get { return ControllerType != null ? ControllerType.FullName : string.Empty; }  
}

public string ActionName { get; private set; }

public MethodInfo ActionMember { get; private set; }

RouteEntry IDirectRouteFactory.CreateRoute(DirectRouteFactoryContext context)  
{  
IDirectRouteBuilder builder = context.CreateBuilder(Template);

builder.Name = RouteName;  
return builder.Build();  
}

public TypedRoute Controller<TController>() where TController : IHttpController  
{  
ControllerType = typeof (TController);  
return this;  
}

public TypedRoute Action<T, U>(Expression<Func<T, U>> expression)  
{  
ActionMember = GetMethodInfoInternal(expression);  
ControllerType = ActionMember.DeclaringType;  
ActionName = ActionMember.Name;  
return this;  
}

public TypedRoute Action<T>(Expression<Action<T>> expression)  
{  
ActionMember = GetMethodInfoInternal(expression);  
ControllerType = ActionMember.DeclaringType;  
ActionName = ActionMember.Name;  
return this;  
}

private static MethodInfo GetMethodInfoInternal(dynamic expression)  
{  
var method = expression.Body as MethodCallExpression;  
if (method != null)  
return method.Method;

throw new ArgumentException("Expression is incorrect!");  
}

public TypedRoute Name(string name)  
{  
RouteName = name;  
return this;  
}

public TypedRoute Action(string actionName)  
{  
ActionName = actionName;  
return this;  
}  
}  

To declare the route in a most convenient way, you’d go through one of the Action methods (one for void actions and the other for actions with a return type), as they would automatically infer the controller. Ironically, in addition to strong typing the class also supports pointing to action/controller using magic strings, because, well, why not.

For the route template, we are simply going to leverage attribute routing string-based templates. This is very powerful because we get, for free, not only famailiar syntax, but support for all of the inline constraints, optional values and default values that come with attribute routing. As a result, in the CreateRoute method, we simply pass the template to the builder object, rather then doing any parsing beforehand. You could obviously choose do that - if so, then that would be the place to apply any of such pre-processing or template parsing.

Next step is to throw in a route provider that extends the DefaultDirectRouteProvider and looks up the relevant route when needed based on the current HttpActionDescriptor. This is shown below.

public class TypedDirectRouteProvider : DefaultDirectRouteProvider  
{  
internal static readonly Dictionary<Type, Dictionary<string, TypedRoute>> Routes = new Dictionary<Type, Dictionary<string, TypedRoute>>();

protected override IReadOnlyList<IDirectRouteFactory> GetActionRouteFactories(HttpActionDescriptor actionDescriptor)  
{  
var factories = base.GetActionRouteFactories(actionDescriptor).ToList();  
if (Routes.ContainsKey(actionDescriptor.ControllerDescriptor.ControllerType))  
{  
var controllerLevelDictionary = Routes[actionDescriptor.ControllerDescriptor.ControllerType];  
if (controllerLevelDictionary.ContainsKey(actionDescriptor.ActionName))  
{  
factories.Add(controllerLevelDictionary[actionDescriptor.ActionName]);  
}  
}

return factories;  
}  
}  

As you probably already noticed the collection of routes is maintained in a dictionary inside this type. This is not very elegant but does its job pretty well. The strongly typed routes will maintained in the following dictionary of dictionaries format (Dictionary<Type, Dictionary<string, TypedRoute») - each controller type will be the key and will get its own dictionary of key value pairs consisting of an action name and a TypedRoute instance corresponding to it. The only limitation of such approach is that you have to have unique action names within a controller (it could be easily overcome though, but it’s beyond the scope of this post).

Our custom TypedDirectRouteProvider ensures that every HttpActionDescriptor we will look up a relevant controller and action in the dictionary of routes, to make sure we support our extra TypedRoutes.

The final step is to provide some API for the developer to be able to register the typed routes - so that they somehow find their way into the dictionary of dictionaries we just set up. There are few ways to tackle this, but an extension method off HttpConfiguration sounds like a good idea. It’s shown below.

public static class HttpConfigurationExtensions  
{  
public static TypedRoute TypedRoute(this HttpConfiguration config, string template, Action<TypedRoute> configSetup)  
{  
var route = new TypedRoute(template);  
configSetup(route);

if (TypedDirectRouteProvider.Routes.ContainsKey(route.ControllerType))  
{  
var controllerLevelDictionary = TypedDirectRouteProvider.Routes[route.ControllerType];  
controllerLevelDictionary.Add(route.ActionName, route);  
}  
else  
{  
var controllerLevelDictionary = new Dictionary<string, TypedRoute> { { route.ActionName, route } };  
TypedDirectRouteProvider.Routes.Add(route.ControllerType, controllerLevelDictionary);  
}

return route;  
}  
}  

The method is rather self explanatory - if the given controller already has something registered, we just add the action/route key-value pair, otherwise we create the controller-specific dictionary beforehand.

You can now start using the new routing - you have to admit that it looks very cool right now:

var config = new HttpConfiguration();  
config.MapHttpAttributeRoutes(new TypedDirectRouteProvider());

config.TypedRoute("test", c => c.Action<TestController>(x => x.Get()));  
config.TypedRoute("test/{id:int}", c => c.Action<TestController>(x => x.GetById(Param.Any<int>())));  

Notice that we pass our custom provider into the MapHttpAttributeRoutes method. If you recall, we didn’t do anything to disable the default attribute routing behavior too, so this call will also respect all the inline attribute routes that you might have defined. Depending on the context, this might be useful or not - if you wish to prevent this hybrid behavior, you’d need to further customize the TypedDirectRouteProvider

To make this kind of strongly typed code compile, we have to pass in some arguments into the action methods. I have defined a helper class for that - since we throw away any of the values regardless (all params are constrained and defined in the template). On the other hand, it should be easy to extend it further and instead of using inline constraints a’la attribute routing, provide the constraints using this helper Param class too - kind of like you might be used doing when working with mocking libraries in tests.

public static class Param  
{  
public static TValue Any<TValue>()  
{  
return default(TValue);  
}  
}  

What else? πŸ”—

So what else could you build with this? Well, quite a few things actually. If you follow this blog you probably already know that very soon I will have a Recipes book coming out. In there we’ll look at some alternative application scenarios for IDirectRouteProvider and IDirectRouteFactory - localizing routes (a single route registration produces a number of localized route entires) and facilitating RPC routing with attribute routing (which is currently not possible in an automated way).

What’s Next πŸ”—

I can see this perhaps going somewhere - even if at this stage it’s demoware level. I have put all the code at Github - you can pick it up from there. There are plenty of things that could be added here, fixes, edge cases handling. If you are interested in contributing just ping me.

Altenratively, you have a great Superscribe routing for Web API from Pete Smith so you might just choose to roll with that. Nonetheless, I hope this short exercise we went through here was an interesting peek into the new routing extensibility in Web API 2.2.

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP