Tuesday, December 14, 2004

I'm Pro-xs:choice

There's been lots of debate about use of DataSets with web services. The issue is interoperability: the default WSDL representation of a DataSet is an arbitrary schema followed by arbitrary XML. That's just a bit on the loose side for most toolkits to deal with in the general case. However, one use case that drives the desire to use a DataSet in the first place is that the type of data to be returned is not known precisely until runtime. Lately, I discovered yet another corner of XmlSerializer that helps me deal with some of these situations without having to resort to using a DataSet.


It turns out that sometimes, although you don't know the exact type of data you'll be returning, you do know that it's going to be one of a known set of types. This is particularly common when you're returning the results of a database query: you might know that all your columns are either strings, booleans, integers, dates, or floating point numbers. If that's the case, there's an XML schema construct that allows you to say, “I want to have one of the following elements appear here.” It's called <xs:choice>, and here's a schema fragment that demonstrates its use:



<xs:element name=“item“>
  <xs:complexType>
    <xs:choice>
      <xs:element minOccurs=“1“ maxOccurs=“1“ name=“string“ type=“xs:string“ />
      <xs:element minOccurs=“1“ maxOccurs=“1“ name=“integer“ type=“xs:int“ />
      <xs:element minOccurs=“1“ maxOccurs=“1“ name=“boolean“ type=“xs:boolean“ />
      <xs:element minOccurs=“1“ maxOccurs=“1“ name=“date“ type=“xs:dateTime“ />
      <xs:element minOccurs=“1“ maxOccurs=“1“ name=“float“ type=“xs:float“ />
    </xs:choice>
  </xs:complexType>
</xs:element>


What this schema means is that when an “item” element appears, it must have as a child exactly one of the elements <string>, <integer>, <boolean>, <date>, or <float>. Further, if the child element is <string>, the contents of that element must be a string. But if the child element is <integer>, then the contents must be an integer, and so on and so forth. It's a reasonably “normal“ bit of schema, and while I have no idea what the support in various non-.NET toolkits looks like, support for it is a lot more likely to exist than for the vagaries of DataSet.


From an XML standpoint, this is a pretty nice thing. It lets us express exactly what we want, namely, that we'll tell you at runtime what our choice for the type of the data is. Where this gets really great is that it's supported by System.Xml.Serialization, which powers the ASP.NET Web Services stack.


Here's how it maps: you define a class with the usual attributes from the System.Xml.Serialization namespace. But when you get to the element that you want to represent as an <xs:choice>, you simply use more than one [XmlElement] attribute, and you use the overload that lets you specify a type. Here's what I mean:


[XmlRoot(”item”)]
public class Item
{
  private object value; 

  [XmlElement(“string“, typeof(string))]
  [XmlElement(“integer“, typeof(int))]
  [XmlElement(“boolean“, typeof(bool))]
  [XmlElement(“date“, typeof(DateTime))]
  [XmlElement(“float“, typeof(float))]
  public object Value
  {
    get { return value; }
    set { this.value = value; }
  }
}


When the serializer encounters this type during either serialization or deserialization, it will use the attributes to control the mapping between element names and .NET types. Which is to say, when the Value is a float, you'll get <float>, when the Value is string, you'll get a <string>, and (better still) vice versa! You have to do a type test and typecast at the other end, like this:


if (foo.Value is string) { DealWithString((string) foo.Value); }
else if (foo.value is float) { DealWithFloat((float) foo.Value); }
// etc.


But big deal. :)


By itself, this is a fairly powerful feature. And there's still more we can do, but I think I'll stop this entry now while this entry is still reasonably short. More later.

4 comments:

  1. Very interesting.



    As far as the arbitrary XML created by a DataSet, wouldn't a typed-DataSet be a decent alternative? I'm one of the few people that think typed-DataSets are the coolest thing since sliced bread. Plus the IDE will build you the XSD at no additional charge in some cases. :-)

    ReplyDelete
  2. I haven't looked at what the XML representation of a typed DataSet is, but presumably it's one of either two things:



    1) Equivalent to the serialization format for an array of strongly-typed objects.

    2) Equivalent to the serialization format of DataSet.



    If it's #1, great, but that doesn't let you vary type at runtime. If it's #2, then it's no different on the wire than using a DataSet. From a marshalling standpoint, you may as well just use an object array or a DataSet.



    Not saying they aren't useful, just that they don't let you do what multiple [XmlElement] declarations does.

    ReplyDelete
  3. Awesome awsome awesome. I'm in the midst of creating an XSD and xs:choice is one of my concerns when I'm ready to code the corresponding classes.



    So just thinking about it real quick, one isn't limited to the just Xml data types, but also custom types..



    Given an (simple) xml type defined in an XSD: <snip>



    <xs:simpleType name="HierarchicalNumberType">

    <xs:annotation>

    <xs:documentation>Type which represents a HierarchicalNumber</xs:documentation>

    </xs:annotation>

    <xs:restriction base="xs:token">

    <xs:pattern value="([A-Z|a-z]+|[\d]+)(\.([A-Z|a-z]+|[\d]+))*"/>

    </xs:restriction>

    </xs:simpleType>



    </snip>



    a corresponding C# type:

    [XmlElement(”HierarchicalNumberType”)]

    public class HierarchicalNumber {...}



    you could also include that in your choice

    [XmlElement(“HierarchicalNumber“, typeof(HierarchicalNumber))]

    [XmlElement(“Integer“, typeof(int))]

    public object Value()

    {

    get {...}

    set {...}

    }



    the same with complex types that may itself include an xs:choice -- ad infintium..



    ReplyDelete
  4. I'm 99% sure your HierarchicalNumber example will work, although I haven't tried it with anything other than simple types. I just don't see why they wouldn't be able to do it.



    I'm slightly less sure about nesting <xs:choice> constructs...although I suspect that would work as well.

    ReplyDelete