Click here to Skip to main content
Click here to Skip to main content

Having fun with the XmlSchemaProvider: Serializing object trees

By , 9 May 2006
 

Introduction

About a year ago, I became very enthusiastic. I found a new attribute in the framework which allows me to determine the WSDL contract which describes my objects. Used in conjunction with the IXmlSerializable interface, I can also make sure the serializer pumps out the correct XML, corresponding to the custom XML-Schema inside the WSDL. Why is this so important? The default XmlSerializer requires all fields to have public get / set properties. This means my Product object needs a public get/set ID property. I personally didn’t want a settable ID property, because it is the never-changing primary key for that object. Furthermore, I did not want to change the public interface of my objects just because they’ll be serialized into a SOAP message. Using XmlSchemaProvider with IXmlSerializable, you can fix all this; keep only the getter, and still serialize the ID.

Now, all of this sounds good of course, and there are various samples on the internet showing how you can use these two classes to serialize your own ‘Product’ objects. However, the examples I found only show how to serialize a single object, not a tree of objects, such as an Order-OrderItem combination or Order-Customer-Address. Is this more difficult then? Well, yeah, a tad-bit, because you’ll have to share the XML-Schema definition somehow.

Not finding the answer on the web, I dug into this problem myself, like I did about a year ago. The only difference is that this time, I have a working solution which I want to share with you guys.

Serializing an Order object

The following example will show how you can utilize XmlSchemaProvider for serializing an object-tree. This example uses two objects, Order and Product, with an Order containing multiple Products. The Product class contains a readonly ID property, and has a private default constructor. The Order class contains a readonly List<T> of Products and also a readonly ID property. Here, I’ll give attention only to the Order class, the Product class is not that interesting after reading up on the subject. Check the code to see how it has been built.

The Order class I’ll describe a bit further, since that’s where the magic happens.

The basic class definition for Order is as follows:

[XmlSchemaProvider("GetOrderSchema")]
public class Order : IXmlSerializable
{
    int _id;
    List<Product> _products = null;

    public int ID { get;}
    public List<Product> Products { get;}
}

To supply the correct schema to the serialization infrastructure, the XmlSchemaProvider attribute is applied to the class. Inside the method tied to the XmlSchemaProvider attribute, the schema for the Order class is constructed using the System.Xml object model, also the Product is included in the schema for the Order class using an XmlSchemaInclude:

public static XmlSchemaComplexType GetOrderSchema(
              XmlSchemaSet schemas)
{
    string tns = "http://diveinit.nl/schemas/test";
    string xmlns = "http://www.w3.org/2001/XMLSchema";

    XmlSchema schema = new XmlSchema();
    // Include the product schema as an include so it can
    // be referenced in the order schema.
    XmlSchema productSchema = Product.Schema;
    XmlSchemaInclude productSchemainclude =
        new XmlSchemaInclude();
    productSchemaInclude.Schema = productSchema;
    schema.Includes.Add(productSchemaInclude);
    ...
    ...
  //-- The rest of the method is ommited for brevity--//
}

Now that the WSDL has been set up correctly, it is time to serialize and deserialize the Order class. This is done using the IXmlSerializable interface, which requires you to build three methods. First up is the serialization using the WriteXml method. This method is also responsible for serializing the Product objects contained in the Order.

void IXmlSerializable.WriteXml(XmlWriter writer)
{
    string ns = "http://diveinit.nl/schemas/test";
    writer.WriteElementString("id", ns, _id.ToString());
    ICollection<Product> products = Products;
    if (products.Count > 0)
    {
        writer.WriteStartElement("ProductCollection");
        foreach (Product product in products)
        {
            writer.WriteStartElement("Product");
            ((IXmlSerializable)product).WriteXml(writer);
            writer.WriteEndElement();
        }
        writer.WriteEndElement();
    }
}

