.Net Serialization World

5.1 Generating Serialized and SOAP Formats

5.1.1 Serialization and Deserialization

While working with complicated applications where distributed applications play a major role, you may frequently need to transfer objects from one application to another. For this, you need to convert objects to an appropriate format before you transfer objects to any target destination. The .NET Framework 2.0 provides predefined formatters that convert objects to binary and SOAP formats. The .NET Framework 2.0 also provides appropriate classes and interfaces that you can use to persist objects to XML format, making it more convenient to exchange data between various platforms. There can be specific situations where you need to convert objects to an application-specific customized format. In such cases, you can create your own formatters by using the classes and interfaces provided by the .NET Framework 2.0 to facilitate customized serialization and deserialization.

To send objects to another location, you may need to convert them to an appropriate format. The .NET Framework 2.0 provides built-in classes to convert data to formats that are portable, or easy to transport to another location. This process of converting data into a portable format is called serialization. At times, you may need to restore the object or data into its original form. This process of restoring the object or data to its original state is called deserialization.

To understand the concept of serialization and deserialization better, let us consider an e-shopping application that stores shopping and product details. These details are managed on one server, and the payment-related details are managed on another server. The shopping details are maintained through an object of the Client class. The products and their prices are stored as state of an object of the Client class along with other information such as credit card number. You need to send the credit card number to a remote server that manages all the finance details. To do this, you need to convert the credit card number to a portable format before sending it to the remote server. The remote server reconstructs this data into an appropriate object. The application residing on the remote server uses this object to retrieve object information, such as product and credit card details to continue the transaction. In this example, the process of converting credit card information into a portable format is referred to as serialization and the process of restoring the object or data to its original state is referred to as deserialization.

This lesson introduces how to save the state of an object to recreate the same object later. In addition, the lesson introduces how objects are serialized into binary and Simple Object Access Protocol (SOAP) formats by using the BinaryFormatter and SoapFormatter classes.
 

5.1.2 BinaryFormatter and SoapFormatter

To determine the serialization format for objects, you need to use a formatter. A formatter is required to indicate how you want to format the data that you are going to save for deserialization. You can store this data at a persistent location and later restore it back to its original state. The .NET Framework provides two formatter classes, BinaryFormatter and SoapFormatter. Both of these classes provide methods for serialization and deserialization.

BinaryFormatter generates a compact stream when you serialize an object. Therefore, BinaryFormatter is useful for storage and socket-based network streams.

SoapFormatter generates a stream in SOAP format when you serialize an object. You use the SoapFormatter class for serialization in applications spanning heterogeneous environments. For example, you use SoapFormatter when you want the information to be accessed and shared between different systems, such as a .NET Framework application and a UNIX process. BinaryFormatter produces a more compact stream of bytes than SoapFormatter. SoapFormatter is generally used for cross-platform interoperability.

The following table provides the description and the implementation of the BinaryFormatter and SoapFormatter classes.

When you create an object in a .NET Framework application, you don't have to worry about how the application's data is stored in memory - the .NET Framework does that for you. However, if you want to send an object to a file, or another application, or across a network then the object will have to be converted to a different format. This is called serialization.

Serialization, as implemented in the System.Runtime.Serialization namespace, is the process of converting an object to a linear series of bytes that can be stored or transferred. In the code below, serialization is used to store a string (which wouldn't be necessary - you'd just write the string to a file) and the DateTime.
To serialize/deserialize the data, a FileStream is opened together with a BinaryFormatter.

using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
private static void Main()
{
string data = "This is the vitally important data" System.DateTime.Now;

// Create a file to save the data to
FileStream fs = new FileStream ("SerializedString.Data", 
FileMode.Create);

// Create a BinaryFormatter object to perform the serialization
BinaryFormatter bf = new BinaryFormatter();

// Use the BinaryFormatter object to serialize the data to the file
bf.Serialize(fs, data);

// Close the file
fs.Close;
}

deserialize:

