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.
- public class ValuesController : ApiController
- {
- static List<string> allValues = new List<string> { "value1", "value2" };
- // GET /api/values
- public IEnumerable<string> Get()
- {
- return allValues;
- }
- // GET /api/values/5
- public string Get(int id)
- {
- if (id < allValues.Count)
- {
- return allValues[id];
- }
- else
- {
- throw new HttpResponseException(this.Request.CreateResponse(HttpStatusCode.NotFound));
- }
- }
- // POST /api/values
- public HttpResponseMessage Post([FromBody]string value)
- {
- allValues.Add(value);
- return this.Request.CreateResponse<int>(HttpStatusCode.Created, allValues.Count - 1);
- }
- // PUT /api/values/5
- public void Put(int id, [FromBody] string value)
- {
- if (id < allValues.Count)
- {
- allValues[id] = value;
- }
- else
- {
- throw new HttpResponseException(this.Request.CreateResponse(HttpStatusCode.NotFound));
- }
- }
- // DELETE /api/values/5
- public void Delete(int id)
- {
- if (id < allValues.Count)
- {
- allValues.RemoveAt(id);
- }
- else
- {
- throw new HttpResponseException(this.Request.CreateResponse(HttpStatusCode.NotFound));
- }
- }
- }
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:
- public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
- {
- if (actionExecutedContext.Request.Headers.Contains(Origin))
- {
- string originHeader = actionExecutedContext.Request.Headers.GetValues(Origin).FirstOrDefault();
- if (!string.IsNullOrEmpty(originHeader))
- {
- actionExecutedContext.Response.Headers.Add(AccessControlAllowOrigin, originHeader);
- }
- }
- }
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.
- class PreflightActionDescriptor : HttpActionDescriptor
- {
- HttpActionDescriptor originalAction;
- string accessControlRequestMethod;
- private HttpActionBinding actionBinding;
- public PreflightActionDescriptor(HttpActionDescriptor originalAction, string accessControlRequestMethod)
- {
- this.originalAction = originalAction;
- this.accessControlRequestMethod = accessControlRequestMethod;
- this.actionBinding = new HttpActionBinding(this, new HttpParameterBinding[0]);
- }
- public override string ActionName
- {
- get { return this.originalAction.ActionName; }
- }
- public override Task<object> ExecuteAsync(HttpControllerContext controllerContext, IDictionary<string, object> arguments)
- {
- HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
- // No need to add the Origin; this will be added by the action filter
- response.Headers.Add(AccessControlAllowMethods, this.accessControlRequestMethod);
- string requestedHeaders = string.Join(
- ", ",
- controllerContext.Request.Headers.GetValues(AccessControlRequestHeaders));
- if (!string.IsNullOrEmpty(requestedHeaders))
- {
- response.Headers.Add(AccessControlAllowHeaders, requestedHeaders);
- }
- var tcs = new TaskCompletionSource<object>();
- tcs.SetResult(response);
- return tcs.Task;
- }
- public override Collection<HttpParameterDescriptor> GetParameters()
- {
- return this.originalAction.GetParameters();
- }
- public override Type ReturnType
- {
- get { return typeof(HttpResponseMessage); }
- }
- public override Collection<FilterInfo> GetFilterPipeline()
- {
- return this.originalAction.GetFilterPipeline();
- }
- public override Collection<IFilter> GetFilters()
- {
- return this.originalAction.GetFilters();
- }
- public override Collection<T> GetCustomAttributes<T>()
- {
- return this.originalAction.GetCustomAttributes<T>();
- }
- public override HttpActionBinding ActionBinding
- {
- get { return this.actionBinding; }
- set { this.actionBinding = value; }
- }
- }
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.