Christian Weyer: Smells like service spirit

What's first?

The Web services empire strikes back - Custom XML Serialization

Part 1: The Web services empire strikes back - Introductory thoughts
Part 2: The Web services empire strikes back - Inner Workings
Part 3: The Web services empire strikes back - Web Services in Visual Studio 2005
Part 4: The Web services empire strikes back - WS-I BP Conformance

One of the most obvious weaknesses when working with ASMX was the ability to just passively take influence on how the .NET types got transferred into XML and back. This process of serialization and deserialization was quite limited through the usage of a certain set of .NET attributes that could be used to decorate your types. Fortunately there is rescue now: the IXmlSerializable interface and the extensibility hook it provides for XmlSerializer-based XML (de-)serialization. Some of you might say that this interface has been around since the very early days of the .NET Framework, so why is it new then? Well, to tell it with a little joke, some people tend to call it ISupportDataSet and not IXmlSerializable. This is because until version 2.0 of the .NET Framework this interface was used solely by the System.Data.DataSet type and some checks for this type had been hard coded in one or the other tools. So, let's forget about those old days, IXmlSerializable has never been supported officially then, but now it is.

In order to define your very own custom serialization and deserialization process you just need to follow a few steps. There are two essential methods in the interface you have to implement. The two methods are WriteXml and ReadXml. The first one, WriteXml, converts an object into its XML representation. You have to write sufficient information to the XmlWriter stream to allow the ReadXml method (see below) to reconstitute your object.
The ReadXml method generates an object from its XML representation. It must reconstitute your object using the information that was written before by the WriteXml method. When this method is called, the reader stream is positioned at exactly the point that the WriteXml method began writing. Typically, this is immediately after the start tag indicating the beginning of your serialized object. When this method returns, it must have consumed exactly those nodes that were written by the WriteXml method, so that reader stream is positioned where that method left it. That's it. There is actually one more official method to implement in the IXmlSerializable interface, but this is just there for legacy purposes.

The GetSchema method is only used internally by the DataSet type and does not give you the control it supposes. To give a developer full schema generation control he needs to implement a static method that generates the schema. The type class has then to be marked with the XmlSchemaProvider attribute. The XmlSchemaProviderAttribute class indicates the name of the method to call to get the schema added to the internal schema set. It takes an XmlSchemaSet and returns the XmlQualifiedName of the starting element. Accordingly, the GetSchema method should just return null for convenience.

With the new version of the .NET Framework there are several classes that now implement this interface by default. Table 1 shows all of these types. Please note that now besides the DataSet you can also easily XML serialize a DataTable (however, it gets wrapped into a DataSet). If anybody is still questing for a serializable HashTable, it is not here. But now you have the official possibility to implement it on your own. Please have a look at Listing 1 to see one possible solution to the serializable dictionary problem.

IXmlSerialzable derived types
System.Data.DataSet
System.Data.DataTable
System.Data.SqlTypes.SqlBinary
System.Data.SqlTypes.SqlBoolean
System.Data.SqlTypes.SqlByte
System.Data.SqlTypes.SqlBytes
System.Data.SqlTypes.SqlChars
System.Data.SqlTypes.SqlDateTime
System.Data.SqlTypes.SqlDecimal
System.Data.SqlTypes.SqlDouble
System.Data.SqlTypes.SqlGuid
System.Data.SqlTypes.SqlInt16
System.Data.SqlTypes.SqlInt32
System.Data.SqlTypes.SqlInt64
System.Data.SqlTypes.SqlMoney
System.Data.SqlTypes.SqlSingle
System.Data.SqlTypes.SqlString
System.Xml.XPath.XPathDocument

Table 1: Types in the .NET Framework 2.0 that implement IXmlSerializable


[XmlSchemaProvider("DictSchema")]
[XmlRoot("dictionary", Namespace =
  "
http://thinktecture.com/demos/dictionary", IsNullable = true)]
public class SerializableDictionary : IXmlSerializable
{
  const string NS = "
http://thinktecture.com/xml/serialization";
  private IDictionary dictionary;

  public SerializableDictionary()
  {
    dictionary = new Hashtable();
  }
  public SerializableDictionary(IDictionary dictionary)
  {
    this.dictionary = dictionary;
  }

  void IXmlSerializable.WriteXml(XmlWriter w)
  {
    w.WriteStartElement("dictionary", NS);
    foreach(object key in dictionary.Keys)
    {
      object value = dictionary[key];
      w.WriteStartElement("item", NS);
      w.WriteElementString("key", NS, key.ToString());
      w.WriteElementString("value", NS, value.ToString());
      w.WriteEndElement();
    }
    w.WriteEndElement();
  }

  void IXmlSerializable.ReadXml(XmlReader r)
  {
    r.Read();
    r.ReadStartElement("dictionary");
    while(r.NodeType != XmlNodeType.EndElement)
    {
      r.ReadStartElement("item", NS);
      string key = r.ReadElementString("key", NS);
      string value = r.ReadElementString("value", NS);
      r.ReadEndElement();
      r.MoveToContent();
      dictionary.Add(key, value);
    }
  }

  XmlSchema IXmlSerializable.GetSchema() { return null; }

  public static XmlQualifiedName DictSchema(XmlSchemaSet xss)
  {
    XmlSchema xs = XmlSchema.Read(new StringReader(
      "<xs:schema id='DictionarySchema' targetNamespace='" + NS +
      "' elementFormDefault='qualified' xmlns='" + NS + "' xmlns:mstns='" +
      NS + "' xmlns:xs='http://www.w3.org/2001/XMLSchema'><xs:complexType " +
      "name='DictionaryType'><xs:sequence><xs:element name='item' type='ItemType' " +
      "maxOccurs='unbounded' /></xs:sequence></xs:complexType><xs:complexType " +
      "name='ItemType'><xs:sequence><xs:element name='key' type='xs:string' />" +
      "<xs:element name='value' type='xs:string' /></xs:sequence></xs:complexType>" +
      "<xs:element name='dictionary' type='mstns:DictionaryType'></xs:element></xs:schema>"),
      null);
    xss.XmlResolver = new XmlUrlResolver();
    xss.Add(xs);
    return new XmlQualifiedName("DictionaryType", NS);
  }
}
Listing 1: Implementation for a serializable dictionary based on IXmlSerializable

This type can then be used in your WebMethod like this:

[WebMethod]
public SerializableDictionary GetDataList()
{
  Hashtable ht = new Hashtable();
  ht.Add(1, "ChristianW");
  ht.Add(2, "Ingo");
  ht.Add(3, "ChristianN");
  ht.Add(4, "Ralf");
  return new SerializableDictionary(ht);
}

Easy like that. When we would take a look at the WSDL generated for this Web service, there would be the proper schema emitted into the types section - just as defined in our DictSchema method above. I think this will open a whole new breed of document based Web services, now that developers have the power to control the exact layout of the XML schema.

Yes, there are some implementations for this for the current .NET Fx - but they are not supported and actually do emit wrong schema and WSDL.

Comments

No Comments