A few weeks ago I was teaching our
WCF course at a client site and I decided to do an example of real-world interoperability by having a WCF client application integrate with the
eBay SOAP API.
Little did I know at the time how much fun awaited me.
After acquiring the necessary eBay sandbox test account, authentication tokens, and official WSDL definition, I was ready to rock 'n roll.
You can quickly tell how huge the eBay API is by looking at the size of the WSDL file, which comes in at 2.37 MB (yes, MB!). If you try to open it in Internet Explorer, the XSLT transformation will take forever to finish, if it finishes at all due to the sheer size of the document. But hey, eBay is a real-world retail application with lots of bells and whistles and they've tried to expose the whole beast. However, due to the size, I was a little worried about svcutil.exe's ability to handle it. But to my surprise, it chewed it up and spit out the necessary proxy code and some configuration elements. Now I was ready to write some code.
For my first test, I set my expectations low and chose to use the GeteBayOfficialTime operation, which simply requests the official time from the eBay server (which is important in a real-time auction system). I was expecting to be able to instantiate the proxy class and invoke the method, but as it turns out, each eBay SOAP operation requires a distinct URL where the method name and authentication tokens are contained in the URL. Here's an example of how you build the request URL:
string endpoint = "https://api.sandbox.ebay.com/wsapi";
string callName = "GetebayOfficialTime";
string siteId = "0";
string appId = "..."; // use your app ID
string devId = "..."; // use your dev ID
string certId = "..."; // use your cert ID
string version = "405";
// Build the request URL
string requestURL = endpoint
+ "?callname=" + callName
+ "&siteid=" + siteId
+ "&appid=" + appId
+ "&version=" + version
+ "&routing=default";
Now I have enough to instantiate the proxy class supplying requestURL for the address:
eBayAPIInterfaceProxy proxy = new eBayAPIInterfaceProxy(
"eBayAPI", requestURL);
The "eBayAPI" string is the name of the client endpoint configuration generated by svcutil.exe. It gives you a basicHttpBinding configuration using transport security (HTTPS).
It feels weird to general a unique URL for each operation within a service, and it quickly becomes tedious from the developer perspective. While this may be the norm in RESTful applications, in SOAP the URL typically identifies the service and the action identifies the operation, which the proxy can easily handle for you.
Now if you look at the signature for GeteBayOfficialTime, you'll also notice that it requires you to supply two objects: a CustomSecurityHeaderType and a GeteBayOfficalTimeRequestType. The eBay security design relies on transport-based security (HTTPS) to secure the entire channel, so it wants you to pass the application credentials within the SOAP body -- they don't use any WS-Security techniques.
Here's an example of how you can build both of these objects for use in the method:
CustomSecurityHeaderType creds = new CustomSecurityHeaderType();
creds.eBayAuthToken = "..."; // use your token
creds.Credentials = new UserIdPasswordType();
creds.Credentials.AppId = appId;
creds.Credentials.DevId = devId;
creds.Credentials.AuthCert = certId;
GeteBayOfficialTimeRequestType request =
new GeteBayOfficialTimeRequestType();
request.Version = version;
With this in place, you're ready to invoke the operation as shown here:
GeteBayOfficialTimeResponseType response =
proxy.GeteBayOfficialTime(ref creds, request);
Console.WriteLine("The time at eBay headquarters is:");
Console.WriteLine(response.Timestamp);
Once I understood how the eBay API worked (which was a bit counterintuitive given my understanding of SOAP), I was able to get the correct code in place and I was sure it would work.
But the SOAP Gods weren't going to let me off the hook so easy.
When I ran the code, I kept getting a helpful "Internal Server Error" message. Great, now I know something went wrong over in eBay Land but I have no idea what. So I pinged the MS interop expert, Kirill, who suggested that it might be the HTTP Content-Type header tripping things up (he had seen this before in a sample he did). It turns out that eBay fails if the charset value is enclosed in quotes as shown here:
Content-Type: text/xml; charset="utf-8"
They only accept the no-quotes version:
Content-Type: text/xml; charset=utf-8
Hmmm… ok, so how did I fix that within my WCF client? The charset value is a property emitted by the message encoder so the easiest way to fix this is to inject a custom text message encoder that doesn't include the quotes. I pulled up the WCF SDK to look for a sample and viola, there's actually a sample text message encoder in there -- and it doesn't include quotes around charset -- so I was able to use it as-is. In order to use the custom message encoder, I had to create a custom binding as shown here:
HttpsTransportBindingElement https = new HttpsTransportBindingElement();
https.MappingMode = HttpMappingMode.Soap;
CustomBinding bind = new CustomBinding();
bind.Elements.Add(new CustomTextMessageBindingElement(
"utf-8", "text/xml", MessageVersion.Soap11WSAddressing10));
bind.Elements.Add(https);
It's important to note that setting the MappingMode to HttpMappingMode.Soap is very important because if you don't, the default for the HttpsTransportBindingElement is to use HttpMappingMode.SoapWithWsAddressing. And guess what, WS-Addressing also causes eBay to fail. I had to learn this one the hard way too. Anyway, now I can supply the custom binding when creating the proxy:
eBayAPIInterfaceProxy proxy = new eBayAPIInterfaceProxy(
bind, new EndpointAddress(requestURL));
With all of this in place, I was finally able to make the call work. Phew.
A few observations:
- eBay has a wacky design for a SOAP API. It feels like they designed it primarily for non-SOAP use and then did a retro-fit. The addressing and security mechanisms are a few manifestations of this.
- Interoperability in the real-world is hard no matter how you shake it. And in order to work through issues you have to understand your SOAP stack (in this case WCF) before you can extend/tweak it (e.g., introducing the custom message encoder into the WCF proxy is not a trivial matter). You also have to understand what's happening on the wire (e.g., the charset & WS-Addressing issues). The Law of Leaky Abstractions rings true, loud and clear, when looking at this type of example.
- WCF is extremely flexible. It provides all of the necessary programming model options and extensibility points that we needed to work through these issues - a great tribute to its design.
I'm afraid that interop snags like these will be more the norm than the exception over the next several years as the dust continues to settle around the various SOAP and WS-* profiles. However, if you're using a good stack, and understand how things work, you can get around them.
Posted
Apr 18 2006, 12:41 PM
by
Aaron Skonnard