Wednesday, July 1, 2009

Serialization in .NET - 3

In my previous two article we discussed Binary Serialization and XML Serialization. This blog we will try to know more about SOAP (Simple Object Access Protocol) serialization. First up lets see how to serialize an object to SOAP format. We will take our good old Car class which most of you are very familiar with. The Car class is pasted below.

public class Car
{       
    public string Color
    { get; set; }
    public string Model
    { get; set; }
    public int NoOfDoors
    { get; set; }
    public int Price
    { get; set; }
    public string CubicCentimeter
    { get; set; }       
    public CarType Type
    { get; set; }
}

We will make use of the above car class for SOAP serialization. The code to serialize a class to SOAP format is similar to XML serialization with a small difference. The code is pasted below.

Car bmw3Series = new Car{Model = "BMW 3 Series", Color = "Blue", NoOfDoors = 4,  CubicCentimeter="3500cc"};
using (System.IO.Stream soapStream = new System.IO.FileStream("SoapFormat.xml", System.IO.FileMode.OpenOrCreate))
{
    System.Xml.Serialization.SoapReflectionImporter soapRefImp = new System.Xml.Serialization.SoapReflectionImporter();
    System.Xml.Serialization.XmlTypeMapping xmlTypeMapping =
        soapRefImp.ImportTypeMapping(typeof(Car));
    System.Xml.Serialization.XmlSerializer xmlSerializer =
        new System.Xml.Serialization.XmlSerializer(xmlTypeMapping);
    xmlSerializer.Serialize(soapStream, bmw3Series);
}

To serialize an object to SOAP format one can make use of XmlSerilizer class. When we initialize the XmlSerializer class for SOAP serialization instead of passing the type of the object we pass an instance of XmlTypeMapping class. This is the subtle difference. As you can see from the above code we are instantiating XmlTypeMapping object by passing the type of the Car object to the ImportTypeMapping method of SoapReflectionImporter class. SoapReflectionImporter class can be used whenever you want to serialize objects to SOAP formats.

The SOAP XML generated from the above code for the Car class is pasted below.

<?xml version="1.0" ?>
<Car xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" id="id1">
  <Color xsi:type="xsd:string">Blue</Color>
  <Model xsi:type="xsd:string">BMW 3 Series</Model>
  <NoOfDoors xsi:type="xsd:int">4</NoOfDoors>
  <Price xsi:type="xsd:int">0</Price>
  <CubicCentimeter xsi:type="xsd:string">3500cc</CubicCentimeter>
</Car>

Another way to serialize objects is by making use of SoapFormatter class. To make use of the SoapFormatter class one has to add a reference to the “System.Runtime.Serialization.Formatters.Soap.dll” assembly. The sample code is pasted below.

using (System.IO.Stream soapStream = new System.IO.FileStream("SoapFormatter.xml", System.IO.FileMode.Create))
{
    System.Runtime.Serialization.Formatters.Soap.SoapFormatter soapFormatter =
        new System.Runtime.Serialization.Formatters.Soap.SoapFormatter();
    soapFormatter.Serialize(soapStream, bmw3Series);
}

The SOAP xml generated from the above code is pasted below.

<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-
instance" xmlns:xsd="http://www.w3.org/2001/
XMLSchema" Xmlns:SOAP-ENC="http://schemas.
xmlsoap.org/soap/encoding/" xmlns:SOAP-ENV=
"http://schemas.xmlsoap.org/soap/envelope/" xmlns:clr="http://schemas.microsoft.com/soap
/encoding/clr/1.0" SOAP-ENV:encodingStyle=
"http://schemas.xmlsoap.org/soap/encoding/">
<SOAP-ENV:Body>
<a1:Car id="ref-1" xmlns:a1="http://schemas.microsoft.com/clr/
nsassem/SoapSerialization/SerializationSample%
2C%20Version%3D1.0.0.0%2C%20Culture%3Dneutral%
2C%20PublicKeyToken%3Dnull">
<_x003C_Color_x003E_k__BackingField id="ref-3">Blue</_x003C_Color_x003E_k__
BackingField>
<_x003C_Model_x003E_k__BackingField id="ref-4">BMW 3 Series</_x003C_Model_x003E_k__BackingField>
<_x003C_NoOfDoors_x003E_k__BackingField>4
</_x003C_NoOfDoors_x003E_k__BackingField>
<_x003C_Price_x003E_k__BackingField>0
</_x003C_Price_x003E_k__BackingField>
<_x003C_CubicCentimeter_x003E_k__BackingField id="ref-5">3500cc</_x003C_CubicCentimeter_
x003E_k__BackingField>
</a1:Car>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

