Article

This article is in continuation to this post Serialization in .NET Part 2

XML Serialization

In addition to the Binary and SOAP formatters, the .NET provides another type of formatter called XmlSerializer. This resides in the System.Xml.Serialization namespace. Working with this type is a little different from working with the Binary and SOAP formatters.

You can use this formatter to serialize the state of a given object in pure XML format. This is in contrast to XML data wrapped within a SOAP message. Since the SOAP-protocol specifics are not included, the XML stream produced is more compact which makes it ideal for transmission and storage. It enables an application to interoperate with other systems and exchange data in XML format. It is used by ASP.NET to generate XML Web service messages.

XmlSerializer does not pay any attention to the Serializable attribute. The class itself offers similar functionality to a certain extent. The serialization process is built into the XmlSerializer.

Restrictions apply to XML Serialization

By default, the locally-installed applications don't have any security restrictions in that they can access the private fields. However, certain security restrictions apply to applications running from the network. XmlSerializer is designed to run in such environments that have limited permissions. Therefore, XmlSerializer has to restrict itself to use the shallow serialization technique. It can serialize only the public members that can be accessed even without any special permission settings.

Several restrictions have been imposed on the classes that the XmlSerializer can process. It will only then be able to successfully deserialize the objects of applications that are running with limited security permissions. The types of restrictions are as follows:

a. Before the XmlSerialer.Deserialize() method initializes all the fields of a class, it should be able to create an instance of the class in insecure environments. However, creating the instance without making a call to the default constructor is possible only if you grant certain permissions to the executing application. Therefore, you must provide a default constructor for the class that you want XmlSerializer to serialize.

b. The class must be declared public. Processing the internal and private classes is only possible if you provide certain permissions to the application.

c. Only public read-write properties and public fields are serialized. The read-only and write-only properties are ignored.

d. The code inside property accessors should not require any security privileges.

Certain other limitations exist on the types that XmlSerializer can serialize and deserialize:

a. You cannot use multi-dimensional arrays because they cannot be mapped to XML. In its place, use nested arrays.

b. An object that is referenced more than one time is serialized each time it is referenced. It is because of the fact that XmlSerializer does not have the ability to preserve the object's identity.

c. A type should not implement IDictionary interface. For example, XmlSerializer is not able to operate on collections like Hashtable, SortedList.

d. You cannot serialize an object graph that contains circular references for e.g. doubly-linked lists.

XmlSerializer is able to operate on certain .NET types, like Arrays, ArrayList, DateTime, DataSet. These types meet the requirements listed above.

No restrictions apply to SOAP Formatter

Unlike XmlSerializer, SoapFormatter does not place any such restrictions.

a. Providing a default constructor for a serialized class is not mandatory.

b. The class does not need to be declared as public.

c. SOAPFormatter can serialize both public and private properties and fields.

d. It can properly resolve circular references, for instance, the doubly-linked lists can be serialized.

e. Dictionaries can be serialized.

f. An object that is referenced multiple times is serialized exactly once. The SOAPFormatter provides a serial number to an object that gets serialized and keeps track of the duplicate objects. When serializing an object, the formatter determines if the same object has already been stored. If not, it serializes the object's data and gives it a serial number. If yes, it simply writes "this object is previously serialized having serial number N".

XML Serialization metadata attributes

XmlSerializer provides certain special serialization metadata attributes. They reside in the System.Xml.Serialization namespace. They override the XmlSerializer's default formatting. They control how objects are mapped to arbitrary XML formats. We'll discuss some of them.

XmlAttribute attribute: It is applied to a field/property. Instead of elements, it maps the field/property to XML attributes. By default, each field/property is represented as an element with the same name in an XML document. This attribute is used when a receiver can only process the XML data in attributes.

XmlElement attribute: It is applied to a field/property. It allows us to specify a different XML element name for a field/property.

XmlRoot attribute: It is applied to a class. It allows us to specify a different name for the root element. By default, XmlSerializer uses the class name.

XmlIgnore attribute: XmlSerializer ignores the field or property for serialization that is marked with the XmlIgnore attribute. It is important to note that XmlSerializer doesn't pay attention to the NonSerialized attribute.

Internal Working of XMLSerializer

XmlSerializer offers several constructors providing alternatives to customize the serialization at runtime. For instance, you can control the serialization of objects contained in a DLL even if you don't have access to the DLLs source code.

This is how a constructor works internally. It uses the reflection technique to examine the public fields and properties of a type. It extracts the type information and stores it in a cache. The type information includes the data types of public properties and fields and the serialization metadata attributes. It also maps each field/property to XML. By default, it maps the field/property to an XML element with the same name. However, if it encounters the XmlElement and XmlAttribute attributes attached to the fields, the mapping is modified accordingly.

The constructor then processes the type information to generate the temporary classes. These classes are then compiled into a temporary assembly making the serialization and deserialization very fast.

Instead of doing it again and again, you should plan to construct an XmlSerializer for a particular type only once. Examining the type, taking out the information, generating the temporary classes, and compiling them into an assembly are time-consuming operations. This will help you save the performance overhead. Also, retain the XmlSerializer instance as long as your application runs.

