In the last few weeks we have encountered a customer reports of performance issues with their Web API applications.
When the 4th report exhibited the same phenomenon I figured it’s time to blog about it.
Why would you customize a controller selector? (hint – try not to)
It gives you a chance to control how the request gets routed. By default Web API uses the controller token from routing to select the controller. If you want a different behavior, such as selection based on an HTTP header, then you need to customize the controller selector.
Wait. Not really. This was true for Web API 1.0. In Web API 2.0 attribute routing and inline constraints were added, with further enhancements in Web API 2.1 and 2.2 . Many of the scenarios once handled only by custom controller selectors could now be implemented using features of attribute routing.
Attribute routing allows you to use declarative templates and custom constraints directly on actions and controllers. This means that you are applying custom behavior in a few select places instead of affecting the behavior of the entire application. If instead you need to apply a custom behavior to all of your actions, attribute routing allows you to provide custom route discovery code. They still apply as normal attribute routes, so the rest of the system keeps functioning as default. The release notes for Web API 2.2 demonstrate a custom IDirectRouteProvider, which is used to customize attribute routes.
Most importantly, attribute routing doesn’t replace a core framework component so you don’t need to worry about concerns such as caching descriptors and API Explorer compatibility.
Here are some further samples for attribute routing:
Versioning with attribute routing
Back to IControllerSelector
I have seen developers use it to enable multiple controllers with the same type name but that are in different namespaces. I have seen it used for versioning controllers based on extra data from the request, such as in an HTTP header. Changing the controller selector for the above scenarios works well for the purpose of action selection, but might not work as well when you try to use the Web API help page or link generation. In your application that might be good enough for your purposes.
So say you still want to use conventional (non-attribute) routing and customize the controller selector. Go ahead, but watch out for the following problems.
What to watch out for
Let’s start by taking a look at the interface you will implement:
public interface IHttpControllerSelector { HttpControllerDescriptor SelectController(HttpRequestMessage request); IDictionary<string, HttpControllerDescriptor> GetControllerMapping(); }
Take a look at GetControllerMapping. The controller mapping says that a controller is mapped to a name. There is no way to specify the namespace or any more information, so if you are going to make controller selection decisions based on its namespace (like in the sample below), note that ApiExplorer is not aware of controllers with the same name but in different namespaces.
Next is the SelectController method. It returns an HttpControllerDescriptor, which seems innocent, but if you read through the constructor of that class, you will see that it calls Initialize() and then reflects over the controller attributes:
object[] attrs = type.GetCustomAttributes(inherit: false);
And then initializes a new controller settings, and initializes a new configuration.
Still doesn’t sound too bad?
With the default controller selector, this will happen just once because the descriptor is cached, but with a naïve customization this could happen on every request. This logic is relatively expensive, including many memory allocations, and a significant performance degradation can be measured. In some cases we have seen web services that could easily serve around 3000 requests per second, go down to less than a 1,000 just because of such a change.
One other thing to note is that calling the default constructor for HttpControllerDescriptor avoids instantiating the configuration, and can leave your application into an unexpected state. This constructor is marked in the documentation that it is for unit tests only and must not be used in production code.
So what to do
If all you are doing is customizing the controller name, just derive from DefaultHttpControllerSelector and override GetControllerName. The default controller selector will take care of caching the descriptor.
If you are implementing something more elaborate and you want to create your own descriptor be aware that you need to implement a caching mechanism, as the default implementation will not be sufficient.
Sample
Here is a sample that properly deals with controller selection (if not the side effects on API Explorer) and properly caches the controller descriptors:
Conclusion
Implementing an IHttpControllerSelector is something you don’t want to opt into lightly. But if you do, please consider caching and effects on other parts of the system.
0 comments