WCF: CORS support for self-hosted WCF REST service

Problem statement
WCF rest service is self-hosted. Client application is web based, and hosted on IIS. It makes cross-boundary call to the WCF service. However, it cannot complete the request as it fails with “405 - method not allowed” response.

What could be done to address the cross-boundary communication in WCF?

CORS

  • JavaScript and the web programming has grown by leaps and bounds over the years, but the same-origin policy still remains.
  • Under this policy, a web browser permits scripts contained in a first web page to access data in a second web page, but only if both web pages have the same origin.
  • This prevents JavaScript from making requests across domain boundaries, and has spawned various hacks for making cross-domain requests.
  • CORS introduces a standard mechanism that can be used by all browsers for implementing cross-domain requests.
  • The specification defines a set of headers that allow the browser and server to communicate about which requests are (and are not) allowed.

Solution
Having said the above description related to CORS, it is service where CORS features are to be enabled. For this, we have to rely on WCF extensibility concepts and provide a way to allow CORS based headers in the request and response.

The following example I have talked about is a WCF application (self-hosted with CORS headers enabled).

To get started, create a console application – CorsEnabledService.

Add the following classes in the console application project.

WCF extensibility related classes

 class CorsConstants
{
       internal const string Origin = "Origin";
       internal const string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
       internal const string AccessControlRequestMethod = "Access-Control-Request-Method";
       internal const string AccessControlRequestHeaders = "Access-Control-Request-Headers";
       internal const string AccessControlAllowMethods = "Access-Control-Allow-Methods";
       internal const string AccessControlAllowHeaders = "Access-Control-Allow-Headers";
       internal const string PreflightSuffix = "_preflight_";
}
 public class CorsEnabledAttribute : Attribute, IOperationBehavior
{
       public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters)
       {
       }

       public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
       {
       }

       public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
       {
       }

       public void Validate(OperationDescription operationDescription)
       {
       }
}
     class CorsEnabledMessageInspector : IDispatchMessageInspector
    {
        private List corsEnabledOperationNames;

        public CorsEnabledMessageInspector(List corsEnabledOperations)
        {
            this.corsEnabledOperationNames = corsEnabledOperations.Select(o => o.Name).ToList();
        }

        public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
        {
            HttpRequestMessageProperty httpProp = (HttpRequestMessageProperty)request.Properties[HttpRequestMessageProperty.Name];
            object operationName;
            request.Properties.TryGetValue(WebHttpDispatchOperationSelector.HttpOperationNamePropertyName, out operationName);
            if (httpProp != null && operationName != null && this.corsEnabledOperationNames.Contains((string)operationName))
            {
                string origin = httpProp.Headers[CorsConstants.Origin];
                if (origin != null)
                {
                    return origin;
                }
            }

            return null;
        }

        public void BeforeSendReply(ref Message reply, object correlationState)
        {
            string origin = correlationState as string;
            if (origin != null)
            {
                HttpResponseMessageProperty httpProp = null;
                if (reply.Properties.ContainsKey(HttpResponseMessageProperty.Name))
                {
                    httpProp = (HttpResponseMessageProperty)reply.Properties[HttpResponseMessageProperty.Name];
                }
                else
                {
                    httpProp = new HttpResponseMessageProperty();
                    reply.Properties.Add(HttpResponseMessageProperty.Name, httpProp);
                }

                httpProp.Headers.Add(CorsConstants.AccessControlAllowOrigin, origin);
            }
        }
    } 
     class EnableCorsEndpointBehavior : IEndpointBehavior
    {
        public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
        {
        }

        public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
        }

        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
        {
            List corsEnabledOperations = endpoint.Contract.Operations
                .Where(o => o.Behaviors.Find() != null)
                .ToList();
            endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new CorsEnabledMessageInspector(corsEnabledOperations));
        }

        public void Validate(ServiceEndpoint endpoint)
        {
        }
    }
     class PreflightOperationBehavior : IOperationBehavior
    {
        private OperationDescription preflightOperation;
        private List allowedMethods;

        public PreflightOperationBehavior(OperationDescription preflightOperation)
        {
            this.preflightOperation = preflightOperation;
            this.allowedMethods = new List();
        }

        public void AddAllowedMethod(string httpMethod)
        {
            this.allowedMethods.Add(httpMethod);
        }

        public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters)
        {
        }

        public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
        {
        }

        public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
        {
            dispatchOperation.Invoker = new PreflightOperationInvoker(operationDescription.Messages[1].Action, this.allowedMethods);
        }

        public void Validate(OperationDescription operationDescription)
        {
        }
    } 
     class PreflightOperationInvoker : IOperationInvoker
    {
        private string replyAction;
        List allowedHttpMethods;

        public PreflightOperationInvoker(string replyAction, List allowedHttpMethods)
        {
            this.replyAction = replyAction;
            this.allowedHttpMethods = allowedHttpMethods;
        }

        public object[] AllocateInputs()
        {
            return new object[1];
        }

        public object Invoke(object instance, object[] inputs, out object[] outputs)
        {
            Message input = (Message)inputs[0];
            outputs = null;
            return HandlePreflight(input);
        }

        public IAsyncResult InvokeBegin(object instance, object[] inputs, AsyncCallback callback, object state)
        {
            throw new NotSupportedException("Only synchronous invocation");
        }

        public object InvokeEnd(object instance, out object[] outputs, IAsyncResult result)
        {
            throw new NotSupportedException("Only synchronous invocation");
        }

        public bool IsSynchronous
        {
            get { return true; }
        }

        Message HandlePreflight(Message input)
        {
            HttpRequestMessageProperty httpRequest = (HttpRequestMessageProperty)input.Properties[HttpRequestMessageProperty.Name];
            string origin = httpRequest.Headers[CorsConstants.Origin];
            string requestMethod = httpRequest.Headers[CorsConstants.AccessControlRequestMethod];
            string requestHeaders = httpRequest.Headers[CorsConstants.AccessControlRequestHeaders];

            Message reply = Message.CreateMessage(MessageVersion.None, replyAction);
            HttpResponseMessageProperty httpResponse = new HttpResponseMessageProperty();
            reply.Properties.Add(HttpResponseMessageProperty.Name, httpResponse);

            httpResponse.SuppressEntityBody = true;
            httpResponse.StatusCode = HttpStatusCode.OK;
            if (origin != null)
            {
                httpResponse.Headers.Add(CorsConstants.AccessControlAllowOrigin, origin);
            }

            if (requestMethod != null && this.allowedHttpMethods.Contains(requestMethod))
            {
                httpResponse.Headers.Add(CorsConstants.AccessControlAllowMethods, string.Join(",", this.allowedHttpMethods));
            }

            if (requestHeaders != null)
            {
                httpResponse.Headers.Add(CorsConstants.AccessControlAllowHeaders, requestHeaders);
            }

            return reply;
        }
    }