using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
private static void Main()
{
// Open the file
FileStream fs = new FileStream ("SerializedString.Data", 
FileMode.Open);

// Create a BinaryFormatter object to perform the deserialization
BinaryFormatter bf = new BinaryFormatter();

// Create the object top store the deserialized data
string data = "";

' Use the BinaryFormatter object to deserialize the data from the file
data = (string)(bf.Deserialize(fs);

// Close the file
fs.Close;

// Display the deserialized string
Console.WriteLine(data);
}

5.2 Serialized XML Formats

 

5.2.1 XmlSerializer class

To understand the use of XML serialization, let us consider the example of a server used for assessment tests of various programming languages. The application provides a set of questions for a specific programming language and analyzes the performance of a candidate based on the answers provided by the candidate. The application has custom classes to represent application-specific types such as question, answer, and result. To make the application generalized for a wide variety of client applications, you want to transport and receive data in the form of XML. The application sends the various questions to the client application by serializing the appropriate question object one by one in the XML format. This format follows an XML schema definition with no type or assembly information. The application receives the response in the form of XML that can be deserialized into an appropriate answer object. This way, you can ensure that the client application does not need to have access to the same assembly and types, which the server application serializes and deserializes into any type according to the design implemented in the client application. During serialization, to make the output of the various types properly structured and formatted, you intend to utilize the facility of specifying elements, attributes, and serialized array with appropriate names in the resulting XML. 

XML is an open standard and an XML stream can be processed by any application regardless of the platform. XML serialization is the process of converting the public properties and fields of an object to a serialized XML format for storage or transport purpose. XML serialization can also be used to serialize objects into XML streams that conform to the SOAP specification. In the .NET Framework 2.0, you use the XmlSerializer class for XML serialization. 

Serialization is a process of saving the state of an object into a stream or buffer. The .NET Framework provides two classes, SoapFormatter and XmlSerializer, to serialize objects. The SoapFormatter class is in the System.Runtime.Serialization.Formatters.Soap namespace and the XmlSerializer class is in the System.Xml.Serialization namespace. The SoapFormatter class serializes and deserializes objects into the SOAP format according to the World Wide Web Consortium (W3C) SOAP specifications. In addition, the SoapFormatter class produces entire SOAP messages with tags, such as header, and provides limited options to customize the generated format. The XmlSerializer class serializes and deserializes objects into and from XML documents. Unlike the SoapFormatter class, you can control the how objects are encoded into the XML format by using the XmlSerializer class. XML is a universal data transport and translation mechanism which is the foundation of ADO.NET in the .NET Framework. The ability to serialize and deserialize into and out of the XML format works with this foundation.

The XmlSerializer class maps the .NET Framework classes to XML types. In general, the serialized XML does not include type information when the object is serialized because the information is retrieved from the type when it is deserialized. However, this does not occur with a type object. In this case, the serializer embeds the XML-type information about the data being serialized.

By default, the XmlSerializer class maps each field and property of the object to be serialized to an XML element with the same name. This implies that the resulting XML, created by serializing a specific object, will possess separate elements to represent the members of the corresponding class with the same naming convention defined for these members in that class. This may be acceptable while working with classes that have few fields and properties. At times, you need to rearrange the XML elements in a more structured way either by specifying custom names to the elements being generated or by grouping related elements together. By using the XmlSerializer class, you can customize the XML format of the object being serialized, rather than depending on the default mapping between the XML elements and the members of the class. The .NET Framework provides several ways to customize how the XmlSerializer class serializes data to XML. You can use XML serialization attributes to declartively mark how your classes are serialized. Then, you can also implement the IXmlSerializeable interface to control serialization through code. Finally, you use the events of the XmlSerializer class to control how to handle unknown elements and attributes.

The following table describes the most commonly used methods available in the XmlSerializer class that are used to serialize or deserialize objects in the XML format.

There are a two formats to choose from when serializing objects, BinaryFormatter and SOAPFormatter. SOAP stands for Simple Object Access Protocol and it is XML-based, and can therefore be used to transmit objects across a network and to applications outwith the .NET Framework. It is however, primarily intended for SOAP Web Services, and is not as flexible as XML serialization.

XML (eXtensible Markup Language) is a standardised, text-based document format for storing application-readable information. It's a standard that can be easily processed by computers. Any type of data (documents, images, binary files, music database information) can be stored using XML.

The .NET Framework includes several libraries for reading and writing XML files, including the System.Xml.Serialization namespace, which provides methods for converting objects to and from XML files. With XML you can write almost any object to a text file for later retrieval. There are three steps to serialize an object to XML:

    1. Create a stream, TextWriter or XmlWriter object which holds the serialized output
    2. Create an XmlSerializer object by passing it the object to be serialized
    3. Call the XmlSerializer.Serialize method to serialize the object and output the results to the stream
using System.IO;
using System.Xml.Serialization;
private static void Main()
{
// Create the file to save the data
FileStream fs = new FileStream("SerializedDate.XML", 
FileMode.Create);

// Create an XmlSerializer object to perform the serialization
XmlSerializer xs = new XmlSerializer(typeof(DateTime));

// Use the XmlSerializer object to serialize the data to the file, that data being the current date
xs.Serialize(fs, System.DateTime.Now);

// Close the file
fs.Close;
}
using System.IO
using System.Xml.Serialization
private static void Main()
// Open the file from which the data is to be read
FileStream fs = new FileStream("SerializedDate.XML", 
FileMode.Open);

// Create an XmlSerializer object to perform the deserialization
XmlSerializer xs = new XmlSerializer(typeof(DateTime));

// Use the XmlSerializer object to deserialize the data from 
the file and cast it to a DateTime
DateTime previousTime = (DateTime)xs.Deserialize(fs);

// Close the file
fs.Close;

// Display the deserialized date and time
Console.WriteLine("Day: " + previousTime.DayOfWeek + (", Time: " + previousTime.TimeOfDay.ToString())
}

To create classes that can be serialized by using XML serialization, the following tasks must be performed:

  • Specify the class as Public
  • Specify all members that must be serialized as Public
  • Create a parameterless constructor

If you serialize a class that meets the requirements fior XML serialization, but does not have any XML serialization attributes then default settings are used at runtime. These defaults use the class and member names, and each member is serialized as a separate XML element. The following simple class will be serialized as follows (sample values included):

public class ShoppingCartItem
{
public Int32 productId; 
public decimal price; 
public Int32 quantity;
public decimal total; 

public ShoppingCartItem()
{
}
}

<?xml version="1.0" ?>
<ShoppingCartItem >
<productId>100</productId>
<price>10.25</price>
<total>20.50</total>
<quantity>2</quantity>
</ShoppingCartItem>

This might be perfectly fine. However, there may be times when your XML documents need to conform to specific standards, and thus you need to lay down how the serialization is to be structured. Supposing the ShoppingCartItem must be called CartItem, the productId is to be an attribute of CartItem rather than a separate XML element and the total is not to be serialized. The following changes should be made:

[XmlRoot ("CartItem")] public class ShoppingCartItem
{
[XmlAttribute] public int productId; 
public decimal price; 
public int quantity; 
[XmlIgnore]public decimal total; 

publicShoppingCartItem
{ 
} 
}
<?xml version="1.0" ?>
<CartItem productId="100">
<price>10.25</price>
<quantity>2</quantity>
</CartItem>

Typically, when two applications are going to exchange data in the form of XML files, the developers work together to create an XML schema file. An XML schema defines the structure of an XML document. Many types of XML schema already exist, and in many cases it makes sense to use an existing schema.
The XML Schema defimnition tool (xsd.exe) produces a set of classes that are strongly typed to the schema and annotated with attributes. When an instance of such a class is serialized, the generated XML adheres to the schema. To generate a class based on a schema follow these steps:

  1. Create or download the XML schema .xsd file on your computer
  2. Open a Visual Studio Command Prompt. This can be started from the Tools folder
  3. From the command prompt type (path to...) ~schemafilename.xsd/classes/language:VB
  4. Open the newly-created file Schema.vb, and add the class to your application

In addition to serializing a public class, an instance of a dataset object can also be serialized:

private void SerializeDataSet(stringfilename)
{ XmlSerializer ser = new XmlSerializer(GetType(DataSet));

// Creates a DataSet; adds a table, column and ten rows
DataSet ds = new DataSet("myDataSet")
DataTable t = new DataTable("table1")
DataColumn c = new DataColumn("thing")
t.Columns.Add(c);
ds.Tables.Add(t);
int i; 
for i = 0; i<10; i++) {
r = t.NewRow();
r[0] = "Thing " + i;
t.Rows.Add(r);
}

TextWriter writer = new StreamWriter(filename);
ser.Serialize(writer, ds);
writer.Close();
}

