Simon Fell > Its just code > SOAP Headers with .NET

Thursday, August 11, 2005

Never a dull day looking at SOAP interop problems (plenty of frustrating ones though). Ran into some interesting behavior in .NET 1.1 when it gets faced with a WSDL that contains headers that are simple types (such as you might do if you were structuring something like WS-Addressing does, with lots of top level soap headers that have simple values). First off, a simple baseline, here is test_noheaders.wsdl, describes a simple services that takes 2 ints, and returns an int, has no headers, everything works as you've come to expect. Now, for the first curve ball, test_1header.wsdl is the exact same WSDL but defines an additional header, thusly

<s:element name="ValidFrom" type="s:dateTime"/>
Along with the relevent entries in the messages and binding etc. Now running wsdl.exe against this WSDL produces a few unexpected results. It generates a new class to represent the header, which is a fine approach, but the generated class looks somewhat suspect.
[System.Xml.Serialization.XmlTypeAttribute(Namespace="http://www.w3.org/2001/XMLSchema")]
[System.Xml.Serialization.XmlRootAttribute("ValidFrom", Namespace="http://test.sforce.com/", IsNullable=false)]
public class dateTime : System.Web.Services.Protocols.SoapHeader {
    
    /// <remarks/>
    [System.Xml.Serialization.XmlTextAttribute()]
    public string[] Text;
}
Huh, why is the value a string array and not a DateTime ?, this leads to a rather hokey client experience of
TestService ts = new TestService();
ts.ValidFrom = new dateTime();
ts.ValidFrom.Text = new String[1];
ts.ValidFrom.Text[0] = XmlConvert.ToString(DateTime.Now.AddSeconds(-1));
Console.WriteLine("{0}+{1}={2}", 1, 2, ts.add(1,2));
But, at least you can run it, and it does the right thing on the wire. On to the next example, test_2headers.wsdl, this adds yet another header of the same type, running wsdl.exe now gives us this, which makes even less sense than last time.
[System.Xml.Serialization.XmlTypeAttribute(Namespace="http://www.w3.org/2001/XMLSchema")]
[System.Xml.Serialization.XmlRootAttribute("ValidFrom", Namespace="http://test.sforce.com/", IsNullable=false)]
public class dateTime : System.Web.Services.Protocols.SoapHeader {
    
    /// <remarks/>
    [System.Xml.Serialization.XmlTextAttribute()]
    public string[] Text;
}

/// <remarks/>
[System.Xml.Serialization.XmlTypeAttribute(TypeName="dateTime", Namespace="http://www.w3.org/2001/XMLSchema")]
[System.Xml.Serialization.XmlRootAttribute("ValidUntil", Namespace="http://test.sforce.com/", IsNullable=false)]
public class dateTime1 : System.Web.Services.Protocols.SoapHeader {
    
    /// <remarks/>
    [System.Xml.Serialization.XmlTextAttribute()]
    public string[] Text;
}
Why does it need 2 different classes to represent the same type ?, note how one has a TypeName and one doesn't, also strange. But wait, it gets worse, this code compiles but doesn't run!, attempt to use the generated code and you'll be faced with
Unhandled Exception: System.InvalidOperationException: Method TestService.add can not be reflected. ---> System.InvalidOperationException: 
There was an error reflecting 'dateTime1'. ---> System.InvalidOperationException: There was an error reflecting type 'dateTime1'. ---> 
System.InvalidOperationException: Types dateTime1 and dateTime both use the XML type name, dateTime, from namespace
http://www.w3.org/2001/XMLSchema. Use XML attributes to specify a unique XML name and/or namespace for the type.
   at System.Xml.Serialization.XmlReflectionImporter.GetTypeMapping(String typeName, String ns, TypeDesc typeDesc)
   at System.Xml.Serialization.XmlReflectionImporter.ImportStructLikeMapping(StructModel model, String ns)
   at System.Xml.Serialization.XmlReflectionImporter.ImportTypeMapping(TypeModel model, String ns, ImportContext context, String dataType, Boolean repeats)
Grrhhh, back to the drawing board, I wonder if .NET 2.0 and Indigo repeat this braindeadedness?

Update: If you make the header element restrictions of the simple types (e.g. test_2headersB.wsdl), that'll fix the runtime reflection errors, but will still leave you with the dumbass string [] value instead of a DateTime.

<s:element name="ValidUntil">
 <s:simpleType>
      <s:restriction base="s:dateTime"/>
 </s:simpleType>
</s:element>
This finally explains why this is like it is.