14

There are many reasons create a RESTful WCF server (it is easy) and even better if you can avoid ASP and it's security box (if all you are doing is simple requests to return information). See: http://msdn.microsoft.com/en-us/library/ms750530.aspx on how to do this.

What I found is that handling AJAX (JQUERY) GET requests is easy. But dealing with JSON in a POST is tricky.

Here is an example of a simple GET request contract:

    [OperationContract]
    [WebGet(ResponseFormat = WebMessageFormat.Json)]
    String Version();

And the implementaion is here (which returns a JSON)

    public partial class CatalogService : ICatalogService
{
    public String Version()
    {
        mon.IsActive = true;
        this.BypassCrossDomain();
        ViewModel.myself.TransactionCount++;
        return ViewModel.myself.VersionString;
    }
}

Ah, but what if you want to POST some JSON. You will find lots of articles on stack overflow that tell you all you have to do is this:

    [OperationContract]
    [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
    BuildResponse BuildToby(BuildRequest request);

which will receive a JSON message, de-serialize into a Plain .NET object (PONO) and let you work with it. And indeed, this worked fine when I constructed the request in Fiddler.

POST /BuildToby HTTP/1.1
User-Agent: Fiddler
Content-Type: application/json
Host: localhost:4326
Content-Length: 1999

However, when you use the following AJAX in JQUERY 1.8, you will find a SURPRISE:

It by specifying content-type of "application/json" you will find that there is a "preflight" check that is fired off by the browser to see if you are allowed to POST something other than a www-url-encloded post message. (there are notes in stack overflow about this).

    var request = JSON.stringify({ FrameList: ExportData.buildList });
    var jqxhr = $.ajax({
    type: "POST",
    url: "http://localhost:4326/BuildToby",
    data: request,
    contentType: "application/json; charset=utf-8",
    dataType: "json"
});

and here is what fiddler reports: (Note it is not a POST message, but a OPTIONS message).

OPTIONS http://localhost:4326/BuildToby HTTP/1.1
Host: localhost:4326
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:19.0) Gecko/20100101 Firefox/19.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Origin: http://ysg4206
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache

What has happened is that a browser (in this case Firefox) has to make a extra call to the server, with a OPTIONS HTTP message, to see if a POST is allowed (of this content type).

All the articles about fixing this are about editing GLOBAL.ASAX which is fine if you are in ASP.NET but are useless if you are doing a self-host WCF.

So now you see the question (sorry for being so long winded, but I wanted to make this a complete article so that others can follow the results).

Community
  • 1
  • 1
Dr.YSG
  • 6,072
  • 13
  • 61
  • 121
  • In the Mono framework for this, it looks like no matter what I use, the OPTIONS request hangs indefinitely. Was this something you saw while implementing this? It's certainly been awhile – RandomInsano Apr 03 '15 at 05:09

4 Answers4

27

Ok, now there are some real MSDN gurus out there who have written solutions, but I cannot figure them out: http://blogs.msdn.com/b/carlosfigueira/archive/2012/05/15/implementing-cors-support-in-wcf.aspx

But I have come up with a simple solution. At least in WCF 4.5 you can add your own OperationContract for dealing with OPTIONS requests:

    [OperationContract]
    [WebInvoke(Method = "OPTIONS", UriTemplate = "*")]
    void GetOptions();

Note that the method signature is void, and has no arguments. This will get called first, and then the POST message will be called.

The implementation of GetOptions is:

    public partial class CatalogService : ICatalogService
{
    public void GetOptions()
    {
        mon.IsActive = true;
        WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
        WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
        WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Headers", "Content-Type");
    }
}

and that is really all you have to do.

You might also want to add this attribute to your service class, so that you can serialize large JSON:

//This defines the base behavior of the CatalogService. All other files are partial classes that extend the service
[ServiceBehavior(MaxItemsInObjectGraph = 2147483647)]       // Allows serialization of very large json structures
public partial class CatalogService : ICatalogService
{
    PgSqlMonitor mon = new PgSqlMonitor();

    private void BypassCrossDomain()
    {
        WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
    }
}

Note I have a little helper method called BypassCrossDomain() which I call on all my POST and GET methods, so that I can deal with cross domain calls.

I spent a lot of research time here (in MSDN forums, stack overflow, blogs) and I hope this will help others trying to do these sorts of projects.

Community
  • 1
  • 1
Dr.YSG
  • 6,072
  • 13
  • 61
  • 121
  • 1
    Thanks, the BypassCrossDomain bit above helped sort out a cross domain problem I was having. – bradmo Apr 05 '13 at 21:18
  • Thank you, this is really helpful. I used it to call some https services from an http context and it worked great! – simdrouin Apr 01 '14 at 20:17
  • seems to just by sending a blank response for me! No headers no nothing! – K-Dawg Apr 29 '15 at 10:14
  • Thanks.It worked. Where from you got this magic GetOptions? Is that documented anywhere? – Joy George Kunjikkuru Dec 04 '15 at 22:44
  • If I could upvote this 100 times I would. This is an excellent solution for building AJAX services using WCF. Other (not so elegant) solutions involve stuff like intercepting the request and throwing an HTTP exception with a 200 code. – DVK May 10 '17 at 17:26
  • Does not work for me on WCF 4.6. Any suggestions? Thanks a lot! – Tom May 19 '20 at 20:49
3

One other addition to Dr.YSG's answer, if you need to support the OPTIONS method on endpoints which take POSTS to individual IDs, you will have to implement multiple GetOptions methods:

    [WebInvoke(Method = "OPTIONS", UriTemplate = "")]
    void GetOptions();
    [WebInvoke(Method = "OPTIONS", UriTemplate = "{id}")]
    void GetOptions(string id);

Its really disappointing that WCF/Microsoft can't automatically generation the proper OPTIONS response based on the signature of the endpoint automatically, but at least it can be handled manually.

jeremyh
  • 4,984
  • 3
  • 19
  • 18
0

In addition to what Dr.YSG listed as his answer, I found that I was getting a Firefox notice that a redirect was occurring, and a "405 Method Not Allowed" error. Adding

WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Allow-Origin", "*");
WebOperationContext.Current.OutgoingResponse.Headers.Add("Access-Control-Max-Age", "1728000");

to the GetOptions class seems to have addressed the issue.

nickvans
  • 850
  • 11
  • 22
  • For anyone reading this years later and finding nothing is working, I've been trying various options for hours. Nothing worked until I put that first line of code (allow-origin) *in the body of my WCF method*. (Not in some GetOptions() method.) – Bob Horn Mar 23 '16 at 01:47
0

After many days searching about it and reading many posts and proposed solutions, i think this question by Dr. YSG was the best resource to understand and solve my problem with angular/wcf/post/CORS etc.

But what really worked for me was this:

protected void Application_BeginRequest(object sender, EventArgs e)
{
    if (Request.HttpMethod == "OPTIONS")
    {
        Response.End();
    }
}

I understand it is not a full (nor beautiful) solution, since i am using global.asax and there are so many possible scenarios, but i just want to share this alternative, since it may help somebody else eventually.

(Besides adding the headers)

heringer
  • 2,088
  • 1
  • 17
  • 29