Similarly, you can serialize arrays, collections and instances of an XmlElement or Xml-Node class. Alternatively you can use DataSet.WriteXml, DatSet.ReadXml and DataSet.GetXml. 

5.2.2 XmlSerialization attributes

You can control the XML serialization of an object or create an alternate XML stream from the same set of classes by using attributes. XML serialization attributes customize how XmlSerializer maps a class, field, or property to an XML document and declare types that are not explicitly referenced in a source file. As a result, XML serialization attributes determine the resulting XML format for each class member associated with an attribute. The XML serialization attributes contain properties that provide information to the XmlSerializer class on how to map an object to XML format. These properties define XML namespaces, limit the scope of the attribute to a certain data type, specify the corresponding type in an XSD schema, control the form of the generated XML, and define whether to create XML for null references.

Consider an example where you are creating an application that first collects responses for a survey provided by different political candidates and then sends that information to the Server application for processing. In this application, you can mark the particular responses to questions in the resulting XML document that includes attributes to align the particular question within the survey for a specific candidate. The XML serialization attributes are divided into two categories, attributes that help you generate custom XML format and attributes that help you generate custom SOAP format. The following table describes these two categories of XML serialization attributes along with the corresponding code samples.

 

5.2.3 Custom XmlSerializable classes using IXmlSerializable

The IXmlSerializable interface provides custom formatting for XML serialization and deserialization. The XmlSerializer class implements the IXmlSerializable interface, and the custom classes use it to control how they will be serialized or deserialized through the XmlSerializer class. For a finer granularity of control on the code and a tighter direction of output, you should implement the IXmlSerializable interface in custom classes. You also obtain control of the XML file when serializing or deserializing objects at run time. As a result, by using the IXmlSerializable interface, you can control the way objects are serialized into XML format. However, you cannot control the serialization of objects into XML format if you use the XmlSerializer class. In addition, the implementation of the IXmlSerializable interface helps you chunk the serialized XML data in a better way.

For example, you need to develop an application that allows a teacher to collect and grade exams for students. These exams then need to be entered into the school’s data processing system for processing at diverse locations. To do this, the data is to be serialized and sent to the Student system, which would store the data to be used for creating report cards. Then, the serialized data needs to be sent to the Reporting application, which uses Extensible Stylesheet Language (XSL) and XSD to transform the data to be included in the reports for sending it to the Board of Education. Finally, the data would be serialized and sent to the Permanent Record application, which would then store the data in the students permanent record. In this situation, you can use the ISerializable interface to permit these diverse applications to use the exact same data as it is being rendered for each particular use.

The following table lists the methods that control serialization and deserialization in the IXmlSerializable interface.

The following code sample shows how to implement the IXmlSerializable interface and its methods. The code implements the IXmlSerializable interface in a class named Temperature and defines the ReadXml method to read data during deserialization. The code also defines the WriteXml method to write data during serialization. The Temperature class provides an equivalent reference type to represent a particular temperature. The class provides a property named Degree to represent the value for degree of the current Temperature object. Implementing the WriteXml method writes the degree value of the Temperature object into the XML stream as a string, and implementing the ReadXml method reads the degree value back from the XML stream and stores it into the corresponding field in the object. The Main method shows how to create and serialize an object of the Temperature class to an XML file on the disk. The object is serialized according to the implementation provided through the WriteXml method in the Temperature class. Finally, the code creates a second object of the Temperature class by deserializing the data in the XML file and displays the newly created object in the console. The data in the XML file is deserialized according to the implementation provided for the ReadXml method in the Temperature class.