The root element is supplied for you, this means you don’t have to write this yourself inside the WriteXml method. So the first item to write is the private ID field of the Order object. Next up are all the Products contained in the Order. They are contained inside an extra element called "ProductCollection". Make sure to use the WriteXml method of the Product object so that is serialized correctly too.

Now for some deserialization. This is a bit more difficult because you do not know how many Products the Order contains, when deserializing. The XmlReader provides a handy method to determine this, which is called IsStartElement.

void IXmlSerializable.ReadXml(XmlReader reader)
{
    string ns = "http://diveinit.nl/schemas/test";
    reader.ReadStartElement();
    _id = reader.ReadElementContentAsInt("id", ns);
    if (reader.IsStartElement("ProductCollection", ns))
    {
        reader.ReadStartElement();
        while (reader.IsStartElement("Product", ns))
        {
            Product product = new Product();
            ((IXmlSerializable)product).ReadXml(reader);
            Products.Add(product);
        }
    }
    reader.ReadEndElement();
}

That is it. A fully functional example on how to use XmlSchemaProvider in conjunction with IXmlSerializable for all your serialization needs!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Wouter van Vugt
Web Developer Code Counsel
Netherlands Netherlands
Member
Wouter van Vugt is a Microsoft MVP with Office Open XML technologies and an independent consultant focusing on creating Office Business Applications (OBAs) with SharePoint, the Office 2007 system and related .NET technologies. Wouter is a frequent contributor to developer community sites such as OpenXmlDeveloper.org and MSDN and has published several white papers and articles as well a book available on line titled Open XML: the markup explained. Wouter is the founder of Code-Counsel, a Dutch company focusing on delivering cutting-edge technical content through a variety of channels. You can find out more about Wouter by reading his blog and visiting the Code-Counsel Web site.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralTrying to build public static XmlQualifiedName GetMySchema(XmlSchemaSet xs)memberhndavis118 Feb '10 - 5:43 
Can you point me in the right direction to build the Schema Correctly;
QuestionBug?memberzux20 Nov '08 - 22:14 
When debugging given sample, methods that provide schemas like:
 
public static XmlSchemaComplexType GetProductSchema(XmlSchemaSet schemas)
 
are not invoked.
 
But when it is modified like:
 
public static XmlQualifiedName GetProductSchema(XmlSchemaSet schemas)
{
XmlSchema schema = Schema;
schemas.Add(schema);
 
return new XmlQualifiedName("Product_Type", "http://diveinit.nl/schemas/test");
}
 
it works fine.
AnswerRe: Bug?memberzux20 Nov '08 - 22:36 
I've missed to specify, this is for VS2008.
GeneralRe: Bug? [modified]memberishimakmola15 Mar '09 - 10:29 
I can't see that those static schemas methods GetProductSchema, GetOrderSchema and Schema are involved in any way in VS2005 either.
However when you are adding web reference, they are involved in building schema.
 
modified on Sunday, March 15, 2009 5:02 PM

QuestionHow to reuse a child schema in multiple parents?memberwout de zeeuw1 Mar '07 - 10:48 
Hi Wouter,
 
Awesome article, I'm putting it to practise doing my own serialization!
 
Anyways, here are some deeper issues I ran into that you might have mitigated already:
 