WCF contract and implementation

     [ServiceContract]
    public interface IValues 
    {
        [WebGet(UriTemplate = "values/{id}", ResponseFormat = WebMessageFormat.Json), CorsEnabled]
        string GetValue(string id);

        [WebInvoke(UriTemplate = "/values", Method = "POST", ResponseFormat = WebMessageFormat.Json), CorsEnabled]
        string AddValue(string value); 
    }
     public class ValuesService : IValues
    {
        public string GetValue(string id)
        {
            return string.Format("You have entered - {0}", id);
        }
        
        public string AddValue(string value)
        {
            return string.Format("POST response - {0}", value);
        }
    }
     class Program
    {
        static void Main(string[] args)
        {
            var epAddress = "localhost:3063/corsService";
            Uri baseAddress = new Uri(epAddress);
            var host = new CorsEnabledServiceHost(typeof(ValuesService), baseAddress);
            host.Open();

            Console.WriteLine("host ready on "+ epAddress + " ...");

            Console.ReadLine();
        }
    }

HTML page on the client application

<!DOCTYPE html>
<html xmlns="www.w3.org/1999/xhtml">
<head>
<title>Values service - test page</title>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.0.0.min.js"></script>
</head>
<body>
<h1>Values service - test page</h1>
<p>Id: <input type="text" id="id" /></p>
<p>
<input type="button" id="getOne" value="Get One" />
<input type="button" id="post" value="Add" />
</p>
<div id="result"></div>

<script type="text/javascript">
var valuesAddress = "localhost:3063/corsService/values";
$("#getOne").click(function () {
var id = "/" + $("#id").val();
$.ajax({
url: valuesAddress + id,
type: "GET",
success: function (result) {
$("#result").text(result);
},
error: function (jqXHR, textStatus, errorThrown) {
$("#result").text(textStatus);
}
});
});

        $("#post").click(function () {
var data = "\"" + $("#id").val() + "\"";
$.ajax({
url: valuesAddress,
type: "POST",
contentType: "application/json",
data: data,
success: function (result) {
$("#result").text(result);
},
error: function (jqXHR, textStatus, errorThrown) {
$("#result").text(textStatus);
}
});
});

    </script>
</body>
</html>

Screenshots
Run the service application

CORS.1

Web application is hosted on IIS. It can be same machine as of service application, or it can be on a difference machine as well.

IE -> access the application -> Open developer tools (F12) -> Network tab -> Enable

CORS.2 CORS.3
Mozilla Firefox -> access the application -> Tools -> Developer -> Debugger

CORS.4 CORS.5

 

The above discussed sample application can be downloaded from here.

Detailed references https://blogs.msdn.microsoft.com/carlosfigueira/2012/05/14/implementing-cors-support-in-wcf/ https://code.msdn.microsoft.com/Implementing-CORS-support-c1f9cd4b/sourcecode

 

I hope this helps!