using System;
using System.IO;
using System.Xml.Serialization;
public class Temperature : IXmlSerializable
{
private float degreeOf;

public float Degree
{
get { return this.degreeOf; }
set { this.degreeOf = value; }
}

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

public void ReadXml(System.Xml.XmlReader reader)
{
this.degreeOf = Convert.ToSingle(reader.ReadString());
}

public void WriteXml(System.Xml.XmlWriter writer)
{
writer.WriteString(this.degreeOf.ToString());
}

public override string ToString()
{
return this.degreeOf.ToString();
}

public static void Main()
{
try
{
StreamWriter tw = new StreamWriter("C:\\Temperature.xml");
XmlSerializer s = new XmlSerializer(typeof(Temperature));
Temperature t1 = new Temperature();
t1.Degree = 39.1C;
s.Serialize(tw, t1);
tw.Close();
FileStream fs = new FileStream("C:\\Temperature.xml", FileMode.Open);
Temperature t2;
t2 = ((Temperature)(s.Deserialize(fs)));
Console.WriteLine(t2);
fs.Close();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
finally
{
Console.WriteLine();
Console.Write("Press ENTER to exit...");
Console.ReadLine();
}
}
}

5.2.4 Handle XML Serialization Events by Using Delegates

The common language runtime (CLR) supports reference types called delegates. In the .NET Framework, delegates are used for event handlers and callback functions. Delegates help you assign a method to be executed or processed in response to an event that occurs in an application. Delegates act as pointers from the event to the method, which is executing code. Delegates can be considered similar to a wire connecting a light switch to a light, and when you flip the switch, the light turns on or off. The event of Light_Flip causes the Change_Light_State method to execute changing the state of the light. A delegate type can represent any method with a compatible signature.

XML serialization delegates represent methods that can handle different events of an XmlSerializer class, such as the event raised when an unknown node, element, or attribute is found in the serialized stream during deserialization. You add an instance of the delegate to the event to associate the event with the event handler. The event handler is called whenever the corresponding event occurs during the process of serialization or deserialization being performed by the XmlSerializer class.

The following table describes the various XML serialization delegates that help you handle events during different situations.

The following code sample shows the use of XmlAttributeEventHandler, XmlNodeEventHandler, and XmlElementEventHandler delegates. It defines two classes, Company1 and Company2. Company2 contains more members than Company1. The code initially serializes an object of Company2 and then deserializes it to an object of Company1. To deal with the unknown members found during the process of deserialization, the code defines appropriate event handler methods to handle the UnknownAttribute, UnknownElement, and UnknownNode events with the help of the appropriate delegates for these events. 

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

public class Company1
{
public string CompanyName;
}

[XmlRoot(ElementName="Company1")]public class Company2
{
public string CompanyName;
[XmlAttribute]public string CompanyType;
[XmlAttribute]public string CompanyId;
public string CompanySize;
public string CompanyLocaton;
}

public class MainClass
{
static void Main()
{
MainClass mainObject = new MainClass();
string filename = "c:\\Delegates.xml";
Company2 comp2 = new Company2();
comp2.CompanyId = "90067";
comp2.CompanyLocaton = "New York";
comp2.CompanyName = "ABC InfoSystems";
comp2.CompanySize = "Large";
comp2.CompanyType = "Technical";

XmlSerializer serializer = new XmlSerializer(typeof(Company2));
TextWriter writer =new StreamWriter(filename);

serializer.Serialize(writer, comp2);

XmlSerializer deserializer = new XmlSerializer(typeof(Company1));

writer.Close();

deserializer.UnknownAttribute += new
XmlAttributeEventHandler(mainObject.Serializer_UnknownAttribute);

deserializer.UnknownElement += new
XmlElementEventHandler(mainObject.Serializer_UnknownElement);

deserializer.UnknownNode += new
XmlNodeEventHandler(mainObject.Serializer_UnknownNode);

FileStream fs = new FileStream(filename, FileMode.Open);

Company1 comp1 = (Company1)deserializer.Deserialize(fs);
fs.Close();
Console.WriteLine("Press enter to exit");
Console.ReadLine();
}

private void Serializer_UnknownNode(object sender, XmlNodeEventArgs e)
{
Console.WriteLine("UnknownNode Name: " + e.Name);
Console.WriteLine("UnknownNode LocalName: " + e.LocalName);
Console.WriteLine("UnknownNode Namespace URI: " + e.NamespaceURI);
Console.WriteLine("UnknownNode Text: " + e.Text);

Company1 comp1 = (Company1)e.ObjectBeingDeserialized;
Console.WriteLine("Object being deserialized " + comp1.ToString());

XmlNodeType myNodeType = e.NodeType;
Console.WriteLine(myNodeType);
}

private void Serializer_UnknownElement(object sender, XmlElementEventArgs e)
{
Console.WriteLine("Unknown Element");
Console.WriteLine(" " + e.Element.Name + " " + e.Element.InnerXml);
Console.WriteLine(" LineNumber: " + e.LineNumber);
Console.WriteLine(" LinePosition: " + e.LinePosition);

Company1 comp1 = (Company1)e.ObjectBeingDeserialized;
Console.WriteLine(comp1.CompanyName);
Console.WriteLine(sender.ToString());
}

private void Serializer_UnknownAttribute(object sender,
XmlAttributeEventArgs e)
{
Console.WriteLine("Unknown Attribute");
Console.WriteLine(" " + e.Attr.Name + " " + e.Attr.InnerXml);
Console.WriteLine(" LineNumber: " + e.LineNumber);
Console.WriteLine(" LinePosition: " + e.LinePosition);

Company1 comp1 = (Company1)e.ObjectBeingDeserialized;
Console.WriteLine(comp1.CompanyName);
Console.WriteLine(sender.ToString());
}
}

5.3 Custom Serialization classes

 

5.3.1 Serialization Types

When you work on an application that is used to serialize and deserialize different types and send the stream to different locations, you need to restrict the serialization of these types to specific destinations only. You also need to control the amount of data that needs to be serialized and perform certain activities just before and after the process of serialization or deserialization. In such situations, you plan to use an event-driven mechanism for such event-specific activities. You decide to create a custom formatter class to perform customized application-specific serialization and deserialization because the predefined formatter classes available in the .NET Framework serialization architecture do not provide such flexibility.

To understand custom serialization classes, consider an example of an Insurance application. This application allows the user to select the type of insurance he or she wants to buy, such as home, auto, flood, and boat. Based on the selection of the type of insurance, the user will be prompted for information related to location, specific type of equipment, or street address. This information is then sent to various types of insurers for underwriting. Once the information is sent, it is deserialized in different ways for each type of insurer. For example, the home insurer would worry about hurricane or flood zones, whereas the car insurer would be more concerned about driver accident records. To manage information about different types of insurers, custom serialization can tailor the specific insurer information by capturing data from the insurers through a general application screen.

The .NET Framework provides appropriate classes and interfaces to build custom formatter classes. By using specific predefined classes, you can retrieve information related to the source or destination of a serialized stream. Apart from this, with the help of various event delegates and handlers, you can also perform event-based activities.

While working with objects, you need to implement serialization in some form to save information in the object or to move the object to another process. Serialization is a functionality that is critical while building a distributed application. You can use the built-in classes or create custom classes to serialize data in the distributed application.

While implementing custom serialization and deserialization functionalities in a class, you need to take care of two things. During the serialization of an object, you need to specify the values of the object to be serialized, and during deserialization, you need to collect values and other information about the object present in the serialized stream. The .NET serialization architecture provides three serialization types: the SerializationEntry structure, the SerializationInfo class, and the StreamingContext structure. You can use these three types to specify values that are required to represent an object in a serialized form and retrieve them appropriately during deserialization. In addition to the values in an object, these serialization types also provide information, such as the type of the object in the serialized stream, corresponding assembly name, and the context in which the current serialization is taking place. These serialization types help you have more control when implementing the custom serialization and deserialization functionality for a certain type.

In the context of the Insurance application example, which allows users to select different types of insurance from a single application, you can use the SerializationEntry and SerializationInfo types to create the output for the insurer applications. You can customize the output XML stream to notify the insurer about the appropriate information entered by the user before processing the insurer’s request. This helps you present targeted information in a format that can be recognized by the back-end application.

With serialization types, you can get the information about serialized and deserialized objects before and after serialization and deserialization, respectively.

The following tables describe and shows the implementation of each of the serialization types.
 

Custom Serialization is the process of controlling the serialization, so that you can serialize and deserialize between different versions of a class. This is done by implementing your own serialization classes.

To override the serialization built into the .NET Framework, you use the ISerializable interface and apply the Serializable attribute to a class.

using System.Runtime.Serialization;
using System.Security.Permissions;
<Serializable()> class ShoppingCartItem : ISerializable
{

public int productId;
public decimal price;
public int quantity;
[NonSerialized] public decimal total;

// The standard, non-serialization constructor
public ShoppingCartItem(int _productID, decimal _price, int, _quantity) 
{
productId = _productID;
price = _price;
quantity = _quantity;
total = price * quantity;
}

// The following constructor is for deserialization
protected ShoppingCartItem(SerializationInfo info, StreamingContext context) 
{
productId = info.GetInt32("Product ID");
price = info.GetDecimal("Price");
quantity = info.GetInteger("Quantity");
total = price * quantity;
}

// The following method is called during serialization
[SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter:=true)]
public virtual void GetObjectData(SerializationInfo info, StreamingContext context) 
{
info.AddValue("Product ID", productId);
info.AddValue("Price", price);
info.AddValue("Quantity", quantity);
}

public override string ToString()
{
return productId + ": " + price + " x " + quantity + " = " + total;
}
}