- Suppose there's a child schema A, and there are 2 parent schema's that have A as a child. How do you prevent adding the same A schema to the XmlSchemaSet?
- What's the best way to include schema's for .NET types like System.Guid? (they are in namespace http://microsoft.com/wsdl/types/):
 
Wout

QuestionHave ReadEndElement forgoten or not?membermerkuriev_sergey6 Feb '07 - 13:02 
I think you forgot one ReadEndElement statement in
 

void IXmlSerializable.ReadXml(XmlReader reader)
{
reader.ReadStartElement(); //1 start
if (reader.IsStartElement("ProductCollection", ns))
{
reader.ReadStartElement(); //2 start
....
reader.ReadEndElement(); //2 end - this element forgot
}
reader.ReadEndElement();//1 end
}

General2 ways to make service added through VS work [modified]memberada cai20 Dec '06 - 1:38 
As is mentioned before by the author that in order to make this work we need to change the WSDL. Well, he's right. So the first solutions is to change part of the wsdl to this:
         <s:complexType name="Product_Type">
            <s:sequence>
               <s:element name="id" type="s:int" />
               <s:element name="name" type="s:string" />
            </s:sequence>
         </s:complexType>
         ....
         <s:complexType name="ProductCollection_Type">
            <s:sequence>
               <s:element minOccurs="0" maxOccurs="unbounded" name="Product" type="tns:Product_Type" />
            </s:sequence>
         </s:complexType>
Then use wsdl.exe to regenerate the service proxy through the changed wsdl
 
Or you can change the other way around, on the server side:
In Product.cs
change the code to
            void IXmlSerializable.ReadXml(XmlReader reader)
            {
                  string ns = "http://diveinit.nl/schemas/test";
                  reader.ReadStartElement();
                  _id = reader.ReadElementContentAsInt("ID", ns);
                  _name = reader.ReadElementContentAsString("Name", ns);
                  reader.ReadEndElement();
            }
 
            void IXmlSerializable.WriteXml(XmlWriter writer)
            {
                  string ns = "http://diveinit.nl/schemas/test";
                  writer.WriteElementString("ID", ns, _id.ToString());
                  writer.WriteElementString("Name", ns, _name);
            }
In Order.cs
change the code to
            public static XmlSchema Schema
            {
                  get
                  {
                     ...
                        XmlSchemaElement product = new XmlSchemaElement();
                        product.Name = "Product";
                        product.SchemaTypeName = new XmlQualifiedName("Product_Type", tns);
                        product.MinOccurs = 0;
                        product.MaxOccursString = "unbounded";
                        productSequence.Items.Add(product);
 
                        return schema;
                  }
            }
 
For me, I prefer the second solution, since it will not need the client to find out the layout of the field and interfere with the automation provided by VS(of course the user can easily find out the layout of the classes by checking the returned data in a explorer).
 
I post this, just in case someone like me failed to find out what to do correctly for this.
And thank you for this great article
 
Thanks, Ada
 

-- modified at 7:43 Wednesday 20th December, 2006
GeneralSerialize/Deserialize to Stringmembersmolesen16 Nov '06 - 3:00 
Hi
 
If I wan't to serialize/deserialize an Order to a String, what do I then have to do??
 
I've tried the following, but the fromText (deserializer) fails
 

Imports Library
Imports System.Text
Imports System.IO
Imports System.Xml
Imports System.Xml.Serialization
 
Module Module1
 
Sub Main()
Dim o As Order = New Order(0)
o.Products.Add(New Product(1111, "Product1"))
o.Products.Add(New Product(2222, "Product2"))
Dim xml As String = toText(o)
Dim o1 as Object=fromText(xml)
End Sub
 
Public Function toText(ByVal o As Order) As String
Dim xs As New XmlSerializer(GetType(Order))
Dim sw As New StringWriter
Dim xw As XmlTextWriter = New XmlTextWriter(sw)
xs.Serialize(xw, o)
Return sw.GetStringBuilder.ToString
End Function
 
Public Function fromText(ByVal xml As String) As Object
Dim xs As New XmlSerializer(GetType(Order))
Dim ae As New UTF8Encoding
Dim ms As New MemoryStream(ae.GetBytes(xml))
Dim o As Object = xs.Deserialize(ms)
Return o
End Function
End Module

Generalthanksmemberzarrandreas16 Jul '06 - 4:21 
cool article.
thanks!Smile | :) Smile | :)
GeneralMixing the Serializable Attribute and IXmlSerializablememberServing4Him5 Jun '06 - 12:29 
Is it possible to mix classes using the SerializableAttribute (ie:
[Serializable]
public enum EntityType
{
Type1,
Type2,
Type3,
Type4
}
) and Classes using the IXmlSerializable Interface?

