8.3 Serialization

only for RuBoard

Reading and writing primitive data types to and from a stream is one thing, but what about objects? Converting an object to a stream is called serialization , and it is used for two primary purposes. The first is to persist an object to storage (such as a file) with the intent of recreating it later. The second is to send an object by value (marshal by value) to another computer or application domain. Serialization is necessary because an object is valid only within the application domain in which it was created. If the architecture demands that objects be shared or used in a distributed environment, then serialization is necessary. Serialization plays a major role in .NET remoting and, fortunately, the framework does most of the work.

When an object is serialized, the name of the assembly (this is the type boundary, after all), the class, and the member data are added to the stream. During this process, any referenced objects are serialized, too (if they are eligible). This is often called an object graph . Circular references are not a problem in the case of object graphs because the CLR keeps track of each serialized object to ensure that the object is not serialized more than once.

This chapter discusses two types of serialization:

  • Binary serialization

  • XML and SOAP serialization

Binary serialization maintains type integrity, which means that every aspect of the object is preserved, including private member data. Maintaining type integrity is useful in situations when state must be maintained , such as in a tightly coupled distributed application.

XML serialization, on the other hand, is not concerned with preservation. Only the public properties and fields of the class are serialized. The assembly name is not included, so technically an object that was serialized to XML no longer represents a type; it's just data. This is perfect in situations when the data must be consumed and its source is irrelevant.

For instance, a shopping cart object(s) could be serialized to XML to represent an order and passed to an order processor. The order processor doesn't care that the data came from the shopping cart; it just wants XML that conforms to a particular XSD schema. Because there is no affinity between the shopping cart and the order processor, it is now possible to get orders from anywherefrom an affiliated web site, perhaps. In other words, receiving orders is not limited to using a shopping cart on the web site. Using binary serialization in this situation would bind the shopping cart and the order processor together because the order processor would need specific knowledge of the shopping cart type to deserialize it.

8.3.1 Serializable and NonSerialized Attributes

Regardless of the serialization technique, the runtime has to know that an object can be serialized. As shown in Example 8-7, this is done by applying the Serializable attribute to the type. Anything that requires exclusion from the serialization process can opt out with the NonSerialized attribute.

Example 8-7. Serialization attributes
 <Serializable( )> _ Public Class Employee          Public Sub New( )         Console.WriteLine("Constructor called")     End Sub         <NonSerialized( )> _     Public employeeID As Integer 'Primary key in database          Public FirstName As String      Public LastName As String      Public Department As String       End Class     Public Class Test         Public Shared Sub Main( )                  Dim empIn As New Employee( )         empIn.employeeID = 42         empIn.FirstName = "Bruce"         empIn.LastName = "Banner"         empIn.Department = "Nuclear Engineering"  'Serialization code will go here  End Sub      End Class 

Normally, data that is excluded has no meaning outside the context of its current domain. Using the shopping cart/order processor example, quite a bit of data might be used to maintain the shopping cart that does not relate to an order directly. For instance, the order processor just needs item identifiers and payment information to do its job. It doesn't need descriptive text for the items in the cart (this text is used for the benefit of the customer when the shopping cart is rendered to HTML).

Ignore the fact that the Employee class is horribly designed. Just to keep things simple, everything that will be serialized is contained in a public field and left wide open , with no encapsulation of data whatsoever. The exception is the employeeID field, which is the primary key for the employee record in the database ( assuming there is a database somewhere). This field demonstrates the NonSerialized attribute and gives the sample a minute degree of credibility.

8.3.2 Binary Serialization

To serialize the Employee object from Example 8-7 into binary form, the System.Runtime.Serialization.Formatters namespace must be imported to provide access to the BinaryFormatter class.

A formatter is simply a class designed to serialize an object to a specific format. In .NET, there are two formatters: BinaryFormatter and SoapFormatter . Another class, called XMLSerializer , can serialize an object to XML. This process will be discussed shortly.

Once the object is ready for serialization (i.e., it has state), the binary formatter can be used in conjunction with a FileStream to write the object to a file in only a few lines of code. Example 8-8 illustrates this; it is identical to Example 8-7 except for the code that handles binary serialization, which is shown in boldface.

Example 8-8. Binary formatters, Part I
 Imports System Imports System.IO Imports System.Runtime.Serialization.Formatters.Binary     <Serializable( )> _ Public Class Employee          Public Sub New( )         Console.WriteLine("Constructor called")     End Sub         <NonSerialized( )> _     Public employeeID As Integer 'Primary key in database          Public FirstName As String      Public LastName As String      Public Department As String       End Class     Public Class Test         Public Shared Sub Main( )             Dim emp As New Employee( )         emp.employeeID = 42         emp.FirstName = "Bruce"         emp.LastName = "Banner"         emp.Department = "Nuclear Engineering"  Dim formatter As New BinaryFormatter( )   Dim streamIn As New FileStream("employee.bin", _   FileMode.Create, _   FileAccess.Write)   formatter.Serialize(streamIn, emp)   streamIn.Close( )     Console.Write("Serialized. Press ENTER to continue...")   Console.ReadLine( )    'Deserialization code will go here         End Sub     End Class 