5.3.2 Serialization Interfaces

Consider that you are creating an application and you need to serialize your objects to be stored in permanent structures or passed to another application for further processing. For example, you may need to serialize the information from a customer to a credit card processing application to complete the transaction and arrange the payment. Sometimes, when you are communicating with the components, you can use built-in serialization classes or any built-in classes available in the .NET Framework 2.0. However, you need to customize the interaction of components to ensure better control over object interaction. In this particular scenario, customizing the interaction of components helps you control how the customer’s credit card information is serialized and transmitted for processing.

The .NET Framework provides serialization interfaces that can be implemented by all built-in classes to enable the serialization functionality. The serialization interfaces are ISerializable, IDeserializationCallback, IFormatter, and IFormatterConverter. All of these interfaces belong to the System.Runtime.Serialization namespace.

The following table describes the various serialization interfaces and their members.

 

The built-in clases of the .NET Framework implement the ISerializable interface. You can make your own classes serializable by adding the Serializable attribute to a class. Some members of the class, such as temporary or calculated values, may not need to be stored. Consider the following ShoppingCartItem class, the fourth member, the total, is calculated from price times quantity and needn't be stored. 

However, when the object is deserialized, the total will need to be recalculated before the object is used. To enable the class to initialze a nonserialized member automatically, the IDeserializationCallBack interface must be used.
The OptionalField comes in when a new member is added to a class in a later version of the application. To ensure compatibility with existing serialized objects created by earlier versions, any new member must be optional. (Also, you must never remove or rename a serialized field!)

[Serializable]
class ShoppingCartItem : IDeserializationCallback 

public int productId; 
public decimal price; 
public int quantity; 
[NonSerialized]
public decimal total; 
[OptionalField]
public bool taxable;

public ShoppingCartItem(int _productId, decimal _price, int _quantity)
{
productId = _productID;
price = _price;
quantity = _quantity;
total = price * quantity;
}

private static void IDeserializationCallBack_OnDeserialization(object sender)
Implements IDeserializationCallback.OnDeserialization
// After deserialization, calculate the total
total = price * quantity;
}
}

5.3.3 Formatter Classes

Consider that you have been assigned a task of creating an application that customers can use to order supplies from your company. These customers may belong to various classes such as New, Standard, and Preferred. You will need to store the data about the customers and use the stored data to allow the customers to place orders. Preferred customers gain a 10 percent savings on all orders. When a customer attains the Preferred status, you need to change the storage of the Customer object while serializing it.

The .NET Framework provides three Formatter classes: Formatter, FormatterConverter, and FormatterServices. These classes provide a base for such conversion and reconstruction of the serialized data into an object form. These classes belong to the System.Runtime.Serialization namespace. You can use these classes when creating custom formatters for a certain type to meet requirements that are either not achievable by the predefined formatters or can be implemented in a more efficient manner through customized formatters. For example, you can change the Standard customer to a Preferred customer.

The following table describes the Formatter classes and their commonly used members.

5.3.4 Serialization Events

The .NET Framework serialization architecture provides certain serialization event handler attributes as a provision to execute application-specific activities based on certain events related to serialization and deserialization processes. Consider a situation where you need to create an E-commerce Insurance application, which allows customers to purchase insurance from different insurance companies through your agency. You can create a Customer object to represent the insurance-related information of a customer whenever the customer attempts to purchase the insurance by using a credit card.

You may want to call a method when the Customer object is serialized. This method encrypts the credit card information before it is sent to the processing application. You can use the event handler attributes as an instruction for particular methods, so that the methods get executed per the event corresponding to the specified attribute. The four types of events that you can handle are events before or after the serialization and events before or after the deserialization. The event handler attributes that you can use for these four events are OnSerializingAttribute, OnSerializedAttribute, OnDeserializingAttribute, and OnDeserializedAttribute, respectively. The method for which you specify any of these attributes has to contain a StreamingContext parameter so that the method is aware of the streaming and knows which StreamingContext to participate in. These attributes help you specify or designate a method to be executed in response to a particular action that they are associated with, such as serializing or deserializing.

By using the event handler attributes, you can customize the values in an object before it is serialized and restore it after serialization. You can perform similar tasks before and after deserialization of the corresponding object. This helps you perform customized context-specific serializations and restore the state of the object to ensure that the object retains a compatible state for the current context.