I would not recommend using SoapFormatter class as this class is obsolete from .NET 3.5 version. Also SoapFormatter class doesn’t support serialization of generic collections.

That's pretty much how objects can be serialized to SOAP format. As in the case with XML serialization there may be situation where you would like to control the way objects are serialized to SOAP format. This can be very much useful when you are using webservices where you would like to take control of the way your objects are serialized. Following are some of the attributes which can be applied to control object serialization.

SoapAttribute

One can apply “SoapAttribute” to convert the property or field as a XML attribute rather than as a XML node which is the normal behavior. If you want to change the attribute name and don’t want it to be same as that of the property name, you can make use of the “AttributeName” property along with “SoapAttribute”. To provide a namespace to the xml attribute you can make use of the “Namespace” property. When you provide namespace it has to abide by w3 guidelines. As a general practice we provide urls as namespace. If you want to provide the data type to the attribute again that can be specified with the help DataType property. Sample code is pasted below.

public class Car
{       
   [System.Xml.Serialization.SoapAttribute (AttributeName="CarColor", DataType="string", Namespace="http://car")]
    public string Color
    { get; set; }
    public string Model
    { get; set; }
    public int NoOfDoors
    { get; set; }
    public int Price
    { get; set; }
    public string CubicCentimeter
    { get; set; }          
}

The serialized XML after applying the “SoapAttribute” is pasted below.

<?xml version="1.0"?>
<Car xmlns:xsi="http://www.w3.org/2001/XMLSchema-inst
ance"xmlns:xsd="http://www.w3.org/2001/XMLSchema" id="id1"d1p1:CarColor="Blue" xmlns:d1p1="http://car">
  <Model xsi:type="xsd:string">BMW 3 Series</Model>
  <NoOfDoors xsi:type="xsd:int">4</NoOfDoors>
  <Price xsi:type="xsd:int">0</Price>
  <CubicCentimeter xsi:type="xsd:string">
3500cc</CubicCentimeter>
</Car>

From the above pasted XML you can see how the Color property has been added as a SOAP attribute to the Car node.

SoapElement

SoapElement can be used to control the xml element’ attributes. Using the SoapElement attribute on a public property/field will convert the property/field to a XML element. The properties which can be used with SoapElement are, ElementName, DataType,  and IsNullable.

With ElementName one can provide a Name to the element if you don’t want the SOAP XML element name to be same as that of the property/field name.

DataType will help you to specify the SOAP XML schema data type.

IsNullable specifies whether a SOAP XML element node should be generated if the property/field is null. If the IsNullable value is true an empty XML element with xsi:nil attribute set to true will be generated for properties/fields having null value else no XML element node will be generated. As we have learned in XML serialization, if IsNullable is used in SOAP serialization it won’t throw any error even if it is used along with value type objects. In SOAP serialization if you have set IsNullable to true then for value type it will create a node with a default value assigned to it. For e.g. if you have used IsNullable for int data type and boolean data type then the SOAP xml will have nodes with 0 and false as their default value, if they are not initialized. Sample code with these implementation and the SOAP XML is pasted below.

public class Car
{
    [System.Xml.Serialization.SoapElement
(ElementName = "CarColor", IsNullable = true)]
        public string Color
        { get; set; }
        [System.Xml.Serialization.SoapElement (ElementName="CarModel")]
        public string Model
        { get; set; }
        public int NoOfDoors
        { get; set; }
        [System.Xml.Serialization.SoapElement (ElementName = "CarPrice", IsNullable = true)]
        public int Price
        { get; set; }       
        public string CubicCentimeter
        { get; set; }          
}