The formatter contains all of the functionality: a Serialize method and a Deserialize method. The Serialize method requires a stream and an object instance to fulfill its obligations. As shown in Example 8-9, Deserialize requires only a stream to an object storein this case, employee.bin .

Example 8-9. Binary formatters, Part II
 Imports System Imports System.IO Imports System.Runtime.Serialization.Formatters.Binary     <Serializable( )> _ Public Class Employee          Public Sub New( )         Console.WriteLine("Constructor called")     End Sub         <NonSerialized( )> _     Public employeeID As Integer 'Primary key in database          Public FirstName As String      Public LastName As String      Public Department As String       End Class     Public Class Test         Public Shared Sub Main( )             Dim empIn As New Employee( )         empIn.employeeID = 42         empIn.FirstName = "Bruce"         empIn.LastName = "Banner"         empIn.Department = "Nuclear Engineering"             Dim formatter As New BinaryFormatter( )         Dim streamIn As New FileStream("employee.bin", _                             FileMode.Create, _                             FileAccess.Write)         formatter.Serialize(streamIn, empIn)         streamIn.Close( )             Console.Write("Serialized. Press ENTER to continue...")         Console.ReadLine( )  Console.WriteLine("Deserializing...")   Dim streamOut As New FileStream("employee.bin", _   FileMode.Open, _   FileAccess.Read)   Dim empOut As Employee = _   CType(formatter.Deserialize(streamOut), Employee)   Console.WriteLine(empOut.employeeID.ToString( ))   Console.WriteLine(empOut.FirstName)   Console.WriteLine(empOut.LastName)   Console.WriteLine(empOut.Department)   Console.Write("Complete. Press ENTER to continue...")   Console.ReadLine( )  End Sub     End Class 

After opening employee.bin for reading, formatter.Deserialize is called to deserialize the object from the stream. CType casts the returned Object to an Employee type, and the object's state is then written to the console. It should appear as follows :

 0 Bruce Banner Nuclear Engineering 

The first item of displayed information, the employeeID , is because it was not serialized. Really, the whole process is reminiscent of cooking dehydrated mashed potatoes from a box.

When designing objects that must be persisted or transported remotely, remember that constructors are not called when objects are deserialized.

8.3.2.1 ICloneable and MemoryStream

Binary formatting is also used to clone objects. Chapter 5 discussed the ICloneable interface, but the details of how to implement it had to be left out until now; it requires a memory stream and a binary formatter.

The interface has one method called Clone , which is supposed to return a copy of an object. When the call is complete, there should be two identical object instances, as opposed to two references on one object.

Example 8-10 modifies the Employee class from the last example to implement the ICloneable interface. The implementation is actually contained in a private method called CloneImp. This is done because ICloneable.Clone returns an Object . By implementing the interface privately, the public method Employee.Clone can delegate to CloneImp and convert the returned Object into an Employee for the caller; definitely a more type-safe approach.

Example 8-10. Object cloning
 Imports System Imports System.IO Imports System.Runtime.Serialization.Formatters.Binary     <Serializable( )> _ Public Class Employee  Implements ICloneable  <NonSerialized( )> _     Public employeeID As Integer 'Primary key in database         Public FirstName As String     Public LastName As String     Public Department As String  Public Function Clone( ) As Employee   Return CType(CloneImp( ), Employee)   End Function   Private Function CloneImp( ) As Object Implements ICloneable.Clone   Dim stream As New MemoryStream( )   Dim formatter As New BinaryFormatter( )   formatter.Serialize(stream, Me)   stream.Seek(0, SeekOrigin.Begin)   Return formatter.Deserialize(stream)   End Function  End Class     Public Class Test         Public Shared Sub Main( )         Dim empIn As New Employee( )         empIn.employeeID = 42         empIn.FirstName = "Bruce"         empIn.LastName = "Banner"         empIn.Department = "Nuclear Engineering"             Dim empOut As Employee = empIn.Clone( )             Console.WriteLine(empOut.employeeID.ToString( ))         Console.WriteLine(empOut.FirstName)         Console.WriteLine(empOut.LastName)         Console.WriteLine(empOut.Department)             Console.ReadLine( )         End Sub     End Class 

Nothing in this example is new. A memory stream works just like any other stream, and the binary formatter doesn't care about what type of stream it has. Once the object is serialized into the stream, a binary copy of the object is in memory. The only thing left to do is turn right around and deserialize it (don't forget to move the stream position back to the beginning first).

The ICloneable implementation is overkill for this class, but think about an extremely large object. Making a copy of an object that has a hundred members in only five lines of code is definitely worth this minimal amount of effort.

When writing Exception classes, make sure they are decorated with the Serializable attribute so that the exceptions can be thrown across application domains and machine boundaries.

8.3.3 SOAP Serialization

Check the contents of employee.bin to verify that it contains binary data; with a few simple changes to Example 8-7 through Example 8-9, it can be serialized to SOAP-compliant XML.

Only two things need to be done to this example to accomplish this goal. First, run a global find and replace on the source file and change every instance of "Binary" to "Soap." The second change occurs at compile time. An additional DLL must be referenced. It probably has the longest name in the modern programming world:

 vbc /t:exe /r:System.Runtime.Serialization.Formatters.Soap.dll     employee.vb 

Change the name of the storage file to employee.xml . Then, as shown in Figure 8-1, the results can be viewed in Internet Explorer, which formats XML in a hierarchical display.

Figure 8-1. Employee object as SOAP-compliant XML
figs/oop_0801.gif

8.3.4 XML Serialization

If absolute control over serialization is needed to ensure conformation to a specific XML schema definition (XSD), look no further than the XmlSerializer class found in the System.Xml.Serialization namespace. It does it all: defining namespaces, encoding fields as attributes or elements, and renaming elements and attributes to accommodate any need.

XML serialization has some limitations. For one, only public properties and fields are serialized. Also, properties and fields must be read/write; read-only properties and fields are not serialized. This limitation could lead to some very bad class design; in order for XML serialization to work, all data for a given class must be exposed.

Think about what it means to serialize an object as XML versus binary data, and then make the decisions regarding design. Binary serialization involves maintaining the complete state of an object between uses. The complete type is preserved (assembly, class name, and private variables ) so that when deserialization occurs, exactly the same object is recreated. XML serialization, on the other hand, describes an object; after all, that is what the public properties of a class represent. This description is useful when parts of an application need to consume data and do not care about objects, let alone maintaining their state.

The tendency exists to expose more in order to serialize an object to XML. Resist the temptation . Binary and XML serialization are used for two different kinds of applications. The old rules still apply; hide as much data as possible.

Example 8-11 contains a short listing that demonstrates how to use the Employee class from the previous examples. It's nothing new, which is part of the beauty of the .NET framework. The constructor for XmlSerializer takes a Type parameter that is obtained by calling Object.GetType . Remember, a class represents an object of some kind. A Type , in this instance, represents the class itself.

Example 8-11. XML serialization
 Imports System Imports System.IO Imports System.Xml.Serialization     <Serializable( )> _ Public Class Employee         <NonSerialized( )> _     Public employeeID As Integer 'Primary key in database         Public FirstName As String     Public LastName As String     Public Department As String     End Class     Public Class Test         Public Shared Sub Main( )         Dim emp As New Employee( )         emp.employeeID = 9112001         emp.FirstName = "Peter"         emp.LastName = "Parker"         emp.Department = "Photography"  Dim employeeType As Type = emp.GetType( )   Dim serializer As New XmlSerializer(employeeType)   Dim writer As New StreamWriter("employee.xml")   serializer.Serialize(writer, emp)   writer.Close( )  End Sub     End Class 

The output to the employee.bin file (shown below) that results from this code is interesting because the XML serializer does not seem to care that the employeeID field is marked NonSerialized . To let the cat out of the bag, only the binary and the SOAP formatters are aware of Serializable and NonSerialized . The XML serializer does not care about the presence of these attributes, and it works off a whole different set of attributes and classes.

 <?xml version="1.0" encoding="utf-8"?> <Employee xmlns:xsd=   http://www.w3.org/2001/XMLSchema   xmlns:xsi=   http://www.w3.org/2001/XMLSchema-instance   >   <employeeID>9112001</employeeID>   <FirstName>Peter</FirstName>   <LastName>Parker</LastName>   <Department>Photography</Department> </Employee> 

By default, fields are serialized as XML elements, meaning they are wrapped in XML tags like this:

 <FirstName>Peter Parker</FirstName> 

If attributes are preferred, the XMLAttribute attribute can affect this change. It generates XML that looks like this:

 <Employee FirstName="Peter Parker" .../> 

Example 8-12 shows the Employee class again (it can replace the Employee class, shown in Example 8-11, with the rest of the code in Example 8-11 remaining unchanged). This time it is configured so that the fields are rendered as attributes. The XMLIgnore (as opposed to NonSerialized ) attribute prevents the employeeID field from being serialized.

Example 8-12. XML serialization as attributes
 Public Class Employee          <XmlIgnore( )> _     Public employeeID As Integer 'Primary key in database          <XmlAttribute( )> _     Public FirstName As String     <XmlAttribute( )> _     Public LastName As String      <XmlAttribute( )> _     Public Department As String      End Class 

Serializing this to XML now produces the output shown below. Notice that no elements are under Employee because all the fields were rendered as attributes:

 <?xml version="1.0" encoding="utf-8"?> <Employee xmlns:xsd=   http://www.w3.org/2001/XMLSchema   xmlns:xsi=   http://www.w3.org/2001/XMLSchema-instance   FirstName="Peter"  LastName="Parker" Department="Photography" /> 

The System.Xml.Serialization namespace contains everything necessary to ensure that an object that conforms to any given schema can be serialized to XML. The XML output shown earlier declares two namespaces, and each has a qualifying prefix ( xsi and xsd , respectively). This is the default behavior of the serializer, but it can be changed by using the XmlType attribute, as shown here (the local IP is used to protect the innocent):

  <XmlType(Namespace:="http://192.168.1.1/", _   TypeName:="NotoriousEmployee")> _  Public Class Employee         <XmlIgnore( )> _     Public employeeID As Integer 'Primary key in database         Public FirstName As String     Public LastName As String     Public Department As String     End Class 

Running the code from Example 8-11 with this class produces the following output:

 <?xml version="1.0" encoding="utf-8"?> <NotoriousEmployee xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http:// www.w3.org/2001/XMLSchema-instance">   <FirstName xmlns="http://192.168.1.1/">Peter</FirstName>   <LastName xmlns="http://192.168.1.1/">Parker</LastName>   <Department xmlns="http://192.168.1.1/">Photography</Department> </NotoriousEmployee> 

Every field now has a namespace associated with it. However, this association might not be optimal. Namespace qualifiers are sometimes needed, but to get them, the serialization code needs to be modified. It's not done with an attribute but it's done using the XmlSerializerNamespaces class.

XmlSerializerNamespaces has a method named Add that takes a key-value pair. As demonstrated in Example 8-13, the key is the prefix associated with the namespace, while the value is the XML namespace itself. After the qualifiers are defined, the object can be passed to XMLSerializer , which has an overloaded Serialize method that accepts an instance of XmlSerializerNamespaces .

Example 8-13. Using XmlSerializerNamespaces
  <XmlType(Namespace:="http://192.168.1.1", _   TypeName:="NotoriousEmployee")> _  Public Class Employee         <XmlIgnore( )> _     Public employeeID As Integer 'Primary key in database         Public FirstName As String     Public LastName As String     Public Department As String     End Class     Public Class Test         Public Shared Sub Main( )             Dim emp As New Employee( )         emp.employeeID = 9112001         emp.FirstName = "Peter"         emp.LastName = "Parker"         emp.Department = "Photography"  Dim xmlNamespace As New XmlSerializerNamespaces( )   xmlNamespace.Add("eye", "http://192.168.1.1")  Dim employeeType As Type = emp.GetType( )         Dim serializer As New XmlSerializer(employeeType)         Dim writer As New StreamWriter("employee.xml")         serializer.Serialize(writer, emp, xmlNamespace)         writer.Close( )         End Sub     End Class 

Now the output contains your own namespace, and all elements will be qualified with a prefix instead of an xmlns attributewhich is exactly what you want. The output appears as follows:

 <?xml version="1.0" encoding="utf-8"?> <NotoriousEmployee xmlns:eye="http://192.168.1.1">   <eye:FirstName>Peter</eye:FirstName>   <eye:LastName>Parker</eye:LastName>   <eye:Department>Photography</eye:Department> </NotoriousEmployee> 

The namespace prefix that is passed to XMLSerializerNamespaces can be anything. However, the XML namespace must match the namespace used by the XMLType attribute.

XML in .NET is one of those topics that is so large that a whole book (and then some) could be written about it. If the chance arises, visit the System.Xml.Serialization documentation because what is covered here is only a small part of the picture. Just because this book doesn't discuss a topic doesn't mean it can't be done. XML in .NET is very robust; it has to be. After all, .NET uses XML serializationit's a core part of the XML web services architecture.

only for RuBoard


Object-Oriented Programming with Visual Basic. Net
Object-Oriented Programming with Visual Basic .NET
ISBN: 0596001460
EAN: 2147483647
Year: 2001
Pages: 112
Authors: J.P. Hamilton

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net