The following are the event handler attributes:

  • OnSerializingAttribute 
    This attribute when applied to a particular method in a class specifies that the method has to be executed just before an object is serialized. Such activities may include assigning appropriate values to the fields in the object depending on the current serialization context. This could be when you are sending an object and want to assign default values to any null fields. You can create the FillNulls method to populate the fields by marking this method with OnSerializingAttribute. You can use the StreamingContext parameter to retrieve information related to the context in which the serialization is taking place. In the context of the E-commerce Insurance application example, OnSerializingAttribute can be applied to a method that will be called when the Customer object is being serialized. The method contains the code that can encrypt the customer credit card information and can be executed just before the object is called for serialization.
  • OnSerializedAttribute 
    This attribute when applied to a method specifies that the method has to be executed after an object is serialized. You can use this method to perform activities, such as restoring the values of certain fields in the object, if they were changed for specific needs prior to the serialization. You can use the StreamingContext parameter to retrieve information related to the context in which the serialization is taking place. In the context of the E-commerce Insurance application example, you can apply OnSerializedAttribute to a method that can be used to determine the Insurer application to which the customer information is sent. This helps you build generic customer details and then the ability to send those customer details to multiple insurers.
  • OnDeserializingAttribute 
    This attribute when applied to a particular method in a class specifies that the method has to be executed just before an object of the corresponding class gets deserialized. This attribute helps you in performing basic initialization tasks of the fields in the object. In this aspect, the method for which this attribute is specified performs tasks similar to a constructor. For example, you can use such methods to assign default values to certain fields in the object, so that even if these values are not updated during deserialization, the object has the default values being specified for the corresponding fields. In the context of the E-commerce Insurance application example, you can apply OnDeserializingAttribute to a method in the Insurance application, which will allow it to determine the type of customer details, the type of insurance requested, and credit card information.
  • OnDeserializedAttribute 
    This attribute when applied to a particular method in a class specifies that the method has to be executed immediately after an object of the corresponding class is deserialized. This attribute can be used to execute methods performing some value fixing tasks on an object immediately after it is deserialized and before the object graph is reconstructed. For example, you may want to calculate the area of a Rectangle object based on the values for length and width of the Rectangle object, which were received during deserialization. You can use this attribute interchangeably with the implementation of IDeserializationCallback interface, which provides a method that can be used to perform logically similar tasks. In the context of the E-commerce Insurance application example, you can apply OnDeserializedAttribute to a method that can decrypt the credit card information to be processed by the application.

In addition to the above listed event handler attributes, the .NET Framework serialization architecture provides the OptionalFieldAttribute attribute.

  • OptionalFieldAttribute 
    You can use this attribute to enable version-tolerant serialization of a particular type. This attribute helps you mark members of a class as optional for serialization or deserialization. This enables version-tolerant serialization of types created for older versions of an application that serializes data. In most cases, during deserialization, if a member of a certain type is not found in a corresponding serialized stream, the runtime generates an exception. Such a situation may arise if a certain type whose object is being serialized has undergone new additions of members in its subsequent versions and the serialized stream represents an object of an older version. If you specify this attribute for new members, it enables normal processing of the older data without any exception being thrown by the formatter. The formatter does not throw an exception if the members are missing or new members are encountered. The benefit is that this attribute gives you the flexibility in modifying classes that are being serialized or deserialized.

The following code sample shows how to implement event handler attributes by using the ISerializable interface and IDeserializableCallback with CustomFormatter and its methods. The code implements both the ISerializable and the IDeserializableCallback interfaces in a class named Rectangle, which gives the result of the area of a rectangle whose length and width are passed through the constructor of the class. The class defines five variables, which are initialized through a constructor and five methods with different event handling attributes. There is an optional field among the variables marked by OptionalFieldAttribute. The class also defines the GetObjectData method to initialize the SerializationInfo class before serialization and the OnDeserialization method, which implements the OnDeserialization method of IDeserializableCallback to get the result after deserialization.

The Rectangle class also provides two methods, Serialize and Deserialize, which serialize and deserialize the object of the Rectangle class respectively, by using the CustomFormatter class. The CustomFormatter class implements IFormatter and IFormatterConverter interfaces to create the custom formatter. The CustomFormatter class provides three properties and two methods, Serialize and Deserialize, which implement the serialize and deserialize methods of the IFormatter interface. The length and the width of the rectangle, stored in the SerializationInfo class, are transformed into Bytes by the Convert method that implements the Convert method of the IFormatterConverter interface. Later in the code, the Main method shows how to create and serialize an object of the Rectangle class into a binary file on the disk by custom formatter and shows how EventHandlerAttributes and OptionalFieldAttribute are used during serialization and deserialization. While doing so, the object is serialized per the implementation provided by the serialize method in the CustomFormatter class. Finally, the code creates a second object of the Rectangle class by deserializing the data in the binary file and displays the newly created object on the console. The data in the binary file is deserialized per the implementation provided for the deserialize method in the CustomFormatter class.

using System;
using System.IO;
using System.Text;
using System.Xml.Serialization;
using System.Runtime.Serialization;

class EventHandlerAttributes
{
public static void Main()
{
Rectangle ob = new Rectangle();
Console.WriteLine(" Before serialization the object contains: ");
ob.Print();
Rectangle.Serialize();
Console.WriteLine(" After serialization the object contains: ");
ob.Print();
Rectangle.Deserialize();
Console.WriteLine( " After deserialization the object contains: ");
ob.Print();
Console.ReadLine();
}
}