GeneralRe: Mixing the Serializable Attribute and IXmlSerializablememberWouter van Vugt6 Jun '06 - 23:40 
I believe you can yes.
 
Wouter van Vugt
 
Trainer - Info Support
http://www.infosupport.com

QuestionRe: Mixing the Serializable Attribute and IXmlSerializablememberServing4Him8 Jun '06 - 9:19 
How would you do that?
I'm trying something like this:

public void ReadXml(XmlReader reader)
{
reader.ReadStartElement("EntityType");
type = (EntityType)reader.ReadContentAs(typeof(EntityType), null);
reader.ReadEndElement();
...

But It's not working.

GeneralSharing implementationmemberMarkus Rennings17 May '06 - 6:53 
Hi Wouter,
 
that's excatly what I need for my project Cool | :cool: . But you should mention that you are sharing the implementation of your objects between client and server. If you add a webreference in visual studio the generated proxy looks a little bit different, than your pached proxy class. So I assume that the interoperability of your solution is not very high, or have you teseted this with other clients like Java Confused | :confused: . I let visual studio generate the serializer classes and analyse them with reflector. For a regular proxy the serializer does all the type conversions and uses the public fields. With the shared implementation the serializer notice the IXMLSerializable interface and just call the interface methods. So I think this could increase performance;)
 
Thanx Markus
GeneralRe: Sharing implementationmemberWouter van Vugt17 May '06 - 9:07 
Hi Markus,
 
what is going on in the client isn't really any business of the server classes. You can re-use the library with the business objects if you want, but the WSDL should be enough, and about the same like when doing it with WSDL.exe. It can also be difficult to re-use the business object library because the business objects might make calls to the back-end system that are not available on the client side. You can see this article as an example on how to create an object layer that serializes correctly into WSDL, but could for instance also be accessible using remoting, or the UI layer.

I personaly use this kind of code to access the business object layer of my portal server using WSDL. The client will use generated objects using WSDL.exe, but my UI layer uses the business objects directly. This saves me from building an extra layer in my server application to allow webservice access.
 
Hope it helps!
 
Regards, Wouter
 
Wouter van Vugt
 
Trainer - Info Support
http://www.infosupport.com

GeneralRe: Sharing implementationmemberMarkus Rennings17 May '06 - 23:08 
Hi Wouter,
 
I think your example is great if you are looking for a way to control the process of XML serialization. Bu I don't agree in the point that you show how to serializes correctly into WSDL.
The WSDL generated by your example couldn't be understood by the normal XML serializer.
I tested this with a proxy generated from the VisualStudio and call the GetOrder(1) method.
So the representation of your tree and especially the collection of products in the order class isn't correct according to the soap spec. Your solution works only if you share the implementation of the business objects.
I think it's not worth worrying about to generate a schema with all the detailed information about the fields inside the class. It would be enough to declare it like the dataset and say the inner part is a sequence of xs:any. If you using this approach and are forced to share the implementation, you can serialize the members of the class with runtime serialization and encode them as base64 arrays. This allows you to serialize any graph, even with circular references.
 
Regards Markus
GeneralRe: Sharing implementationmemberWouter van Vugt17 May '06 - 23:11 
Hi Markus,
 
what I am basically trying to say is that you can control the WSDL being generated. If it is not ok this way, alter the generated WSDL, and serialize the object differently. This should not be a big hassle, and you should not need to use the object library on the clients end.
 
Regards, Wouter
 
Wouter van Vugt
 
Trainer - Info Support
http://www.infosupport.com

Generalvery nice!memberspamcatcher66617 May '06 - 6:35 
I've been nothing but confused until now. Everyone does have examples on how to do it, but some have element based, some have type based, some have localized elements (?A?! how do I work with that?! haha) But you're the only one with the trees. Thanks a bunch. I really like your paradigm of exposing the schema via a static property, that will no doubt come in handy in other ways someday too.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130516.1 | Last Updated 9 May 2006
Article Copyright 2006 by Wouter van Vugt
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid