Udostępnij za pośrednictwem


CORS support in ASP.NET Web API – RC version

The code for this post is published in the MSDN Code Gallery .

A few months back I had posted some code to enable support for CORS (Cross-Origin Resource Sharing) in the ASP.NET Web API. At that point, that product was in its Beta version, and with the Release Candidate (RC) released last month, some of the API changes made the code stop working (and in the per-action example, it even stopped building). With many comments asking for an updated version which builds, here they are.

The first version (a global message handler, which enabled CORS for all controllers / actions in the application), actually didn’t need any update at all. Actually, the message handler didn’t need any updates, but the actions in the values controller which take the string as a parameter need a [FromBody] decoration due to the model binding changes between the Beta and the RC version. In RC, parameters of simple types (such as string) by default come from the URI (either in the route or in the query string), and the application passed the parameter via the request body.

  1. public class ValuesController : ApiController
  2. {
  3.     static List<string> allValues = new List<string> { "value1", "value2" };
  4.  
  5.     // GET /api/values
  6.     public IEnumerable<string> Get()
  7.     {
  8.         return allValues;
  9.     }
  10.  
  11.     // GET /api/values/5
  12.     public string Get(int id)
  13.     {
  14.         if (id < allValues.Count)
  15.         {
  16.             return allValues[id];
  17.         }
  18.         else
  19.         {
  20.             throw new HttpResponseException(this.Request.CreateResponse(HttpStatusCode.NotFound));
  21.         }
  22.     }
  23.  
  24.     // POST /api/values
  25.     public HttpResponseMessage Post([FromBody]string value)
  26.     {
  27.         allValues.Add(value);
  28.         return this.Request.CreateResponse<int>(HttpStatusCode.Created, allValues.Count - 1);
  29.     }
  30.  
  31.     // PUT /api/values/5
  32.     public void Put(int id, [FromBody] string value)
  33.     {
  34.         if (id < allValues.Count)
  35.         {
  36.             allValues[id] = value;
  37.         }
  38.         else
  39.         {
  40.             throw new HttpResponseException(this.Request.CreateResponse(HttpStatusCode.NotFound));
  41.         }
  42.     }
  43.  
  44.     // DELETE /api/values/5
  45.     public void Delete(int id)
  46.     {
  47.         if (id < allValues.Count)
  48.         {
  49.             allValues.RemoveAt(id);
  50.         }
  51.         else
  52.         {
  53.             throw new HttpResponseException(this.Request.CreateResponse(HttpStatusCode.NotFound));
  54.         }
  55.     }
  56. }

The second version – CORS support on a per-action basis – had some changes. That version was implemented using two components: one filter attribute, and one action selector (to define the action that would respond to preflight requests). The code for the filter was almost the same, with the exception that the property of the HttpActionExecutedContext in the action filter which stored the response changed from Result to Response:

  1. public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
  2. {
  3.     if (actionExecutedContext.Request.Headers.Contains(Origin))
  4.     {
  5.         string originHeader = actionExecutedContext.Request.Headers.GetValues(Origin).FirstOrDefault();
  6.         if (!string.IsNullOrEmpty(originHeader))
  7.         {
  8.             actionExecutedContext.Response.Headers.Add(AccessControlAllowOrigin, originHeader);
  9.         }
  10.     }
  11. }