[Serializable()]class Rectangle : ISerializable, IDeserializationCallback
{
public Double LengthRect;
public Double WidthRect;

private SerializationInfo InfoSerial;
private StreamingContext ContextStream;

[NonSerialized()]public Double AreaRect;

public Rectangle(Double length, Double width)
{
this.LengthRect = length;
this.WidthRect = width;
}

public Rectangle(SerializationInfo
info, StreamingContext context)
{
this.InfoSerial = info;
this.ContextStream = context;
}

public void OnDeserialization(Object sender)
{
this.LengthRect = this.InfoSerial.GetDouble("LengthRect");
this.WidthRect = this.InfoSerial.GetDouble("WidthRect");
AreaRect = LengthRect * WidthRect;
}

public override string ToString()
{
this.AreaRect = this.LengthRect * this.WidthRect;
return string.Format("Length={0}, Width={1}, Area={2}", LengthRect, WidthRect, AreaRect);
}

public void GetObjectData(System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext context)
{
info.AddValue("LengthRect", this.LengthRect);
info.AddValue("WidthRect", this.WidthRect);
}

public static void Serialize()
{
Rectangle c = new Rectangle(10, 20);
Console.WriteLine("Object being serialized: " + c.ToString());

FileStream fs = new FileStream("c:\\DataFile.dat", FileMode.Create);

CustomFormatter formatter = new CustomFormatter();
try
{
formatter.Serialize(fs, c);
}
catch (SerializationException e)
{
Console.WriteLine("Failed to serialize. Reason: " + e.Message);
throw;
}
finally
{
fs.Close();
}
}

public static void Deserialize()
{
Rectangle c = null;
FileStream fs = new FileStream("c:\\DataFile.dat", FileMode.Open);
try
{
CustomFormatter formatter = new CustomFormatter();
c = ((Rectangle)(formatter.Deserialize(fs)));
Console.WriteLine("Object being serialized: " + c.ToString());
}
catch (SerializationException e)
{
Console.WriteLine("Failed to deserialize. Reason: " + e.Message);
throw;
}
finally
{
fs.Close();
}
}

public int Var1;
private String var2;
[NonSerialized()]public string Var3;
private String var4;
[OptionalFieldAttribute(VersionAdded = 2)]String optionalField;

public Rectangle()
{
Var1 = 11;
var2 = "Hello World!";
Var3 = "This is a nonserialized value";
var4 = null;
optionalField = "This is an optional value";
}

public void Print()
{
Console.WriteLine(" variable1 = " + Var1);
Console.WriteLine(" variable2 = " + var2);
Console.WriteLine(" variable3 = " + Var3);
Console.WriteLine(" variable4 = " + var4);
Console.WriteLine(" variable5 = " + optionalField);
}

[OnSerializing()]
internal void OnSerializingthisthod(StreamingContext context)
{
var2 = "This value went into the data file during serialization.";
}

[OnSerialized()]
internal void OnSerializedthisthod(StreamingContext context)
{
var2 = "This value was reset after serialization.";
}

[OnDeserializing()]
internal void OnDeserializingthisthod(StreamingContext context)
{
Var3 = "This value was set during deserialization";
}

[OnDeserialized()]
internal void OnDeserializedthisthod(StreamingContext context)
{
var4 = "This value was set after deserialization.";
}
}

public class CustomFormatter : System.Runtime.Serialization.IFormatter, IFormatterConverter
{
public System.Runtime.Serialization.SerializationBinder Binder
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}

public System.Runtime.Serialization.StreamingContext Context
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}

public object Deserialize(System.IO.Stream serializationStream)
{
StreamReader st = new StreamReader(serializationStream);
string str = st.ReadToEnd();
char[] temp = new char[] { char.Parse(",") };
string[] s = str.Split(temp);

SerializationInfo ss = new SerializationInfo(typeof(Rectangle), new CustomFormatter());
ss.AddValue("LengthRect", double.Parse(s[0]));
ss.AddValue("WidthRect", double.Parse(s[1]));

Rectangle ob = new Rectangle(ss, new StreamingContext());
if ((ob) is IDeserializationCallback)
{
ob.OnDeserialization(ob);
}

return ob;
}

public void Serialize(System.IO.Stream serializationStream, object graph)
{
Rectangle g = ((Rectangle)(graph));

SerializationInfo ss =
new SerializationInfo(typeof(Rectangle), new CustomFormatter());
if ((g) is ISerializable)
{
g.GetObjectData(ss, new StreamingContext());
}
byte[] ba = (byte[])this.Convert(ss, System.TypeCode.Byte);
serializationStream.Write(ba, 0, ba.Length);
}

public System.Runtime.Serialization.ISurrogateSelector SurrogateSelector
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
public object Convert(object value, System.TypeCode typeCode)
{
byte[] ba =
Encoding.ASCII.GetBytes(((SerializationInfo)(value)).GetDouble("LengthRect") + "," +
((SerializationInfo)(value)).GetDouble("WidthRect"));
return ba;
}

public object Convert(object value, System.Type typeCode)
{
byte[] ba = Encoding.ASCII.GetBytes(((SerializationInfo)(value)).GetDouble("LengthRect") +
"," + ((SerializationInfo)(value)).GetDouble("WidthRect"));
return ba;
}


// All of these have to be here because IFormatterConverter is being implemented...

public bool ToBoolean(object value)
{ throw new NotSupportedException(); }

public byte ToByte(object value)
{ throw new NotSupportedException(); }

public char ToChar(object value)
{ throw new NotSupportedException(); }

public System.DateTime ToDateTime(object value)
{ throw new NotSupportedException(); }

public decimal ToDecimal(object value)
{ throw new NotSupportedException(); }

public double ToDouble(object value)
{ throw new NotSupportedException(); }

public short ToInt16(object value)
{ throw new NotSupportedException(); }

public int ToInt32(object value)
{ throw new NotSupportedException(); }

public long ToInt64(object value)
{ throw new NotSupportedException(); }

public SByte ToSByte(object value)
{ throw new NotSupportedException(); }

public float ToSingle(object value)
{ throw new NotSupportedException(); }

public string ToString(object value)
{ throw new NotSupportedException(); }

public ushort ToUInt16(object value)
{ throw new NotSupportedException(); }

public uint ToUInt32(object value)
{ throw new NotSupportedException(); }

public ulong ToUInt64(object value)
{ throw new NotSupportedException(); }
}

