Andrew Cooper
Andrew Cooper

Reputation: 32586

Serialising SOAP parameters in C#

I'm trying to write a .Net client to a vendor's SOAP Service, but I'm having trouble getting the parameters to the SOAP messages to serialise to a form that the service recognises.

Using wsdl.exe I generated a service proxy class which works fine in itself. However, one of the messages takes an argument which is an array of key/value pairs - this is the bit I'm having problems with.

The WSDL for the message is:

  <message name='Execute'>
    <part name='ContextHandle' type='xsd:string'/>
    <part name='ScriptLanguage' type='xsd:string'/>
    <part name='Script' type='xsd:string'/>
    <part name='Params' type='xsd:anyType'/>
  </message>

The service proxy class has this code:

[System.Web.Services.WebServiceBindingAttribute(Name="EngineSoapBinding", Namespace="http://www.smarteam.com/dev/ns/iplatform/embeddedscripts/wsdl/")]
internal partial class Service : System.Web.Services.Protocols.SoapHttpClientProtocol {

    ....

    [System.Web.Services.Protocols.SoapRpcMethodAttribute("http://www.smarteam.com/dev/ns/iplatform/embeddedscripts/action/Execute", RequestNamespace="http://www.smarteam.com/dev/ns/iplatform/embeddedscripts", ResponseNamespace="http://www.smarteam.com/dev/ns/iplatform/embeddedscripts", Use=SoapBindingUse.Literal)]
    [return: System.Xml.Serialization.SoapElementAttribute("Result")]
    public object Execute(string ContextHandle, string ScriptLanguage, string Script, object Params) {
        object[] results = this.Invoke("Execute", new object[] {
                    ContextHandle,
                    ScriptLanguage,
                    Script,
                    Params});
        return ((object)(results[0]));
    }

    ....

}

In the vendor's documentation the Params argument should be an array of key/value pairs.

I've been able to capture network traffic from another working client to this service and got the following example of a message that the service recognises (SOAP envelope removed and formatted for clarity):

<STES:Execute xmlns:STES="http://www.smarteam.com/dev/ns/iplatform/embeddedscripts" xmlns:sof="http://www.smarteam.com/dev/ns/SOF/2.0">
    <ContextHandle>0019469#00228</ContextHandle>
    <ScriptLanguage>javascript</ScriptLanguage>
    <Script><![CDATA[Context.Result = Context.Params("Number");]]></Script>
    <Params SOAP-ENC:arrayType="sof:DictionaryItem[2]">
        <sof:DictionaryItem>
            <key xsi:type="xsd:string">Number</key>
            <value xsi:type="xsd:int">10</value>
        </sof:DictionaryItem>
        <sof:DictionaryItem>
            <key xsi:type="xsd:string">Hello</key>
            <value xsi:type="xsd:string">World</value>
        </sof:DictionaryItem>
    </Params>
</STES:Execute>

I've tried various data structures for the Params argument, but nothing I've tried gives anything close to this XML serialisation.

The closest I've been able to get is to write a DictionaryItem class which implements IXmlSerializable with the following WriteXml method:

    public void WriteXml(XmlWriter writer)
    {
        writer.WriteStartElement("key");
        writer.WriteValue(Key);
        writer.WriteEndElement();
        writer.WriteStartElement("value");
        writer.WriteValue(Value);
        writer.WriteEndElement();
    }

I then give the Params argument a List<DictionaryItem> which results in the following serialization on the wire.

<Execute xmlns="http://www.smarteam.com/dev/ns/iplatform/embeddedscripts">
    <ContextHandle xmlns="">0022541#00228</ContextHandle>
    <ScriptLanguage xmlns="">javascript</ScriptLanguage>
    <Script xmlns="">Context.Result = Context.Params("Number");</Script>
    <Params xmlns="">
        <DictionaryItem xmlns="http://www.smarteam.com/dev/ns/iplatform/embeddedscripts">
            <key>Number</key>
            <value>10</value>
        </DictionaryItem>
        <DictionaryItem xmlns="http://www.smarteam.com/dev/ns/iplatform/embeddedscripts">
            <key>Hello</key>
            <value>World</value>
        </DictionaryItem>
    </Params>
</Execute>

Can anyone point me in the right direction to get this working?

** Update **

It doesn't surprise me in the slightest that this vendor is using a deprecated message format. Their whole product is a mess, and I'd gladly ditch it if I could. It's written in .Net, but has a COM API and this web service in a deprecated format. They do supply a client library for the web service, but it's writtn in Java. Huh?

I'm going to go back to my original idea and write a wrapper around the Java client, using ikvmc. At least I know I can get that to work, even if all the type conversion will be messy.