The code for the action selector itself was actually unchanged, but the nested type to implement the new action descriptor had quite a lot of changes, mostly related to the OM changes in the HttpActionDescriptor class (related to return types). Besides trivial changes (e.g., from ReadOnlyCollection<T> to Collection<T>), there were two larger changes:

  • The Execute method is now asynchronous (and also named ExecuteAsync). Since we don’t need to execute any asynchronous operation (we already know the operation result at that point), we’re now using a TaskCompletionSource<TResult> as explained by Brad Wilson in his series about TPL and Servers.
  • The class also defines a new property, ActionBinding, which defines the binding from the request to the parameters. Unlike the other operations in the class which we can simply delegate to the original descriptor, we can’t do that for the preflight action, since it’s possible that the actions take some additional parameter (which is the case in the values controller used in the example). In this case, we simply create a new instance of HttpActionBinding which doesn’t take any parameters to return to the Web API runtime.
  1. class PreflightActionDescriptor : HttpActionDescriptor
  2. {
  3.     HttpActionDescriptor originalAction;
  4.     string accessControlRequestMethod;
  5.     private HttpActionBinding actionBinding;
  6.  
  7.     public PreflightActionDescriptor(HttpActionDescriptor originalAction, string accessControlRequestMethod)
  8.     {
  9.         this.originalAction = originalAction;
  10.         this.accessControlRequestMethod = accessControlRequestMethod;
  11.         this.actionBinding = new HttpActionBinding(this, new HttpParameterBinding[0]);
  12.     }
  13.  
  14.     public override string ActionName
  15.     {
  16.         get { return this.originalAction.ActionName; }
  17.     }
  18.  
  19.     public override Task<object> ExecuteAsync(HttpControllerContext controllerContext, IDictionary<string, object> arguments)
  20.     {
  21.         HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
  22.  
  23.         // No need to add the Origin; this will be added by the action filter
  24.         response.Headers.Add(AccessControlAllowMethods, this.accessControlRequestMethod);
  25.  
  26.         string requestedHeaders = string.Join(
  27.             ", ",
  28.             controllerContext.Request.Headers.GetValues(AccessControlRequestHeaders));
  29.  
  30.         if (!string.IsNullOrEmpty(requestedHeaders))
  31.         {
  32.             response.Headers.Add(AccessControlAllowHeaders, requestedHeaders);
  33.         }
  34.  
  35.         var tcs = new TaskCompletionSource<object>();
  36.         tcs.SetResult(response);
  37.         return tcs.Task;
  38.     }
  39.  
  40.     public override Collection<HttpParameterDescriptor> GetParameters()
  41.     {
  42.         return this.originalAction.GetParameters();
  43.     }
  44.  
  45.     public override Type ReturnType
  46.     {
  47.         get { return typeof(HttpResponseMessage); }
  48.     }
  49.  
  50.     public override Collection<FilterInfo> GetFilterPipeline()
  51.     {
  52.         return this.originalAction.GetFilterPipeline();
  53.     }
  54.  
  55.     public override Collection<IFilter> GetFilters()
  56.     {
  57.         return this.originalAction.GetFilters();
  58.     }
  59.  
  60.     public override Collection<T> GetCustomAttributes<T>()
  61.     {
  62.         return this.originalAction.GetCustomAttributes<T>();
  63.     }
  64.  
  65.     public override HttpActionBinding ActionBinding
  66.     {
  67.         get { return this.actionBinding; }
  68.         set { this.actionBinding = value; }
  69.     }
  70. }

And that’s basically it. The code in the gallery will contain both projects (global CORS + per-action CORS), along with a simple project which can be tested (in CORS-enabled browsers, such as Chrome, IE10+ and FF3.5+) for both services.

[Code from this post]

Comments

  • Anonymous
    July 01, 2012
    Hi Carlos,your code will not work when the controller is decorated with [Authorize] - the custom attributes must return [AllowAnonymous] then.We have a complete implementation for Web API here: brockallen.com/.../cors-support-in-webapi-mvc-and-iis-with-thinktecture-identitymodel
  • Anonymous
    August 08, 2012
    Just a note that this does not allow preflighted cross-site requests to work on IIS Express in Chrome or Firefox.  This seems to be a bug in IIS Express (or perhaps it was never meant to work).  Preflighted requests send an OPTIONS request to the web server to check for capabilities.  For whatever reason, IIS Express ignores the CorsHandler message handler (breakpoint never gets hit in the debugger) and never returns the CORS headers in the response.  As a result, Chrome and Firefox both believe they don't have CORS access and won't make the request.  Cassini does not exhibit this problem and correctly returns the headers in the OPTIONS response.
  • Anonymous
    September 16, 2012
    Great stuff, thank you! This is definitely something I'll need in a presentation I will have.
  • Anonymous
    October 08, 2012
    the post is done and I can see my posted data in the database, but I still get an error in Chrome and FF:XMLHttpRequest cannot load nonstopwords.com/.../gameover. Origin http://victorantos.com is not allowed by Access-Control-Allow-Origin.
  • Anonymous
    January 02, 2013
    @victorantos you must check that the initial request's Origin header and the response's Access-Control-Allow-Origin match up, most probably the reason for your error.
  • Anonymous
    August 09, 2013
    The comment has been removed