The .NET Framework supports a number of serialization events if you are using the BinaryFormatter class. For SoapFormatter or custom serialization, you are limited to to using the IDeserializationCallback interface. There are four serialization events:

  • Serializing - event raised just before serialization takes place
  • Serialized - event raised just after the serialization takes place
  • Deserializing - event raised just before deserialization takes place
  • Deserialized - event raised just after the deserialization takes place

The stream is untouched, any changes being made before or after. For a method to respond to any one of these events, it must have the attibute matching the event eg. OnSerializing and it must accept a StreamingContext object as its parameter, and it must be a Sub rather than a Function (in C# parlance, 'it must return void'). The code below shows an example: 

[Serializable]
class ShoppingCartItem

public int product; 
public decimal price; 
public int quantity; 
[NonSerialized]
public decimal total; 

[OnSerializing]
void CalculateTotal(StreamingContext sc)
{
total = price * quantity;
}

[OnDeserialized]
void CheckTotal(StreamingContext sc)
{
if (total == 0) { CalculateTotal() }
}
}
}

5.3.5 Managing Deserialized Objects using ObjectManager class

You are creating an E-commerce Insurance application. This application allows the end user to select the type of insurance, enter data appropriate to the type of insurance, and then request that insurance with payment from insurance companies. The Insurer application receives the applications from the E-commerce User Interface application and then processes data according to the options selected by the users. When the E-commerce User Interface application submits the data to the Insurer application, it includes information about what type of insurance is requested.

The ObjectManager class assists you in recovering objects from the stream during deserialization. This class belongs to the System.Runtime.Serialization namespace. During deserialization, the formatter responsible for the corresponding deserialization queries the ObjectManager to determine whether a reference to an object in the serialized stream refers to an object that has already been deserialized (a backward reference), or to an object that has not yet been deserialized (a forward reference). In case of a backward reference, Formatter immediately completes the reference. In case of a forward reference, Formatter can register a fixup with the ObjectManager. Fixup refers to the process of finalizing object references that are not already completed during deserialization. This allows the ObjectManager to complete the reference after the required object is deserialized. Therefore, the ObjectManager class helps you track objects in a particular serialized stream when they are deserialized.

The following code sample shows how to implement the ObjectManager class during serialization. The code deals only with the deserialization part because the object manager plays a crucial role at the time of deserialization. The code provides one structure called SampleStructure and two serializable classes called SampleClass1 and SampleClass2, which implement the ISerializable interface. The code shows how to fix the variable varObject of type Object after deserialization. The Main method shows how to use the methods RegisterObject, RecordFixUp, and DoFixUps of the ObjectManager class.

 

using System;
using System.Text;
using System.Runtime.Serialization;

class TestObjectManager
{
static void Main()
{
SerializationInfo infoClass1 =
new SerializationInfo(typeof(SampleClass1),
new FormatterConverter());

SampleStructure sampleStruct = new SampleStructure();
sampleStruct.VarInt = 100;
infoClass1.AddValue("ObjSampleClass1", sampleStruct);

// infoClass2 will be used to deserialize
// an instance of SampleClass2.

SerializationInfo infoClass2 =
new SerializationInfo(typeof(SampleClass2),
new FormatterConverter());
infoClass2.AddValue("VarInt", 4);

// Build the object graph we are aming for using the
// SerializationInfo objects.

ObjectManager objMan =
new ObjectManager(null,
new StreamingContext(StreamingContextStates.All, null));

// Create an empty SampleClass1 and register it.

object objSampleClass1 =
FormatterServices.GetUninitializedObject(typeof(SampleClass1));

// Create an empty SampleStructure and register it.

object obj = infoClass1.GetValue("ObjSampleClass1",
typeof(SampleStructure));

objMan.RegisterObject(obj, 2, null, 1, null);

// Ask the ObjectSampleStructure to perform
// 'obj.varObject = objSampleClass2'
// assignment during fixup.

objMan.RecordFixup(2, typeof(SampleStructure).GetField("VarObject"), 3);

// Create an empty SampleClass2 and register it.

object objSampleClass2 =
FormatterServices.GetUninitializedObject(typeof(SampleClass2));

objMan.RegisterObject(objSampleClass2, 3, infoClass2);
objMan.RegisterObject(objSampleClass1, 1, infoClass1);

Console.WriteLine("VarInt = {0}, VarObject = {1}",
((SampleStructure)obj).VarInt,
((SampleStructure)obj).VarObject == null ? "null" : "NOT null");

// varInt = 100, varObject = null
// The fixup hasn't happend yet.

objMan.DoFixups();

Console.WriteLine("VarInt = {0}, VarObject = {1}",
((SampleStructure)obj).VarInt,
((SampleStructure)obj).VarObject == null ? "null" : "NOT null");

// varInt = 100, varObject = NOT null
// The fixup has happened.

SampleClass1 root = (SampleClass1)objMan.GetObject(1);

Console.WriteLine("VarInt = {0}, VarObject = {1}",
root.ObjSampleClass1.VarInt,
root.ObjSampleClass1.VarObject == null ? "null" : "NOT null");

// objSampleClass1 is being completed before the obj.varObject fixup,
// so the value copy in SampleClass1 is copying the unfixed-up
// object.

Console.ReadLine();
}
}

[Serializable]
class SampleClass1 : ISerializable
{
public SampleStructure ObjSampleClass1;

SampleClass1(SerializationInfo info, StreamingContext context)
{
ObjSampleClass1 = (SampleStructure)info.GetValue("ObjSampleClass1",
typeof(SampleStructure));
}

public void GetObjectData(SerializationInfo info,
StreamingContext context)
{
// We do not need this as we are only deserializing, not serializing.
}
}

[Serializable]
struct SampleStructure
{
public int VarInt;
public object VarObject;
}

[Serializable]
class SampleClass2 : ISerializable
{
public int VarInt;

private SampleClass2(SerializationInfo info, StreamingContext context)
{
VarInt = info.GetInt32("VarInt");
}
public void GetObjectData(SerializationInfo info,
StreamingContext context)
{
// We do not need this as we are only deserializing, not serializing.
}
}

 

 

 

 

 

 

 

原文地址:https://www.cnblogs.com/malaikuangren/p/2568931.html