As for picking an answer @Cheeso and @Aaronaught have both been very helpful, so I've flipped a coin and given it to @Cheeso.

Upvotes: 3

Views: 9807

Answers (2)

Cheeso
Cheeso

Reputation: 192597

The example message you've shown there shows a message using what is known as "SOAP Section 5 encoding". SOAP encoding (not to say SOAP) was deprecated by all major tools and services vendors a looooong time ago, due to problems with compatibility and interoperability. Like in 2004 or so.

Seriously. Nobody should be using that stuff any longer. There is no excuse.

But even so, you should be able to get it to work.

The challenge is to get the XML namespaces right on each of the request and response elements. Just looking at the example request message - the one that "works" - you can see it's sort of deranged. There's the namespace with the STES prefix on the toplevel request element - Execute. Then, all the child elements get no namespace at all. This is weird.

The same weird on/off namespace thing occurs in the Params array. The wrapper element is in the namespace with the sof prefix. But the key and value child elements are not in that namespace- they are in no namespace at all.

In your attempt, you have a couple mismatches then;

  • In your case, the DictionaryItem element is in the http://www.smarteam.com/dev/ns/iplatform/embeddedscripts namespace. It should be in the http://www.smarteam.com/dev/ns/SOF/2.0 namespace.

  • In your case, the key and value elements are in the http://www.smarteam.com/dev/ns/iplatform/embeddedscripts namespace. They should be in no namespace at all.

Normally a proper WSDL means you do not have to worry about any of these things. I am puzzled as to why your WSDL is not generating a proxy that does the right thing. What I recommend people do in this case is get a wSDL that really works. Oftentimes that means writing a mock version of the service in ASMX.

If you can generate a service in ASMX that accepts messages of the form accepted by the real service, and if you can generate a client that interacts with that ASMX service, then the client should also work with the real service. the reason I recommend ASMX is that it's so easy to tweak and retry things.

Toward that end, here's what I cam eup with.

<%@ WebService Language="c#"
      Class="Cheeso.CooperService"
      %>

using System.Web.Services;
using System.Web.Services.Description;
using System.Web.Services.Protocols;
using System.Xml.Serialization;
using System.Collections;

namespace Cheeso
{
    [SoapType(Namespace="http://www.smarteam.com/dev/ns/SOF/2.0")]
    public class DictionaryItem
    {
        public string key { get; set; }
        public string value { get; set; }
    }


    [System.Web.Services.WebService
     (Name="EngineSoapBinding",
      Namespace="http://www.smarteam.com/dev/ns/iplatform/embeddedscripts/wsdl/")]
    public class CooperService : System.Web.Services.WebService
    {

        [WebMethod]
        [SoapRpcMethod
         ("http://www.smarteam.com/dev/ns/iplatform/embeddedscripts/action/Execute",
          RequestNamespace="http://www.smarteam.com/dev/ns/iplatform/embeddedscripts",
          ResponseNamespace="http://www.smarteam.com/dev/ns/iplatform/embeddedscripts",
          Use=SoapBindingUse.Encoded)]
        [return: System.Xml.Serialization.SoapElementAttribute("Result")]
        public object Execute(string ContextHandle,
                              string ScriptLanguage,
                              string Script,
                              DictionaryItem[] Params)
        {
            return "The answer is 42. What is the question?";
        }

    }
}

This ASMX file should produce a WSDL and an interface that is equivalent to your real service. Generate the WSDL from it (using the ?wsdl query), and then write a test client. Examine the messages on the wire and tweak as necessary.

You can see that I applied a REAL type to the Params array. Also I decorated that type with the SoapType attribute and specified the desired xml namespace.

In your problem statement you didn't describe the response message. You'll need to go through a similar exercise - tweaking and adjusting - in order to shape the response message "expected" by your client to match the responses actually generated by the real service.

Also, remember that the xmlns prefixes are not significant. It's nto the prefix you need to match, it's the XML namespace itself. You don't need STES:Execute. You can use any namespace prefix, as long as it maps to the correct xml namespace.

Good luck.

If you get a chance, convince them to move to a WS-I compliant service interface. Interop is much easier when the service complies with the WS-I recommendations.


EDIT

This is a trace of the actual message from the client, generated using that WSDL:

