Thursday, April 21, 2005 - Posts

Solving web service interop problems

This week I solved a problem concerning web service interoperability posted on the Norwegian .NET User Group (NNUG) forum. The solution shows how to tackle incompatible SOAP envelopes, so I reckoned I post the problem and solution to my blog as well, since it is of general interest.

The Problem
A developer had been assigned a task which seemed trivial. He was to retrieve business information thru a web service provided by a thrid party. The service had a simple contract with a single operation accepting a business information message as a parameter. When the operation was invoked, the proxy always returned empty value objects. The developer had used Fiddler to assert that an actual SOAP message was returned from the service. The response message looked like this:

    1
 <?xml version="1.0" encoding="utf-8"?>
    2 <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
    3     <SOAP-ENV:Header />
    4     <SOAP-ENV:Body>
    5         <ForetakSokResponse xmlns="http://dbonline.no/test-webservices/xsd/ForetakInfo">
    6             <ForetakData>
    7                 <Dunsnr>820584895</Dunsnr>
    8                 <Orgnr>974217368</Orgnr>
    9                 <KodeType></KodeType>
   10                 <KodeTekst>Aktivt</KodeTekst>
   11                 <Navn>MARKED BEDRIFT</Navn>
   12                 <Adresse>RING 43</Adresse>
   13                 <Postnr>0245</Postnr>
   14                 <Poststed>OSLO</Poststed>
   15             </ForetakData>
   16             <Meldinger>
   17                 <MeldingsKode>999</MeldingsKode>
   18                 <MeldingsTekst>Kommando ferdig behandlet.</MeldingsTekst>
   19             </Meldinger>
   20         </ForetakSokResponse>
   21     </SOAP-ENV:Body>
   22 </SOAP-ENV:Envelope>

At first glance, this is a perfectly valid SOAP envelope containing a response message consisting of two complex types; “ForetakData” and “Meldinger”. However whenever the service was invoked, the developer always ended up with an instance of the ForetakSokResponse class with empty arrays for ForetakData and Meldinger.

The Solution
The web service was developed using Apache Axis. This framework serializes complex types in a slightly different way than the .NET XmlSerializer. The message about must declare the XML schema instance namespace in order to be deserialiezable in .NET  framework. The message below shows how the message must be altered to be XmlSerializer compliant:

    1
 <?xml version="1.0" encoding="utf-8"?>
    2 <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    3     <SOAP-ENV:Header />
    4     <SOAP-ENV:Body>
    5         <ForetakSokResponse xmlns="http://dbonline.no/test-webservices/xsd/ForetakInfo">
    6             <ForetakData xmlns="">
    7                 <Dunsnr>820584895</Dunsnr>
    8                 <Orgnr>974217368</Orgnr>
    9                 <KodeType></KodeType>
   10                 <KodeTekst>Aktivt</KodeTekst>
   11                 <Navn>MARKED BEDRIFT</Navn>
   12                 <Adresse>RING 43</Adresse>
   13                 <Postnr>0245</Postnr>
   14                 <Poststed>OSLO</Poststed>
   15             </ForetakData>
   16             <Meldinger xmlns="">
   17                 <MeldingsKode>999</MeldingsKode>
   18                 <MeldingsTekst>Kommando ferdig behandlet.</MeldingsTekst>
   19             </Meldinger>
   20         </ForetakSokResponse>
   21     </SOAP-ENV:Body>
   22 </SOAP-ENV:Envelope>

Notice the xmns:xsi attribute on the SOAP-ENV:Envelope element and the xmlns attributes on the ForetakData and the Meldinger elements. These are the only differences from the original message. And this message deserializes without a problem.

Detecting differences in a seemingly correct SOAP envelope and the envelope the proxy expects requires profound knowledge of WSDL and SOAP implementations, and is generally difficult. I use Think Tectures excellent Web Service Contract First (WSCF) tool to create an ASP.NET mock implementation of the service and compare the response messages from this service to the response messages from the web service I’m having interop problems with. This turns the painstaking task of spotting the differences into a breeze.

Changing the original services was not an option, so this problem had to be solved in the client. Luckily; the .NET SOAP client implementation has been designed with extensibility in mind. You can extend the request and response processing pipes in any way you like through SoapExtensions. A SoapExtension enables you to do custom processing on the request or response at four different stages; before and after serialization and before and after deserialization.

With our challenge we need to alter the SOAP envelope before it is deserialized by the client. In other words we need to hook our SoapExtension in at the BeforeDeserialize stage.

