Cross Origin Resource Sharing with WCF JSON REST Services

4 minute read

My KonfDB platform provides a reliable way of configuration management as a service for cross-platform multi-tenant applications. When we refer to cross-platform capabilities, one of the ways to allow clients built using native technologies is by the way of REST services. WCF allows us to host a service and expose multiple endpoints using different protocols. So when KonfDB was in the design phase, I chose WCF as a tech-stack to support multiple endpoints and protocols.

I had written an article REST services with Windows Phone which should be a good starting point to understand WCF-REST services. Now, when you want this service to be accessible from different platforms – web, mobile, or across domains (in particular, Ajax requests) then we need to design few interceptors and behaviours that could allow Cross Origin Resource Sharing (CORS)

For this post, I will use the code from my own KonfDB platform. So those interested can actually visit the GitHub repository and explore more as well.

First, how CORS works

 

CORS works by providing specific instructions (sent from server) to the browsers which the browsers respect. These specific instructions are "additional" HTTP headers which are based on HTTP methods – GET or POST with specific MIME types. When we have HTTP POST method with specific MIME, the browser needs to "preflight" the request. Preflight means that the browser first sends an HTTP OPTIONS request header. Upon approval from the server, browser then sends the actual HTTP request.

So in a nutshell, we need some provision to handle these additional HTTP headers. In this post, we will see how we can change a RESTful service to support CORS.

REST Service Interface

 

A typical non-REST service interface defines methods and decorates them with OperationContract attribute. A REST service requires an additional attribute – one of these WebGet, WebPut or WebInvoke. So in the below example, to support Cross Origin Resource Sharing (CORS), we will decorate the method with attribute WebInvoke and set its Method="*"

 
[ServiceContract(Namespace = ServiceConstants.Schema, Name = "ICommandService")]
public interface ICommandService : IService
{
        [OperationContract(Name = "Execute")]
        [WebInvoke(Method = "*", ResponseFormat = WebMessageFormat.Json,
            BodyStyle = WebMessageBodyStyle.Bare,
            UriTemplate = "/Execute?cmd={command}&token={token}")]
        ServiceCommandOutput ExecuteCommand(string command, string token);
}

RESTful Behaviour and Endpoint

 

In KonfDB, WCF service is hosting in the Windows Service container. To provide consistent behaviour to bindings and for purpose of future extensibility, I have derived bindings from the native bindings available in .NET framework. So my REST binding looks like,

 

    public class RestBinding : WebHttpBinding
    {
        public RestBinding()
        {
            this.Namespace = ServiceConstants.Schema;
            this.Name = ServiceConstants.ServiceName;
            this.CrossDomainScriptAccessEnabled = true;
        }
    }

 

The important point to note is CrossDomainScriptAccessEnabled is set to true. This is very essential for WCF service to work with CORS – and yes, it is safe!

Defining a CORS Message Inspector and Header

As said in the earlier part of the post, we need a mechanism to intercept the request and add additional HTTP headers to tell the browser that the service does support CORS. Since this functionality is required at an endpoint level, we will define an endpoint behaviour for this. The code for the EnableCorsEndpointBehavior looks like,

 
public class EnableCorsEndpointBehavior : BehaviorExtensionElement, IEndpointBehavior
    {
        public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) {        }

        public void ApplyClientBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.ClientRuntime clientRuntime) { }

        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher)
        {
            var requiredHeaders = new Dictionary();

            requiredHeaders.Add("Access-Control-Allow-Origin", "*");
            requiredHeaders.Add("Access-Control-Request-Method", "POST,GET,PUT,DELETE,OPTIONS");
            requiredHeaders.Add("Access-Control-Allow-Headers", "X-Requested-With,Content-Type");

            var inspector = new CustomHeaderMessageInspector(requiredHeaders);
            endpointDispatcher.DispatchRuntime.MessageInspectors.Add(inspector);
        }

        public void Validate(ServiceEndpoint endpoint) { }

        public override Type BehaviorType
        {
            get { return typeof(EnableCorsEndpointBehavior); }
        }

        protected override object CreateBehavior()
        {
            return new EnableCorsEndpointBehavior();
        }
    }

 

Few important points to note

  • First, here are the headers Access-Control-Allow-Origin=* and Access-Control-Request-Method has OPTIONS set in it. If you want requests only from a particular domain name, you can change the value of Access-Control-Allow-Origin=http://www.mydomain.com and it should work correctly.
  • Second, we have passed these additional headers to a MessageInspector using another class CustomHeaderMessageInspector

The CustomHeaderMessageInspector class, that acts as a DispatchInspector, has the functionality to add these headers to the reply so that the client is aware of CORS. The CustomHeaderMessageInspector class looks like,

internal class CustomHeaderMessageInspector : IDispatchMessageInspector
    {
        private readonly Dictionary _requiredHeaders;
        public CustomHeaderMessageInspector(Dictionary headers)
        {
            _requiredHeaders = headers ?? new Dictionary();
        }

        public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
        {
            return null;
        }

        public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
        {
            var httpHeader = reply.Properties["httpResponse"] as HttpResponseMessageProperty;
            foreach (var item in _requiredHeaders)
            {
                httpHeader.Headers.Add(item.Key, item.Value);
            }
        }
    }

 

Last bit, adding this behaviour to the endpoint. Since the service is self-hosted and there is no WCF configuration file, the code looks like

             
            var serviceEndpoint = host.AddServiceEndpoint(typeof (T), binding.WcfBinding, endpointAddress);
            serviceEndpoint.Behaviors.Add(new WebHttpBehavior());
            serviceEndpoint.Behaviors.Add(new FaultingWebHttpBehavior());
            serviceEndpoint.Behaviors.Add(new EnableCorsEndpointBehavior());
            return serviceEndpoint;

Hosting and Testing this service

Using usual ServiceHost you can host this service and the service should run perfectly. To test this service, you can write a jQuery code

            $('#btnGet').click(function () {
                var requestUrl = 'http://localhost:8882/CommandService/Execute?cmd=someCommand&token=alpha';
                var token = null;
                $.ajax({
                    url: requestUrl,
                    type: "GET",
                    contentType: "application/json; charset=utf-8",
                    success: function (data) {
                        var outputData = $.parseJSON(data.Data);
                        token = outputData.Token;

                        ExecuteOtherRequests(token);
                    },
                    error: function (e) {
                        alert('error:' + JSON.stringify(e));
                    }
                });
            });

If you test this on Chrome with Inspector (F12), the network interaction would appear as shown in the screenshot below,

For a single Ajax request, as expected there is an HTTP OPTIONS request followed by HTTP GET request. If CrossDomainScriptAccessEnabled is not set to true in RestBinding, then we would get a HTTP 403 error - METHOD NOT FOUND.

If we look into the headers of the first request, we see that our WCF service (CustomHeaderMessageInspector) has added additional headers (highlighted) back into the request.

Since the browser got a HTTP Status Code = 200, it initiated the second (actual) request which is a HTTP GET request.

You can view the source code in KonfDB GitHub repository.

Updated: