Magical Web API action selector – HTTP-verb and action name dispatching in a single controller

If you follow Web API on User Voice or track Web API issues on Codeplex, you’d probably know that one of the most popular requested features of Web API is to allow the developers to combine HTTP verb action dispatching (default one), with action-name based dispatching in a single controller.

The rationale is very obvious, and I’m pretty sure there is not a single Web API developer in the world, who hasn’t run into this problem – by not being allowed to combine these, whenever you want to create a nested resource, you need to add a new controller and manually register a funky nested route to facilitate it.

Let’s create a custom action selector to solve this.

The problem

By default Web API chooses controller actions based on an HTTP Verb (RESTful approach). You can force it into an RPC mode (action name based), but you can’t combine these two in a single controller, and as it would throw an Ambiguous match exception.

Imagine we would like to provide and API with the following URIs (very typical scenario, no?):

In order to achieve this out-of-the box with Web API, you are required to jump through some hoops – create a few separate controllers, and set up several different nested routes manually.

It would be much better if we had this resource, and all of its subresources (since they are part of one logical coherent entity) defined in a single controller, and would need just one default route to facilite the given URI structure.

Building a custom action selector

Normally on the blog, I try to go in details through the code we are writing together, but today’s is slightly more complicated (not very though, ultimately it’s just reflection and some LINQ), so I will just highlight the main points below.

We will create a class that implements IActionSelector, as that would allow us to plug into the hook provided by the Web API under GlobalConfiguration.Configuration.Services.

So what’s happening here?
1. We inherit from the default ApiControllerActionSelector and override the virtual SelectAction method.

2. Then, through reflection, we grab all the methods (“actions”) of a controller class from the controller context, which happen to be valid API actions. At this point we don’t care about verb or action based dispatching yet.

3. We build a collection of ReflectedHttpActionDescriptor objects for each valid method. This will give us access to such information as what kind of HTTP method a given action supports (this automatically takes care of the HTTP attributes with which the developer might have decorated an action, so we don’t need to worry about that)

4. We check – from the RouteData – what type of parameters we have in the request query. By default this solution is inteded to support three levels of nesting (as I showed in the route list initially) – so we will check for {action} and {subaction} tokens in the route. If we find a subaction, it means we need to dispatch based on the subaction name, so we try to find a matching method inside the controller (that also supports the current request’s HTTP method!). The same applies for an action, except the order is important – if we have a subaction, it means we don’t need to check for an action anymore.

5. If neither subaction nor action are found in the route data, it means we will need to try to dispatch based on the HTTP verb. So we try to find a method inside a controller that’s prefixed with the current request’s HTTP method – for example GetAll, Post and so on – so a standard verb based dispatching behaviour.

6. At this point we probably have found some matching methods, but it is very likely that we have some duplicates – because as you might have noticed – we only checked method names, and supported HTTP verbs, but we didn’t take into account any overloads. So if the developer created overloads, we will have more than 1 match. To work out which is the best one, we run a small helper method, that compares the matches to the route data parameters (I adapted this method from the core Web API) and this should typically yield a single best match.

7. The final step is to check how many matches we still have. If 0 – throw a 404. If more than 1 – throw an ambiguous match error. If 1 – dispatch the action and let the Web API pipeline continue.

Adding a route

We just said we allow this to go three levels deep (in fact, with some minor changes this could easily support infinite levels of depth, but I’m on the subway now, so will just leave it as it is for now). In order to support that, let’s add a generic route, and remeber that we introduced magical tokens action and subaction.

As you see, we now have a very nice, deep route template, which can support every single of the cases I initially showed.

Now let’s add a controller. It will just return dummy data, but that’s not the point – the idea is to illustrate tht we are hitting what we want to hit.

Notice – the top level of our resource (customer) will be dispatched based HTTP verb. The lower levels (action/subaction) will use action-based dispatching. Now, if this was using the default action selector, obviously it has no business of workin – we’d run into all kinds of ambiguity errors.

But instead, let’s plug in our hybrid selector and see what happens:

– GET /api/customer/

– GET /api/customer/1

– GET /api/customer/1/orders

– GET /api/customer/1/orders/3

– GET /api/customer/1/orders/3/shipments

– GET /api/customer/1/orders/3/shipments/1

– POST /api/customer

– POST /api/customer/1/orders

– POST /api/customer/1/orders/3/shipments

I could keep taking screenshots like this, but clearly – it works.

Summary

Now, one more point before we get all excited. This is not an optimal implementation YET. The main reason is that it resolves the matches on every request, which holds a performance penalty. What should be added to this solution (and what I plan to do, or maybe someone is willing to join forces :)) is caching of the resolved versions. That’s also what Web API core does now internally.

In the meantime, I have put the source code & a demo of this on Github, so feel free to grab it and play around. Cheers!

source on GitHub