This post was written for the Beta version of the ASP.NET MVC 4. The updates needed to make them run in the latest bits (Release Candidate) are listed in this new post.
The code for this post is published in the MSDN Code Gallery.
Last post I showed one way to implement CORS support in the ASP.NET Web APIs. It was somewhat simple, and enabled requests from CORS-aware browsers to all resources exposed by the APIs. This is basically equivalent to the CrossDomainScriptAccessEnabled property in WCF HTTP endpoints (although that was for JSONP). We can do better, though. Instead of enabling support for all actions, we can choose which ones we want to support cross-domain requests for, so we can enable cross-domain requests to GET, PUT and POST, but not DELETE, for example. This post will show how this can be implemented in a fairly simple way with the ASP.NET Web API action selection and filters support.
The straightforward way to approach this problem (which is what I originally tried) was to simply have an action filter applied to the operations which I wanted to support CORS – similar to the code below.
The action filter is really small, and when I tried it for the first request (get all values), it worked perfectly. The code executes after the action returns; if the request had an “Origin” header, then we tag the response with an “Access-Control-Allow-Origin” for the value of that header, and it all works out.
Then I tried adding a new value (POST) to the values list. And it failed – the browser showed an error, and the request didn’t make it to the operation and the action filter didn’t get executed. The problem was that for “unsafe” requests (such as POST, PUT and DELETE), the browser first sends a preflight request, a HTTP OPTIONS request (see last post for more information) asking what kind of CORS support the service has. But there are no routes which map OPTIONS requests to any actions, which causes the request to fail.
To solve this problem we can use a custom action selector, which will map preflight OPTIONS requests for incoming URIs which have already a route mapping to an action to a new HttpActionDescriptor, which will intercept those requests and return a response with the appropriate Access-Control-Allow headers if the action has the [CorsEnabled] attribute applied to it.
We can see the action selector below. If the request is a CORS preflight request (OPTIONS method, with an “Origin” header), then we’ll replace the request with the method requested by the preflight request (via the “Access-Control-Request-Method” header), then delegate to the default action selector to try to find if that request maps to any action. If such action exists, and if that action has the EnableCorsAttribute filter applied to it, then we’ll return our own action descriptor (PreflightActionDescriptor). Otherwise we’ll simply delegate the call back to the default action selector.
The custom action descriptor wraps the original one, and delegates most of the operations to it. The only members which it will implement itself are the ReturnType property (we’ll return a HttpResponseMessage directly), and the Execute method. On the Execute we create the response just like we did on the message handler example: map the “Access-Control-Request-[Method/Headers]” from the request to the “Access-Control-Allow-[Methods/Headers]” in the response. Notice that since we’re delegating all calls to the original action, including the list of filters, we don’t need to add the “Access-Control-Allow-Origin” header, since it will be added by the filter itself.
Now all that’s left is to hook up the action selector to the dispatcher. There are two ways to do that: either use the service resolver and set the CorsPreflightActionSelector as the implementer for the IHttpActionSelector interface (that would use it for all controllers in the application), or use the [HttpControllerConfiguration] attribute applied to the controller for which you want to enable the CORS preflight support, as shown below.
That’s it. Now we can remove the message handler which was used for the previous sample, and the cross-domain calls should continue working just fine.
[Code in this post]