//Car object to be serialized.
Car bmw3Series = new Car{Model = "BMW 3 Series", NoOfDoors = 4, CubicCentimeter="3500cc"};

//Serialized SOAP XML
<?xml version="1.0"?>
<Car xmlns:xsi="http://www.w3.org/2001/XMLSchema
- instance" xmlns:xsd="http://www.w3.org/2001/
XMLSchema" id="id1">
  <CarColor xsi:nil="true" />
  <CarModel xsi:type="xsd:string">BMW 3 Series</CarModel>
  <NoOfDoors xsi:type="xsd:int">4</NoOfDoors>
  <CarPrice xsi:type="xsd:int">0</CarPrice>
  <CubicCentimeter xsi:type="xsd:string">3500cc</CubicCentimeter>
</Car>

From the above xml one can see there is “xsi:nill” XML attribute set to true for “CarColor” node as we have applied “IsNullable” true. Also notice “CarPrice” node having “0” as its value. Since we have used “IsNullable” it is generating a node with a default value.

SoapEnum

Can be used to describe how Enum needs to be serialized. It has only one property named Name using which you can specify what should be the name of the particular Enum value. Sample code is pasted below.

public enum CarType
{
    [System.Xml.Serialization.SoapEnum(Name = "CompactCar")]
    SmallCar,
   [System.Xml.Serialization.SoapEnum(Name= "CompactSedan")]
    CS,
    Sedan,
    SportsCar,
    Suv
}

If the “Name” property is specified then the enum’ member will have the name specified in the “Name” property else it will default to the member name.

SoapIgnore

If you want a public field or property not to be serialized then you can make use SoapIgnore attribute. This will prevent a particular public field or property from being serialized. SoapIgnore doesn’t have any property. The serialized XML and SoapIgnore attribute applied to Color property of the Car class is pasted below.

public class Car
{
    [System.Xml.Serialization.SoapIgnore]
    public string Color
    { get; set; }
    [System.Xml.Serialization.SoapElement (ElementName="CarModel")]
    public string Model
    { get; set; }
    public int NoOfDoors
    { get; set; }
    [System.Xml.Serialization.SoapElement(ElementName = "CarPrice", IsNullable = true)]
    public int Price
    { get; set; }       
    public string CubicCentimeter
    { get; set; }          
}

//Car object which needs to be serialized
Car bmw3Series = new Car{Model = "BMW 3 Series",
NoOfDoors = 4, Color="Blue", CubicCentimeter="3500cc"};

//Serialized XML with SoapIgnore attribute applied.
<?xml version="1.0"?>
<Car xmlns:xsi="http://www.w3.org/2001/
XMLSchema-instance" xmlns:xsd="http://www.w3.org
/2001/XMLSchema" id="id1">
  <CarModel xsi:type="xsd:string">BMW 3 Series</CarModel>
  <NoOfDoors xsi:type="xsd:int">4</NoOfDoors>
  <CarPrice xsi:type="xsd:int">0</CarPrice>
  <CubicCentimeter xsi:type="xsd:string">3500cc</CubicCentimeter>
</Car>500cc</CubicCentimeter>
</Car>

From the above pasted SOAP XML one can note that Color attribute is missing because of the usage of SoapIgnore.

SoapInclude

SoapInclude can be used to specify the derived classes to be included in the SOAP format. For e.g. you have Car class and other classes like “CompactCar”, “Sedan” etc are deriving from Car class then you can make use SoapInclude. Suppose you have a class called CarCollection where you have a property called CarInstance which is a generic collection of base type Car. When the CarCollection object is serialized one has to tell the compiler what classes the collection can hold or derives from. If one is not specifying the inherited classes using the SoapInclude attribute the serialization process throws the following error.

InvalidOperationException outer exception: "There was an error generating the XML document."
InvalidOperationException inner exception: "The type SoapSerialization.CompactCar was not expected. Use the XmlInclude or SoapInclude attribute to specify types that are not known statically."

To avoid the above error you can use SoapInclude attribute. A sample code is pasted below.

//Derived classes.
public class CompactCar : Car
{
    public string SomeProperty
        { get; set; }
}

