David Foderick's Blog - OnMaterialize()

Control WCF Serialization of Collections with IXmlSerializable

WCF doesn't always serialize custom collections of objects the way that you would like. For example, a Family may have a collection of Members.

    public class SerializeObjectModel
    {
        [DataContract(Name = "Family")]
        public class Family
        {
            [DataMember(Name = "Members")]
            public FamilyMembers MembersList = new FamilyMembers();
        }

        public class FamilyMembers : List<string>
        {
        }
}

If you load up the list with some data and serialize it through WCF (WCF uses the DataContractSerializer by default to serialize objects so you can simulate serialization in a test harness by calling the class directly)

            Family f = new Family();
            f.MembersList.Add("A");
            f.MembersList.Add("B");
            f.MembersList.Add("C");
            f.MembersList.Add("D");
            ...
            DataContractSerializer ser = new DataContractSerializer(typeof(Family));
            ser.WriteObject(cwriter, f);

It will serialize like this:

  <Family xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schema/
s.datacontract.org/2004/07/ObjectModel">
  <Members xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays
">
    <d2p1:string>A</d2p1:string>
    <d2p1:string>B</d2p1:string>
    <d2p1:string>C</d2p1:string>
    <d2p1:string>D</d2p1:string>
  </Members>
</Family>

That's fine. Now you add some additional members to the FamilyMembers class, maybe a list of family members that have been deleted fromt the list (granny passed away, RIP).

        public class FamilyMembers : List <string>
        {
            //DKF: added this list
            public List<string> DeletedList = new List<string>();

You run it through the WCF Serializer
            Family f = new Family();
            f.MembersList.Add("A");
            ...
            //WANT TO GET THIS SERIALIZED TOO!
            f.MembersList.DeletedList.Add("Dave Foderick");

and you don't see the deleted list! It looks exactly like the previous output. If the deleted list never gets to the other end of the wire then they never get removed from the database.

To get the list to serialize the way we would like, we have to get control of how WCF serialized it. This post is the golden nugget that describes the precedence of how lists are serialized. You see that what is serializing our list is #6 which apparently iterates through the collection and spits out the members, ignoring anything else that may be of interest to us on the list iteself. The only hook into the serialization process that has higher prececedence and will allow us the flexibility (CollectionDataContractAttribute won't do!) is IXmlSerializable. So modify the collection class to implement that interface.

        public class FamilyMembers : List<string> , IXmlSerializable
        {
            //DKF: added this list
            public List<string> DeletedList = new List<string>();

            #region IXmlSerializable Members

            public System.Xml.Schema.XmlSchema GetSchema()
            {
                return null;
            }

            public void ReadXml(XmlReader reader)
            {
                throw new Exception("The method or operation is not implemented.");
            }

            public void WriteXml(XmlWriter writer)
            {
                //First, serialize all the items in the base list
                writer.WriteStartElement("Items");
                foreach (string item in this)
                {
                    writer.WriteElementString("Member", item);
                }
                writer.WriteEndElement();

                //now, serialize all the items in the deleted list
                writer.WriteStartElement("DeletedItems");
                foreach (string deletedItem in DeletedList)
                {
                    writer.WriteElementString("Member", deletedItem);
                }
                writer.WriteEndElement();
            }

More code to write for sure but victory is ours!

<Family xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schema
s.datacontract.org/2004/07/ObjectModel">
  <Members>
    <Items>
      <Member>A</Member>
      <Member>B</Member>
      <Member>C</Member>
      <Member>D</Member>
    </Items>
    <DeletedItems>
      <Member>Dave Foderick</Member>
    </DeletedItems>
  </Members>
</Family>

To get this working right in a real app, you have to write the deserialization logic too. Here is a more realistic implementation (TDto is a generic parameter for a custom class that replaces string in the above samples).

        #region IXmlSerializable Members

        /// 
        /// Get xml schema
        /// In this case, we don't have any
        /// 
        /// 
        public System.Xml.Schema.XmlSchema GetSchema()
        {
            return null;
        }

        private DataContractSerializer GetSerializer()
        {
            Type dtoType = typeof(TDto);
            DataContractSerializer ser = new DataContractSerializer(dtoType);
            return ser;
        }

        /// 
        /// Deserialize the object
        /// 
        /// 

        public void ReadXml(XmlReader reader)
        {
            DataContractSerializer ser = GetSerializer();

            //read past the first element. This is the outermost element for the list
            reader.ReadStartElement();

            //read in the undeleted objects
            ReadXmlObjects(reader, "Items", this);

            //now read in the deleted list
            ReadXmlObjects(reader, "DeletedItems", this._deletedList);
        }

        private string PrefixedElementName(XmlReader reader, string elementName)
        {
            string retval = reader.Prefix;
            if (reader.Prefix.Length > 0)
            {
                retval += ":";
            }
            retval += elementName;
            return retval;
        }

        /// 
        /// read objects into list
        /// 
        /// 

        /// 

        /// 

        private void ReadXmlObjects(XmlReader reader,string elementName, IList list)
        {
            DataContractSerializer ser = GetSerializer();

            Type dtoType = typeof(TDto);
            string prefixedCollection = PrefixedElementName(reader,elementName);
            if (reader.IsStartElement(prefixedCollection))
            {
                reader.ReadStartElement();
                //TODO: very tricky!
                //element name must match classname. Is this always true???
                //another strategy is to embed a count of the number of object to 
                //read inside the xml. Then this code can be a foreach
                string prefixedClassName = PrefixedElementName(reader,dtoType.Name);
                while (reader.IsStartElement(prefixedClassName))
                {
                    TDto dto = (TDto)ser.ReadObject(reader);
                    list.Add(dto);
                }
                if (reader.IsStartElement() == false)
                {
                    reader.ReadEndElement();
                }
            }
        }

        /// 
        /// Serialize the object
        /// 
        /// 

        public void WriteXml(System.Xml.XmlWriter writer)
        {
            //This is the WCF default Serializer
            DataContractSerializer ser = new DataContractSerializer(typeof(TDto));

            //First, serialize all the items in the base list
            writer.WriteStartElement("Items");
            foreach (TDto item in this)
            {
                ser.WriteObject(writer, item);
            }
            writer.WriteEndElement();
            //ser.WriteObject(writer, this.Items);

            //now, serialize all the items in the deleted list
            writer.WriteStartElement("DeletedItems");
            foreach (TDto deletedItem in _deletedList)
            {
                ser.WriteObject(writer, deletedItem);
            }
            //ser.WriteObject(writer, _deletedList);
            writer.WriteEndElement();

        }

        #endregion
Download my test harness with sample code.
Posted: Jan 18 2007, 06:56 PM by admin | with no comments |
Filed under:

Comments

No Comments

Leave a Comment

(required) 

(required) 

(optional)

(required)