Поделиться через


Implementing CORS support in ASP.NET Web APIs – take 2

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.

  1. [EnableCors]
  2. public IEnumerable<string> Get()
  3. {
  4.     return allValues;
  5. }
  6.  
  7. // GET /api/values/5
  8. [EnableCors]
  9. public string Get(int id)
  10. {
  11.     // implementation ommitted
  12. }
  13.  
  14. // POST /api/values
  15. [EnableCors]
  16. public HttpResponseMessage Post(string value)
  17. {
  18.     // implementation ommitted
  19. }
  20.  
  21. // PUT /api/values/5
  22. [EnableCors]
  23. public void Put(int id, string value)
  24. {
  25.     // implementation ommitted
  26. }
  27.  
  28. // DELETE /api/values/5
  29. public void Delete(int id)
  30. {
  31.     // implementation ommitted
  32. }

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.

  1. public class EnableCorsAttribute : ActionFilterAttribute
  2. {
  3.     const string Origin = "Origin";
  4.     const string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
  5.  
  6.     public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
  7.     {
  8.         if (actionExecutedContext.Request.Headers.Contains(Origin))
  9.         {
  10.             string originHeader = actionExecutedContext.Request.Headers.GetValues(Origin).FirstOrDefault();
  11.             if (!string.IsNullOrEmpty(originHeader))
  12.             {
  13.                 actionExecutedContext.Result.Headers.Add(AccessControlAllowOrigin, originHeader);
  14.             }
  15.         }
  16.     }
  17. }

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.

  1. public class CorsPreflightActionSelector : ApiControllerActionSelector
  2. {
  3.     public override HttpActionDescriptor SelectAction(HttpControllerContext controllerContext)
  4.     {
  5.         HttpRequestMessage originalRequest = controllerContext.Request;
  6.         bool isCorsRequest = originalRequest.Headers.Contains(Origin);
  7.         if (originalRequest.Method == HttpMethod.Options && isCorsRequest)
  8.         {
  9.             string accessControlRequestMethod = originalRequest.Headers.GetValues(AccessControlRequestMethod).FirstOrDefault();
  10.             if (!string.IsNullOrEmpty(accessControlRequestMethod))
  11.             {
  12.                 HttpRequestMessage modifiedRequest = new HttpRequestMessage(
  13.                     new HttpMethod(accessControlRequestMethod),
  14.                     originalRequest.RequestUri);
  15.                 controllerContext.Request = modifiedRequest;
  16.                 HttpActionDescriptor actualDescriptor = base.SelectAction(controllerContext);
  17.                 controllerContext.Request = originalRequest;
  18.                 if (actualDescriptor != null)
  19.                 {
  20.                     if (actualDescriptor.GetFilters().OfType<EnableCorsAttribute>().Any())
  21.                     {
  22.                         return new PreflightActionDescriptor(actualDescriptor, accessControlRequestMethod);
  23.                     }
  24.                 }
  25.             }
  26.         }
  27.  
  28.         return base.SelectAction(controllerContext);
  29.     }
  30. }

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.

  1. class PreflightActionDescriptor : HttpActionDescriptor
  2. {
  3.     HttpActionDescriptor originalAction;
  4.     string accessControlRequestMethod;
  5.  
  6.     public PreflightActionDescriptor(HttpActionDescriptor originalAction, string accessControlRequestMethod)
  7.     {
  8.         this.originalAction = originalAction;
  9.         this.accessControlRequestMethod = accessControlRequestMethod;
  10.     }
  11.  
  12.     public override string ActionName
  13.     {
  14.         get { return this.originalAction.ActionName; }
  15.     }
  16.  
  17.     public override object Execute(HttpControllerContext controllerContext, IDictionary<string, object> arguments)
  18.     {
  19.         HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
  20.         
  21.         // No need to add the Origin; this will be added by the action filter
  22.  
  23.         response.Headers.Add(AccessControlAllowMethods, this.accessControlRequestMethod);
  24.  
  25.         string requestedHeaders = string.Join(
  26.             ", ",
  27.             controllerContext.Request.Headers.GetValues(AccessControlRequestHeaders));
  28.  
  29.         if (!string.IsNullOrEmpty(requestedHeaders))
  30.         {
  31.             response.Headers.Add(AccessControlAllowHeaders, requestedHeaders);
  32.         }
  33.  
  34.         return response;
  35.     }
  36.  
  37.     public override ReadOnlyCollection<HttpParameterDescriptor> GetParameters()
  38.     {
  39.         return this.originalAction.GetParameters();
  40.     }
  41.  
  42.     public override Type ReturnType
  43.     {
  44.         get { return typeof(HttpResponseMessage); }
  45.     }
  46.  
  47.     public override ReadOnlyCollection<Filter> GetFilterPipeline()
  48.     {
  49.         return this.originalAction.GetFilterPipeline();
  50.     }
  51.  
  52.     public override IEnumerable<IFilter> GetFilters()
  53.     {
  54.         return this.originalAction.GetFilters();
  55.     }
  56.  
  57.     public override ReadOnlyCollection<T> GetCustomAttributes<T>()
  58.     {
  59.         return this.originalAction.GetCustomAttributes<T>();
  60.     }
  61. }

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.

  1. [HttpControllerConfiguration(HttpActionSelector = typeof(CorsPreflightActionSelector))]
  2. public class ValuesController : ApiController
  3. {
  4.     //...
  5. }

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]

Comments

  • Anonymous
    March 06, 2012
    HiGreat post!I know that this is just an example, but shouldn't some kind of validation of the originHeader take place in the EnableCorsAttribute.. to make sure that the request is coming from a "friendly" source?
  • Anonymous
    March 07, 2012
    It really depends. If your service doesn't rely on any cookies for maintaining state, for example, or if it doesn't have any state at all, it should be fine for it to be accessed in a cross-domain way by any client. Remember, if someone wants to call your service and they know the address, they will - the cross-domain restriction is enforced by the browsers, so someone can simply create a C# program, for example, and send the request to your service.
  • Anonymous
    March 07, 2012
    Great post... but I'm having trouble getting it to work with chrome.  Also I downloaded your code package and i cannot get it to compile and run.  I have vs 2010 with all the latest updates and mvc 4.Any help?
  • Anonymous
    March 08, 2012
    What error do you get? It should work with VS 2010 with SP1 - that's what I used to create it - and the ASP.NET MVC 4 Beta download.
  • Anonymous
    March 08, 2012
    Hey Carlos,First error is:System.Web.HttpException occurred Message=File does not exist. Source=System.Web ErrorCode=-2147467259 WebEventCode=0 StackTrace:      at System.Web.StaticFileHandler.GetFileInfo(String virtualPathWithPathInfo, String physicalPath, HttpResponse response) InnerException:Second error is:Microsoft.CSharp.RuntimeBinder.RuntimeBinderException occurred Message='System.Dynamic.DynamicObject' does not contain a definition for 'Title' Source=Microsoft.CSharp StackTrace:      at Microsoft.CSharp.RuntimeBinder.RuntimeBinderController.SubmitError(CError pError) InnerException:I may be doing something stupid... But I download a fair amount of examples and don't seem to have issues like this.Thanks for your help!D
  • Anonymous
    March 09, 2012
    The comment has been removed
  • Anonymous
    March 09, 2012
    I can't get this working. I downloaded your code and tried it. It works only if the all call is from same project.  I am using IE. For Firefox, cross domain code works right of the gate without having to add anything to my ASP .NET project. Any ideas?
  • Anonymous
    March 09, 2012
    The support for cross-domain calls on IE 9 (and earlier) isn't great. With IE10 (which comes on Win8) it should work better. One workaround for it to work on IE9 (and IE8 as well, I think) is to force jQuery to go to the xDomainRequest object internally by settingjQuery.support.cors = true;in the beginning of the JS code.
  • Anonymous
    March 17, 2012
    Thanks Carlos,I followed your directions and now have your example working with both chrome and ie (provided I add the $.support.cors = true; for ie).  I suspect it has something to do with your nuget config.  I can pack up mine and ship it to you if you want.I'm still having trouble getting mine to work.  Apparently it is getting stuck in my validation module trying to validate the "OPTIONS" request.
  • Anonymous
    March 18, 2012
    Hey Carlos,I got your example working.  And I got my code working... yay... thanks for your help.BUT... when I install my mvc app on a remote host, and access it through jquery ajax on my local machine, ie still works fine but chrome is giving me the ole' NETWORK_ERR: XMLHttpRequest Exception 101.  My guess is that I have to either set (or not) the 'Origin' header in my ajax client, or enable something else on the server.  If I should set the Origin header what should I set it to?Thanks for your help.Dave
  • Anonymous
    March 19, 2012
    Woohoo... got it all working with ie, ff, and chromeTurns out I did not have IIS6 configured all of the way... found this web site: haacked.com/.../asp.net-mvc-on-iis-6-walkthrough.aspxAnyway, let me know if you want my clean package zipped and sent.Dave
  • Anonymous
    April 29, 2012
    The comment has been removed
  • Anonymous
    June 14, 2012
    Having issues now that I upgraded to the MVC RC.Have you tried the code with the new release?
  • Anonymous
    June 19, 2012
    Hey Carlos... still having trouble getting this to work on the latest release.  Looks like HttpActionDescriptor may have changed.
  • Anonymous
    June 25, 2012
    For RC: http://pastebin.com/tNdS5P5A from forums.asp.net/.../10
  • Anonymous
    June 25, 2012
    Corrected: http://pastebin.com/85ENCejp
  • Anonymous
    June 27, 2012
    Do you have any updated source for the latest ASP .NET Web API RC?
  • Anonymous
    June 28, 2012
    The latest code is not working for Google Chrome and FireFox. why?
  • Anonymous
    July 01, 2012
    Hi Dave, Jonathan, anonymous and Tanveer, I updated the code to work with the ASP.NET Web API Release Candidate version. I added a link on the top of this page for a post which explains the differences, or you can go directly to blogs.msdn.com/.../cors-support-in-asp-net-web-api-rc-version.aspx for the new post.
  • Anonymous
    November 05, 2012
    actionExecutedContext.Result.Headers.Add(AccessControlAllowOrigin, originHeader); should be actionExecutedContext.Request.Headers.Add(AccessControlAllowOrigin, originHeader);