Below is the complete C# source code for a SoapExtension that makes the SOAP envelope .NET compliant. The attributes needed by the XmlSerialier to deserialize the message are injected in the InjectNamespaceDeclarations method at the end of the message.

    1 using System;
    2 using System.IO;
    3 using System.Web.Services.Protocols;
    4 
    5 namespace DBClient
    6 {
    7     public class NamespaceInjectorExtension : SoapExtension
    8     {
    9         private Stream oldStream;
   10         private Stream newStream;
   11         public override Stream ChainStream(Stream stream)
   12         {
   13             oldStream = stream;
   14             newStream = new MemoryStream();
   15             return newStream;
   16         }
   17         public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute)
   18         {
   19             return attribute;
   20         }
   21         public override object GetInitializer(Type serviceType)
   22         {
   23             return typeof(NamespaceInjectorExtension);
   24         }
   25         public override void Initialize(object initializer)
   26         {
   27             return;
   28         }
   29         public override void ProcessMessage(SoapMessage message)
   30         {
   31             switch (message.Stage) 
   32             {
   33                 case SoapMessageStage.BeforeDeserialize:
   34                     InjectNamespaceDeclarations(message);
   35                     break;
   36                 case SoapMessageStage.BeforeSerialize:
   37                 case SoapMessageStage.AfterSerialize:
   38                 case SoapMessageStage.AfterDeserialize:
   39                     break;
   40                 default:
   41                     throw new ArgumentException(string.Format("Invalid Stage: {0}",message.Stage.ToString()));
   42             }
   43         }
   44         private void InjectNamespaceDeclarations(SoapMessage message)
   45         {
   46             TextReader reader = new StreamReader(oldStream);
   47             string xml=reader.ReadToEnd();
   48 
   49             int nsDeclPos=xml.IndexOf("xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"")+"xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"".Length;
   50             xml=xml.Insert(nsDeclPos," xmlns:xsi=\http://www.w3.org/2001/XMLSchema-instance\");
   51             int foretakDataPos=xml.IndexOf("<ForetakData>")+"<ForetakData".Length;
   52             xml=xml.Insert(foretakDataPos," xmlns=\"\"");
   53             int meldingerPos=xml.IndexOf("<Meldinger>")+"<Meldinger".Length;
   54             xml=xml.Insert(meldingerPos," xmlns=\"\"");
   55 
   56             StreamWriter writer=new StreamWriter(newStream);
   57             writer.Write(xml);
   58             writer.Flush();
   59             newStream.Position=0;
   60         }
   61     }
   62 }

To attach the SoapExtension to an operation on the proxy you’ll also have to develop a SoapExtensionAttribute. Since the implantation of such an attribute is straight forward, the source code has been eluded. We need to declare this attribute on the method for the operation in the proxy. The code for this is shown below.

   61 [NamespaceInjectorExtension]
   62 [System.Web.Services.Protocols.SoapHeaderAttribute("BrukerAutorisasjonValue")]
   63 [System.Web.Services.Protocols.SoapDocumentMethodAttribute("http://www.dbonline.no/test-webservices/service/ForetakSok", Use=System.Web.Services.Description.SoapBindingUse.Literal, ParameterStyle=System.Web.Services.Protocols.SoapParameterStyle.Bare)]
   64 [return: System.Xml.Serialization.XmlElementAttribute("ForetakSokResponse", Namespace="http://dbonline.no/test-webservices/xsd/ForetakInfo")]
   65 public ForetakSokResponse sokForetak([System.Xml.Serialization.XmlElementAttribute(Namespace="http://dbonline.no/test-webservices/xsd/ForetakInfo")] ForetakSok ForetakSok)
   66 {
   67     object[] results = this.Invoke("sokForetak", new object[] {
   68                                                                     ForetakSok});
   69     return ((ForetakSokResponse)(results[0]));
   70 }

We are now able to invoke our web services as we are accustomed to and the problem is solved.

The Conclusion
The WS-I organization is working to promote web services interoperability across platforms, operating systems and programming languages. This work is important to ensure that web services become the lingua franca it has the potential to become. Unfortunately, not all SOAP implementations aren’t interoperable. When you’re experiencing interop problems, message transformations is a powerful tool. The solution above does lightweight string manipulation to solve the problem. On a larger scale, the SoapExtension could easily be extended to perform XSLT transformations on the message to handle more complex interop problems.