public class Sedan : Car
{
    public int SedanCapacity
        { get; set; }
}
\*Class has a generic collection of type car. One has to use SoapInclude to tell the serializer that the class can have classes of type CompactCar and Sedan.*/
[System.Xml.Serialization.SoapInclude(typeof(CompactCar)),
        System.Xml.Serialization.SoapInclude(typeof(Sedan))]
  
public class CarCollection
{         
        public System.Collections.Generic.List<Car> CarInstance
        { get; set; }
}

Sometimes using the SoapInclude attribute may not solve the problem rather it can throw a new error. The error with inner exception is pasted below.

System.InvalidOperationException: "There was an error generating the XML document."
System.InvalidOperationException: "Token StartElement in state Epilog would result in an invalid XML document."

To solve this you need to add a start element and end element before and after the serialized SOAP xml using XmlTextWriter’ WriteStartElement and WriteEndElement methods respectively. The logic behind this is to add a root node. The SOAP serialization code is modified and pasted below.

using (System.IO.Stream soapStream = new System.IO.FileStream("SoapFormat.xml",
    System.IO.FileMode.Create))
{
    using (System.Xml.XmlTextWriter xmlWriter = new System.Xml.XmlTextWriter(soapStream, System.Text.Encoding.UTF8))
    {
        System.Xml.Serialization.SoapReflectionImporter soapRefImp =
            new System.Xml.Serialization.SoapReflectionImporter();
        System.Xml.Serialization.XmlTypeMapping xmlTypeMapping =
            soapRefImp.ImportTypeMapping(typeof(CarCollection));
        System.Xml.Serialization.XmlSerializer xmlSerializer =
            new System.Xml.Serialization.XmlSerializer(xmlTypeMapping);
        xmlWriter.WriteStartElement("carRoot");
        xmlSerializer.Serialize(xmlWriter, carCol);                    
        xmlWriter.WriteEndElement();
    }               
}

Deserializing a SOAP XML

Deserializing an SOAP XML is pretty simple and similar to the serialization logic. You need to create an instance of “SoapReflectionImporter” class and using the “ImportTypeMapping” method of “SoapReflectionImporter” create an instance of “XmlTypeMapping” class. Once these things are done create an instance of “XmlSerializer” class by passing the “XmlTypeMapping” object as one of the parameters. After this call the Deserialize method of the “XmlSerializer” class. The sample code is pasted below.

System.Xml.Serialization.SoapReflectionImporter soapReflImp =
                        new System.Xml.Serialization.SoapReflectionImporter();
System.Xml.Serialization.XmlTypeMapping xmlTypeMap =
        soapReflImp.ImportTypeMapping(typeof(CarCollection));
System.Xml.Serialization.XmlSerializer xmlSerial =
        new System.Xml.Serialization.XmlSerializer(xmlTypeMap);
using (System.Xml.XmlTextReader xmlTextReader = new System.Xml.XmlTextReader("SoapFormat.xml"))
{
    /*Needed to read the root element. In the serialization code we have added the "carRoot" element.*/
    xmlTextReader.ReadStartElement("carRoot");
    CarCollection carColl = (CarCollection)xmlSerial.Deserialize(xmlTextReader);
    xmlTextReader.ReadEndElement();
}

One can see from the above code that we are using “ReadStartElement” method of “XmlTextReader” class. This is done because we have manually added the “carRoot” element by making use of “WriteStartElement” method of “XmlTextWriter” while serializing the object. If “ReadStartElement” method is not used then the following error will be thrown.

System.InvalidOperationException: "There is an error in XML document (1, 2)."
Inner exception - System.InvalidOperationException: "<carRoot xmlns=''> was not expected."

So that’s about SOAP serialization. Next we will see how to write custom serializers so that we can take control of serialization, till then try to learn more.

Sandeep

2 comments:

  1. Is it possible to get the same DataType of the object before serializing after deserializing using that "DataType" attribute

    ReplyDelete
  2. Hi,
    I don't think there will a problem in getting datatype. As the serialization serializes the datatype as well in an attribute called "type". Currently my laptop is crashed so I couldn't validate this. Once my laptop is up and running will validate the same. In the meantime I think you can test the same.

    ReplyDelete

Please provide your valuable comments.