In this post, I’ll show how to extend the routing logic in ASP.NET Web API, by creating a custom controller selector. Suppose that you want to version your web API by defining URIs like the following:
/api/v1/products/
/api/v2/products/
You might try to make this work by creating two different “Products” controllers, and placing them in separate namespaces:
namespace MyApp.Controllers.V1 { // Version 1 controller public class ProductsController : ApiController { } } namespace MyApp.Controllers.V2 { // Version 2 controller public class ProductsController : ApiController { } }
The problem with this approach is that Web API finds controllers by class name, ignoring the namespace. So there’s no way to make this work using the default routing logic. Fortunately, Web API makes it easy to change the default behavior.
The interface that Web API uses to select a controller is IHttpControllerSelector. You can read about the default implementation here. The important method on this interface is SelectController, which selects a controller for a given HttpRequestMessage.
First, you need to understand a little about the Web API routing process. Routing starts with a route template. When you create a Web API project, it adds a default route template:
"api/{controller}/{id}"
The parts in curly brackets are placeholders. Here is a URI that matches this template:
http://www.example.com/api/products/1
So in this example, the placeholders have these values:
- controller = products
- id = 1
The default IHttpControllerSelector uses the value of “controller” to find a controller with a matching name. In this example, “products” would match a controller class named ProductsController. (By convention, you need to add the “Controller” suffix to the class name.)
To make our namespace scenario work, we’ll use a route template like this:
"api/{namespace}/{controller}/{id}"
Here is a matching URI:
http://www.example.com/api/v1/products/1
And here are the placeholder values:
- namespace = v1
- controller = products
- id = 1
Now we can use these values to find a matching controller. First, call GetRouteData to get an IHttpRouteData object from the request:
public HttpControllerDescriptor SelectController(HttpRequestMessage request) { IHttpRouteData routeData = request.GetRouteData(); if (routeData == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } // ...
Use IHttpRouteData to look up the values of “namespace” and “controller”. The values are stored in a dictionary as object types. Here is a helper method that returns a route value as a type T:
private static T GetRouteVariable(IHttpRouteData routeData, string name) { object result = null; if (routeData.Values.TryGetValue(name, out result)) { return (T)result; } return default(T); }
Use this helper function to get the route values as strings:
string namespaceName = GetRouteVariable(routeData, "namespace"); if (namespaceName == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } string controllerName = GetRouteVariable (routeData, "controller"); if (controllerName == null) { throw new HttpResponseException(HttpStatusCode.NotFound); }
Now look for a matching controller type. For example, given “namespace” = “v1” and “controller” = “products”, this would match a controller class with the fully qualified name MyApp.Controllers.V1.ProductsController
.
To get the list of controller types in the application, use the IHttpControllerTypeResolver interface:
IAssembliesResolver assembliesResolver = _configuration.Services.GetAssembliesResolver(); IHttpControllerTypeResolver controllersResolver = _configuration.Services.GetHttpControllerTypeResolver(); ICollectioncontrollerTypes = controllersResolver.GetControllerTypes(assembliesResolver);
This code performs reflection on all of the assemblies in the app domain. To avoid doing this on every request, it’s a good idea to cache a dictionary of controller types, and use the dictionary for subsequent look ups.
The last step is to replace the default IHttpControllerSelector with our custom implementation, in the HttpConfiguration.Services collection:
config.Services.Replace(typeof(IHttpControllerSelector), new NamespaceHttpControllerSelector(config));
You can find the complete sample hosted on aspnet.codeplex.com.
In order to keep the code as simple as possible, the sample has a few limitations:
- It expects the route to contain a “namespace” variable. Otherwise, it returns an HTTP 404 error. You could modify the sample so that it falls back to the default IHttpControllerSector in this case.
- The sample always matches the value of “namespace” against the final segment of the namespace (i.e., the inner scope). So “v1” matches “MyApp.Controllers.V1” but not “MyApp.V1.Controllers”. You could change this behavior by modifying the code that constructs the dictionary of controller types. (See the
InitializeControllerDictionary
method.)
Also, versioning by URI is not the only way to version a web API. See Howard Dierking’s blog post for more thoughts on this topic.