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

Strath

January 16th, 2013

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

Because nested resources have never been easier

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

Be Sociable, Share!

  • Pingback: The Morning Brew - Chris Alcock » The Morning Brew #1275

  • Pingback: Dew Drop – January 17, 2013 (#1,481) | Alvin Ashcraft's Morning Dew

  • http://twitter.com/bradwilson Brad Wilson

    Yay, Github!
    Boo, no license.

    • http://www.strathweb.com/ Filip W

      aaand there is a license :)

  • Jeremy

    Thanks for the article, very useful. May I ask what that “testing interface” is (in the screenshots)? Is it a special web site on top of the API or a browser extension? Sorry if you mentioned that before, I must’ve missed it.

    • http://www.strathweb.com/ Filip W

      it’s Postman, a REST plugin for Chrome

  • Pingback: Distributed Weekly 190 — Scott Banwart's Blog

  • http://www.gregmaclellan.com/ Greg MacLellan

    I don’t mean to disrepect the work you’ve done here, but I have to say, if I were to jump into your code base, it would take me quite a while to figure this convention out.

    First I’d wonder how this works (since it’s not standard), and my first thought would be that you’ve hardcoded a bunch of routes somewhere (groan), not that you’ve built a clever IActionSelector.

    The other issue I have is the convention is not obvious. You’ve put comments in (this should be a BIG hint the convention is not obvious), but I have to ask my self: are they correct? Has anyone ever forgot to update them? If I’m trying to add a new URL, now I have to go a step further and try and understand the convention and implementation.

    If you haven’t seen it, you should take a look at http://attributerouting.net/. This has the same end goal — ability to add a complex/nested URI scheme — without the learning curve that comes with by-convention implementations.

    With AR, you get this:

    [GET("/api/customer/{customerid}/orders/{orderid}/shipments")]
    public string Shipments(int customerid, int orderid)

    and I bet you understand exactly how that works, without needing to read any bit of documentation.

    I’ve seen this trend recently especially in .NET where developers are getting so caught up in the ease of “by convention” but forgetting usability (for other dev’s). By convention is great when you know all the conventions (and there are only a few of them), but for someone new to the framework and/or codebase the conventions need to be obvious and easily learnable.

    • http://www.strathweb.com/ Filip W

      I really appreciate the comment but I think you completely missed the point of the article. The aim is to provide a technical solution to dispatch actions from a single controller using HTTP verb and action name (which happens to be one of the most requested features in the community). By default you can’t mix action based and verb based routing in Web API – and that’s exactly what this ActionSelector solves it (and without any custom convention – more on that later).

      I’m a big fan of Attribute Routing but it is a completely separate topic – it does not solve the dispatching issue (in fact it doesn’t even attempt to) but it just hides it. It works off the default action selector, so you still can’t mix HTTP-verb dispatching and action-name dispatching in a single controller, but works around that by registering a new route for every single action.

      So, sure you could apply these to individual actions:

      [GET("/api/customer")]
      [GET("/api/customer/{customerid}")]
      [GET("/api/customer/{customerid}/orders")]
      [GET("/api/customer/{customerid}/orders/{orderid}")]
      [GET("/api/customer/{customerid}/orders/{orderid}/shipments")]
      [GET("/api/customer/{customerid}/orders/{orderid}/shipments/{shipmentid}")]

      Which is great – but then you end up having a separate route in the route table for *every single* method in your controller. For this sample API branch (“customer” and descendants) this means 6 routes. If you have 50 branches, that would be 300 routes.

      In fact, you don’t even need Attribute Routing to achieve routing like the one I demoed – you could just go to the route table and register all these routes manually (the only difference you wouldn’t have them in attributes above the actions) and the effect would be identical – since AR does not do any magic except providing a nice shortcut way to work with existing dispatching and routing.

      With a customized action selector, like I showed, you can achieve the same with 1 single route (if you used standardized id/actionid/subactionid at all levels) or with 50 routes, if you *really* want to name your ids in a non-generic way (instead of using the generic single route I created) – you could still drastically reduce the number of routes compared to AR (1 route per branch so 6 times less):

      config.Routes.MapHttpRoute(
      name: “DefaultApi”,
      routeTemplate: “api/customer/{id}/{action}/{orderid}/{subaction}/{shipmentid}”,
      defaults: new { controller = “Customer”, id = RouteParameter.Optional, action = RouteParameter.Optional, orderid = RouteParameter.Optional, subaction = RouteParameter.Optional, shipmentid = RouteParameter.Optional }
      );

      And ultimately, the point of this article is to provide a performance friendly mechanism (to reduce number of routes to avoid performance penalty) to achieve deep nesting of routes. So – unlike you suggest – there are no “bunch of routes” anywhere, it all works off a *single* route (or a dramatically reduced number of routes, depneding on how generic you make them).

      As far as convention goes, I used {controller}/{action}/{subaction} pattern, to provide 3 level routing support. {controller} and {action} are already part of Web API core and are supported in the dispatching engine out of the box, so you can used them already there. The only difference is the 3rd level {subaction}, and for each level there is an id ({id}/{actionid}/{subactionid}) but I don’t really see that as a steep learning curve. By the way, you are not bound by that at all (I showed a sample route per tree just above), the only reason to use standardized ids is to have a single route.

      By the way, if this whole thing was intended to work only 2 levels deep (no {subaction} which is the only thing I introduced) then there would be even no custom convention whatsoever, since it would be using the same convention as Web API core. Perhaps that’s the way to go as well – as you might have noticed, this is all work in progress, no Nuget package yet. If you wish to contribute, you are more than welcome to :)

  • http://ilovedevelopment.blogspot.com/ Luke Baughan

    Hey Filip, great post (think I tweeted this one already) just wanted to say thanks for the post AND the mention of Postman Chrome extension (from the comments) seems like a brilliant tool, Ive just installed it and had a little play. Would be great to know which other tools/extensions you use regularly and their purpose!

    • http://www.strathweb.com/ Filip W

      I think that’s a good suggestion. Some people were already asking about that so soon might be a time to put together a “tooling” post :-)

  • Andrew Siemer

    This is exactly the fix I needed for a project I am currently working on. Thank you!!!

    There does seem to be an oddity that I have not been able to track down (though I have found a fix for it …for now). Essentially when I have added areas support (custom controller selector) and then start adding more than just the expected parameters to the actions I start to get multiple actions messaging (your selector finds more than one action). When I step into the actions that are picked it is clear to see that one of the actions is the right choice as the parameters that are present are correct and the other picked action is lacking the required parameters. One has id and actionid where as the other action is missing the actionid parameters. Very frustrating. Seems to have to do with the line that controls the filtering of actions:

    actionsFound = actionsFound.Where(descriptor => _actionParams[descriptor].All(combinedParameterNames.Contains));

    A simple fix…which may cause me pain down the road…is to change the data type of the parameters. I have the following two GET actions

    Shelves(string version, string merchantid, string id, string actionid)

    and

    Shelves(string version, string merchantid, string id, int count = 20, int skip = 0 …and many others).

    This fails with multiple methods error.

    Changing the first Shelves actionid parameter to int makes everything happy …though I would think that the data type wouldn’t be an issues given that the action signature is unique and the parameters that are equired (id, actionid, subactionid) are appropriately present/not present. Odd. :)

    Thoughts?

  • http://twitter.com/x0040h Alexey Suvorov

    Almost close to netkernel )

  • tarini.venugopal

    Hello Filip,

    Excellent work.. did you get a chance to optimize the above code ? can you please share if you have optimized .. I am looking the similar approach but in my I want to restrict one level like below

    api/{Action}/{ActionId}/{Controller}/{ID} ( Simple Relationships)

    urls :
    api/owner/1/dogs ( Dogs belongs to Owner 1)
    api/owner/1/dogs/2 ( Dog 2 of owner 1 details)
    api/owner/dogs ( Get all the dogs and their owners)
    api/owner/dogs/1
    Thanks in advance

  • Zorro

    Why no simply add routes, with differents param?

    Like

    api/{controller}/{id} => GetById(int id)

    api/{controller}/{idPotatoe}/{action}/{actionId} => Shipments(int idPotatoe,int actionId)

    Something is wrong with that?

  • Zorro

    Why no simply add routes with different params, something is wrong with that?

  • Pingback: Web API: Mixing Traditional & Verb-Based Routing | Applied Information Sciences Blog

  • Kip Streithorst

    I put together a blog post that shows verb-based and action based routing working together in the same ApiController without a custom action selector. http://blog.appliedis.com/2013/03/25/web-api-mixing-traditional-verb-based-routing/

  • Pingback: Technology Post Roundup–7th Edition « Jonathan Rozenblit

  • Andre’ Hazelwood

    I made a few changes to the code to allow for including the method name to the Entity to allow for cleaner operations on entities.

    For example:

    // If no actions were found, look for one that contains the action (to help with overloading).

    if ( !actionsFoundSoFar.Any() ) {

    actionsFoundSoFar =

    actionDescriptors.Where(

    i => i.ActionName.ToLowerInvariant().Contains( subactionName.ToString().ToLowerInvariant() ) && i.SupportedHttpMethods.Contains( method ) ).ToArray();

    }

    Allows me to now have a method that looks like:

    [HttpPut]
    public HttpResponseMessage PutEntity( string id ) { … }

    and

    [HttpPut]

    public HttpResponseMessage PutEntity2( string id ) { … }

    in the same ApiController

    Where I can then read off the entity definition from the content stream and allows me to have multiple similar methods for different entities without specifying the entity as an incoming object. For example, I can handle creating a default entity and return it back to the client without them having to worry about the format coming in.

  • agilbert201

    This is great, and very much appreciated! Was starting to fell like an alien and weird for my frustration trying to get “resource” routes to work.

  • darkUrse

    Hi, thanks for your code, very useful ! I have had however a few issues which I mostly corrected. To get your input, I added the suggestions on your github repo. Hopefully that’ll be useful and can help someone else ( or not ). Cheers

  • darkUrse

    Hi, first of all, thanks for your work ( I’ve already posted a message here, but for some reason it still doesn’t appear. So my apologize if I post a double ).
    Otherwise I wanted to thank you; I found this class of yours very very useful but encountered a few issues which I’ve mostly (appropriately?) corrected. I left a trace of it on your github repo by the way.
    The main issue I found with it is an ever growing _actionParams list. Clearing the list right before re-populating gives some strange issues though. Any idea what could be happening ?

    • http://www.strathweb.com/ Filip W

      I have no idea, I haven’t used it in quite a while to be honest! I’ll try to allocate some time to the problem though, thanks for the comment!