Introduction and motivation
[Level T3] A question keeps popping up on various forums (StackOverflow, ASP.NET Forums, etc) on how to define routing in ASP.NET Web API. And more important is that there does not seem to be any consensus on how to approach this - well probably since ASP.NET Web API is still in preview (RC).
In this post, I want to have a fresh look at routing in ASP.NET Web API and present a hierarchical model for organising resources.
In this post, I want to have a fresh look at routing in ASP.NET Web API and present a hierarchical model for organising resources.
Background
Resources are important part of RESTful architectual style. In REST all server operations (API) are defined as interactions with resources - in HTTP terms it would mean Verb interactions with URLs. This is in contrast with RPC style where server operations are defined as method calls.ASP.NET Web API's routing mirrors ASP.NET MVC routing - similar to some other aspects of Web API which uses ASP.NET MVC as the baseline since it is a familiar model for developers.
Routing was first introduced to ASP.NET by MVC. Later on routing code was integrated into system.web.dll.
Routing in ASP.NET MVC is very flat since all routes are defined from the root. This was one of the reasons MVC Areas were introduced to add another layer to top root routes. This can work for MVC but RESTful resource organisation usually requires more nested structure while using conventionally routing can result in configuration burden as well performance penalty.
How routing works
MVC (and Web API) routes are generally designed for a handful (and in most cases less than 100) routes. You were expected to use the default route for most cases and add a few more routes for occasional cases where the pattern was different. This in fact worked in most MVC applications but RESTful resource design requires richer and a more nested structure as we will see below.Route definition in ASP.NET Web API - as you all have probably used and know - is based on adding route to the RouteCollection:
var routes = GlobalConfiguration.Configuration.Routes; // in web hosted environment routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } );Each mapping will add a new HttpRoute object to the collection. HttpRoute itself is an implementation of IHttpRoute:
public interface IHttpRoute { string RouteTemplate { get; } IDictionary<string, object> Defaults { get; } IDictionary<string, object> Constraints { get; } IDictionary<string, object> DataTokens { get; } HttpMessageHandler Handler { get; } IHttpRouteData GetRouteData(string virtualPathRoot, HttpRequestMessage request); IHttpVirtualPathData GetVirtualPath(HttpRequestMessage request, IDictionary<string, object> values); }
Most important method is GetRouteData where the matching takes place. Basically if an implementation of IHttpRoute returns a non-null IHttpRouteData then route has matched.
Now how the matching work? Well, RouteCollection will loop through all the routes and calls GetRouteData and the first one to return a non-null will be the matched route for a given URL:
// snippet from HttpRouteCollection foreach (IHttpRoute route in _collection) { IHttpRouteData routeData = route.GetRouteData(_virtualPathRoot, request); if (routeData != null) { return routeData; } }
Did you notice something fundamental above? If you have 1000 routes and your given URL matches the 1000th route, GetRouteData (which involves complex and heavy string processing if you look at the Web API source code) has to be called for the first 999 routes and fail until it reaches your matched route. With 100, this probably not an issue but with numbers going up, performance will take a hit.
RESTful organisation of resources
This StackOverflow question is one of many questions on the Web API routing according to REST style. One of the main challenges is that, we as Microsoft developers, have used to design our API in an RPC fashion. This habit has been ingrained in our psyche since remoting days and then Web Services and more recently WCF. So it is only natural to fall into the same habit when designing or REST API.Martin Fowler on his article Steps towards the glory of REST talks about 3 levels of REST implementation. We will be talking about the level 1 and briefly about 2. Level 3 which is the most noble constraint of REST focuses on hypermedia which is beyond the topic of our discussion.
So at the level 1, we design the server to expose its API as resources. In case of HTTP and URLs, this will look like the directory/file structure on the disk - and as such hierarchical. If we mix REST with DDD concepts, each aggregate root of the publicly exposed domain (which is called Server domain, see the post on Client-Server) will be exposed at the root (considering API root is /api/):
/api/{AggregateRoot}/{id}
An example for cars would be /api/Car/1243. As such, we would have a CarController that receives the id through the URL. Now all of this is easily achievable using the default route very much in the good old MVC fashion.
However, the picture gets more complicated when we want to expose the tyres of the car. Let's say a car has Front/Rear Left/Right tyres as such FL, FR, RL and RR. One approach would be to expose the tyres as the aggregate root and have an ID for each tyre:
/api/Tyre/1234567
Well, this will work but tyre is really not a aggregate root since it really would only have a meaning as part of the car. So ideally we should expose them only as part of a car:
/api/Car/1243/Tyre/FR
So we would need a TyreController to handle the scenario and in this case we need a route similar to below:
/api/Car/{carId}/{controller}/{id}
Now this can be written in the generic form of:
/api/{parent}/{parentId}/{controller}/{id}
But as you can see in this case we need to define carId as parentId. Also not all of the domain has similar constraints for ID so this approach will impose a heavy limitation on our routes. On the other hand, we might have more than one nested levels where this can be even more complexed.
In addition to entity levels, we also have operations. Operations also can be defined as a resource, for example in case of a puncture:
POST /api/Car/1243/Tyre/FR/Puncture
This will create a puncture in the tyre. When we say create, we do not mean that necessarily a physical record needs to be created against the tyre. In fact mapping between REST resources and domain models is usually loose. The point is every operation needs to exposed as a resource and all operations to be performed using HTTP verbs. For example, repairing puncture can be represented as:
DELETE /api/Car/1243/Tyre/FR/Puncture
So the operations will complicate the routing even more. If the domain is big and complex, we could end up with many routes. As such, the performance will be hampered and we end up with the burden of defining all these routes at the root level.
So what is the solution? Solution is to turn Web API's flat routing into a hierarchical one.
AttributRouting maybe?
ReplyDeleteThanks for this - Was a lovely read. Did you ever get to write a blog post about hierarchical routing in ASP.NET MVC?
ReplyDeleteThanks :) Afraid not, but pleased that you enjoyed reading.
Delete