SyntaxHighlighter

Wednesday, May 16, 2012

Handle HTTP Form Post requests with a WCF service

I have been spending a lot of time working with WCF of late and one of my goals were to build an interface capable of handling HTTP Post requests. My inspiration comes from the fantastically simple implementation by Stripe:

curl https://api.stripe.com/v1/charges \  
   -u vtUQeOtUnYr7PGCLQ96Ul4zqpDUO4sOE: \  
   -d amount=400 \  
   -d currency=usd \  
   -d "description=Charge for site@stripe.com" \  
   -d "card[number]=4242424242424242" \  
   -d "card[exp_month]=12" \  
   -d "card[exp_year]=2012" \  
   -d "card[cvc]=123"

Let's see how we can create a similar implementation using WCF.

In Visual Studio 2010, create a new WCF Service Application, which we're going to call the Network. The following files will be created within your project:

  • IService1.cs Your service contract
  • Service1.svc The service implementation 
  • Web.config Your service configuration information
You can rename these, but I'll leave them as they are, since renaming them can cause complications later on.

Let's start with our service interface. By default VS will generate two service methods (GetData and GetDataUsingDataContract) and a class called CompositeType. You can remove the methods and class and replace it with the following:

using System.IO;
using System.ServiceModel;
using System.ServiceModel.Web;

namespace Network
{
    [ServiceContract]
    public interface IService1
    {
        [OperationContract]
        [WebInvoke(UriTemplate = "ping")]
        string Ping(Stream input);
    }
}

The key here is to add the WebInvoke attribute. The UriTemplate indicates which URL will be mapped to this particular method call. In our case, Ping(...) will be executed if any HTTP Post request is sent to http://localhost:port/service1.svc/ping.

Next we need to implement our service. Essentially we are just pinging our service with some message which will be echoed back.

using System;
using System.Collections.Specialized;
using System.IO;
using System.Web;

namespace Network
{
    public class Service : IService1
    {
        public string Ping(Stream input)
        {
            var streamReader = new StreamReader(input);
            string streamString = streamReader.ReadToEnd();
            streamReader.Close();

            NameValueCollection nvc = HttpUtility.ParseQueryString(streamString);
            return string.IsNullOrEmpty(nvc["message"]) 
                ? "The 'message' key value pair was not received."
                : nvc["message"];
        }
    }
}

All we need to do now is to configure our service in the web.config. The key here is to create a  webHttpBinding endpoint with a corresponding webHttp endpointBehavior:

<?xml version="1.0"?>
<configuration>
  <system.serviceModel>
    <behaviors>
      <endpointBehaviors>
        <behavior name="webEndpointBehavior">
          <webHttp/>
        </behavior>
      </endpointBehaviors>
    </behaviors>
    <services>
      <service name="Network.Service1">
        <endpoint address="" 
                  behaviorConfiguration="webEndpointBehavior" 
                  binding="webHttpBinding" 
                  bindingConfiguration="" 
                  contract="Network.IService1"/>
      </service>
    </services>
  </system.serviceModel>
</configuration>

Now we can run our service. For consistency, we'll use the VS Development Server as our host and specify a static port. Go into the project properties, select the Web tab and set the Specific Port to 8000.

Set your Service.svc as the start page, and build and debug.

You can either create a plain HTML page with a form that posts to your service or you can use a tool such as curl to do a submission. Let's put it to the test:

c:\>curl http://localhost:8000/service1.svc/ping -d message=pong
"pong"

Success!

Update:
If you'd like to test this solution from a web page, you can use the following bit of HTML code:
<html>
  <body>
    <form action="http://localhost:8000/service1.svc/ping" method="post">
      <input name="message" type="text" value="pong" />
      <input type="submit" />
    </form>
  </body>
</html>
The response received is:
<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">pong</string>
One might want to get rid of the tags around the response string as well, but I'll get back to that in another post.

6 comments:

Anonymous said...

Hello,

It work like a charm with curl but from a HTTP form like this

form action="http://localhost:50718/services/FormServices.svc?Ping" method="post"

the browser move me on another page with endpoint error (cannot fin endpoint)

regards

Andre Hauptfleisch said...

The idea is not do a POST directly from a browser. We are merely throwing SOAP out of the window, and using HTTP POSTs instead. So you might still be able to POST to the service from an HTTP form, but the response might not be what you are after.

I've updated the post to show an example of what you might expect as a response.

Anonymous said...

I have found my mistake. I called FormServices.svc?Ping not FormServices.svc/Ping

Idiot...

Anonymous said...

I followed your example and when I hit f5 to run the project from visual studio, I get Microsoft WCF Test Client window with error message stating Service metadata may not be accessible. Make sure you are service is running and exposing metadata. I googled around and tried various solutions like adding mex to webconfig etc. But still no luck. Any idea?

Andre Hauptfleisch said...

One would not be able to test the service with the normal WCF Test Client.

You can still hit F5, ignore the error, and the use another tool to post to the said URL. There's two easy ways one could do it. Firstly, use a tool such as Curl to create a simple post. Secondly, create a simple HTML form with the relevant field names and post to the said URL.

Munkhtur said...

@Andre thanks for the response I am actually asking this question on stackoverflow now. If you do not mind looking at it. Here is the link to the question.
http://stackoverflow.com/questions/18522632/this-operation-is-not-supported-in-the-wcf-test-client-because-it-uses-type-syst