<soap:Envelope
    xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/"
    xmlns:tns="http://www.smarteam.com/dev/ns/iplatform/embeddedscripts/wsdl/"
    xmlns:types="http://www.smarteam.com/dev/ns/iplatform/embeddedscripts/wsdl/encodedTypes"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <q1:Execute xmlns:q1="http://www.smarteam.com/dev/ns/iplatform/embeddedscripts">
      <ContextHandle xsi:type="xsd:string">00913983</ContextHandle>
      <ScriptLanguage xsi:type="xsd:string">Canadian, eh?</ScriptLanguage>
      <Script xsi:type="xsd:string">To be or not to be....</Script>
      <Params href="#id1" />
    </q1:Execute>
    <soapenc:Array id="id1"
                   xmlns:q2="http://www.smarteam.com/dev/ns/SOF/2.0"
                   soapenc:arrayType="q2:DictionaryItem[2]">
      <Item href="#id2" />
      <Item href="#id3" />
    </soapenc:Array>
    <q3:DictionaryItem id="id2"
                       xsi:type="q3:DictionaryItem"
                       xmlns:q3="http://www.smarteam.com/dev/ns/SOF/2.0">
      <key xsi:type="xsd:string">17</key>
      <value xsi:type="xsd:string">s9dkjdls</value>
    </q3:DictionaryItem>
    <q4:DictionaryItem id="id3"
                       xsi:type="q4:DictionaryItem"
                       xmlns:q4="http://www.smarteam.com/dev/ns/SOF/2.0">
      <key xsi:type="xsd:string">fish</key>
      <value xsi:type="xsd:string">barrel</value>
    </q4:DictionaryItem>
  </soap:Body>
</soap:Envelope>

Even though this is different than your target message, this should be parseable by your server-side, if it conforms to SOAP v1.1 section 5 encoding spec. This request message uses the "multiple reference" serialization whereas your example target message uses "single reference". But they should be equivalent to the server side. Should be.

But as I said originally, there were lots of problems getting SOAP section 5 encoding to work interoperably.

Upvotes: 2

Aaronaught
Aaronaught

Reputation: 122664

The WCF solution is really rather simple, just use these classes in your import:

[DataContract(Namespace = "http://www.smarteam.com/dev/ns/iplatform/embeddedscripts")]
[KnownType(typeof(SofDictionaryItem[]))]
[XmlSerializerFormat(Style = OperationFormatStyle.Rpc, Use = OperationFormatUse.Encoded)]
public class Execute
{
    [DataMember(Order = 0)]
    public string ContextHandle { get; set; }

    [DataMember(Order = 1)]
    public string ScriptLanguage { get; set; }

    public string Script { get; set; }

    [DataMember(Name = "Script", Order = 2, EmitDefaultValue = false)]
    private CDataWrapper ScriptCData
    {
        get { return Script; }
        set { Script = value; }
    }

    [DataMember(Order = 3)]
    public object Params { get; set; }
}


[DataContract(Namespace = "http://www.smarteam.com/dev/ns/SOF/2.0", Name = "DictionaryItem")]
public class SofDictionaryItem
{
    [DataMember]
    public object Key { get; set; }

    [DataMember]
    public object Value { get; set; }
}

I'm using Marc Gravell's CDataWrapper here to force the CDATA tags around the Script.

The DataContractSerializer will generate output that's nearly identical to what you're seeing over the wire already:

<Execute xmlns="http://www.smarteam.com/dev/ns/iplatform/embeddedscripts" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
    <ContextHandle>0019469#00228</ContextHandle>
    <ScriptLanguage>javascript</ScriptLanguage>
    <Script><![CDATA[Context.Result = Context.Params("Number")]]></Script>
    <Params i:type="a:ArrayOfDictionaryItem" xmlns:a="http://www.smarteam.com/dev/ns/SOF/2.0">
        <a:DictionaryItem>
            <a:Key i:type="b:string" xmlns:b="http://www.w3.org/2001/XMLSchema">Number</a:Key>
            <a:Value i:type="b:int" xmlns:b="http://www.w3.org/2001/XMLSchema">10</a:Value>
        </a:DictionaryItem>
        <a:DictionaryItem>
            <a:Key i:type="b:string" xmlns:b="http://www.w3.org/2001/XMLSchema">Hello</a:Key>
            <a:Value i:type="b:string" xmlns:b="http://www.w3.org/2001/XMLSchema">World</a:Value>
        </a:DictionaryItem>
    </Params>
</Execute>

The only potential problem is the ArrayOfDictionaryItem, which is a convention that .NET always seems to use for array types. If you actually look at the generated WSDL for these types, you'll see that it actually references the soapenc:arrayType, but that may not be sufficient here if the endpoint is unaware of this convention. If that is the case, then unfortunately I think you'll have to resort to IXmlSerializable, because I've never been able to find a way to disable the ArrayOf generation in .NET.

As Cheeso mentions, the RPC/encoded SOAP format is officially deprecated by the WS-I and should never be used in production services anymore. One of the reasons it was deprecated was because it was lousy for interop and painful to implement. If possible, you really should talk to the vendor about getting an update, which ought to be using the standard document/literal wire format.

Upvotes: 2

Related Questions