The XmlSerializer.Serialize() method checks an object to determine its type. It then searches the cache to get the corresponding type-mapping. Finally, it serializes the object according to the format specified in the mapping.

The XmlSerializer.Deserialize() method examines the XML data to determine the types that it'll construct. It then searches the cache to get the corresponding type-mapping. Finally, it constructs the object according to the format specified in the mapping.

XML Serialization Example

using System;
using System.IO;
using System.Collections;
using System.Xml.Serialization;

//Note that the Serializable attribute is not required
public class ChatRoom {
    private int roomId;
    private string roomName;

    //Notice the use of XmlIgnore attribute
    [XmlIgnore]
    private ArrayList onlineUsers = new ArrayList();

    //XmlSerializer requires you to provide a default constructor
    public ChatRoom() { }

    public ChatRoom(int roomId, string roomName) {
        this.roomId = roomId;
        this.roomName = roomName;
    }

    //Since XmlSerializer uses shallow serialization technique, it allows you 
    //to serialize only the public properties and fields of an object. Either
    //declare roomId and roomName as public, or make them available through public 
    //properties.
    public int RoomId {
        get { return roomId; }
        set { roomId = value; }
    }

    public string RoomName {
        get { return roomName; }
        set { roomName = value; }
    }

    public void AddUser(string name) {
        onlineUsers.Add(name);
    }

    public void Display() {
        ...
    }
}

class XmlSerializeDemo {
    public static void Main() {
        ChatRoom room1 = new ChatRoom(1, "Current Affairs");

        //Create a file named Chat.xml and link the stream writer with this file
        FileInfo fileRef = new FileInfo("Chat.xml");
        Stream writer = fileRef.Create();

        //XmlSerializer constructor accepts a Type parameter. You'll pass the type 
        //ChatRoom. This instance of XmlSerializer is specifically bound to serialize 
        //the ChatRoom objects.
        XmlSerializer xml = new XmlSerializer(typeof(ChatRoom));
        xml.Serialize(writer, room1);
        writer.Close();

        //Open the Chat.xml file to read from
        Stream reader = fileRef.Open(FileMode.Open, FileAccess.Read, FileShare.None);

        ChatRoom dRoom = (ChatRoom) xml.Deserialize(reader);
        reader.Close();
        dRoom.Display();
    }
}

Example: Explicitly declaring the types of objects you're about to serialize

You need to tell the XmlSerializer constructor beforehand about all the types that it's going to serialize. Consider serializing an object of type ArrayList. The serializer will attempt to serialize the objects stored inside the ArrayList. It is not possible for you to statically declare the types that you wish to store inside the ArrayList. It is due to the fact that you cannot hardcode the attributes inside the ArrayList class.

ArrayList rooms = new ArrayList();
ChatRoom room1 = new ChatRoom(1, "Current Affairs");
room1.AddUser("John");

//Add room1 in rooms ArrayList
rooms.Add(room1);

FileInfo fileRef = new FileInfo("ChatRooms.xml");
Stream writer = fileRef.Create();

//Pass the ArrayList type to the XmlSerializer constructor
XmlSerializer xml = new XmlSerializer(typeof(ArrayList));
xml.Serialize(writer, rooms);       //throws an exception

The above statement throws an exception because you were not able to tell the constructor that an object of type ChatRoom is stored in the rooms ArrayList.

In this case, you need to declare the types that are referenced from ArrayList. Use the overloaded constructor that accepts a Type array, it allows you to tell the serializer constructor that the ChatRoom type is being referenced from ArrayList.

XmlSerializer xml = new XmlSerializer(typeof(ArrayList),
                                        new Type[] {typeof(ChatRoom)} );
xml.Serialize(writer, rooms);       //Ok

XmlInclude attribute

The XMLSerializer constructor cannot automatically locate the derived types at the time when it examines the structure of a type.

The XmlInclude attribute is applied to a class allowing you to declare the derived types. You must furnish this attribute for each of the derived types, XmlSerializer will only then be able to serialize the objects of the derived types.

The constructor uses the information provided in the XmlInclude attribute to determine the derived type and extract and process the type mapping for the derived type.

//Attach the XmlInclude attribute to the Document class so that the serializer can know 
//about the File type derived from Document. The serializer will also analyze the File 
//type and stores its type mapping in the cache.
[XmlInclude(typeof(File))]
[XmlRootAttribute("doc")]   //change the root element's name to doc
public class Document {

    public Document() { }

    //XmlAttribute directs the XmlSerializer to serialize the DocumentId field
    //as an XML attribute rather than an XML element.
    [XmlAttribute]
    public int DocumentId;
}

public class File : Document {
    public File() { }
    public string folderName;
}

//User
File text = new File();
...
XmlSerializer xml = new XmlSerializer(typeof(Document));

//Serialize() first determines the type of 'text' object which is an instance of File. 
//It then searches the cache to get the type mapping for the type File. It is able to 
//successfully serialize the object according to the format specified in the mapping.
xml.Serialize(writer, text);

For Part 3, click this link: Serialization in .NET - Part 4