Serialization in .Net
Serializing Objects
When you create an object in a .NET Framework application, you probably never
think about how the data is stored in memory. You shouldn't have to—the .NET
Framework takes care of that for you. However, if you want to store the contents of an
object to a file, send an object to another process, or transmit it across the network,
you do have to think about how the object is represented because you will need to convert it to a different format. This conversion is called serialization.
What Is Serialization?
Serialization, as implemented in the System.Runtime.Serialization namespace, is the
process of serializing and deserializing objects so that they can be stored or transferred
and then later re-created. Serializing is the process of converting an object into
a linear sequence of bytes that can be stored or transferred. Deserializing is the process
of converting a previously serialized sequence of bytes into an object.
How to Serialize an Object
At a high level, the steps for serializing an object are as follows:
1. Create a stream object to hold the serialized output.
2. Create a BinaryFormatter object (located in System.Runtime.Serialization.Formatters
.Binary).
3. Call the BinaryFormatter.Serialize method to serialize the object, and output the
result to the stream.
At the development level, serialization can be implemented with very little code. The following
console application—which requires the System.IO, System.Runtime.Serialization,
and System.Runtime.Serialization.Formatters.Binary namespaces—demonstrates this:
' VB
Dim data As String = "This must be stored in a file."
' Create file to save the data to
Dim fs As FileStream = New FileStream("SerializedString.Data", _
FileMode.Create)
' Create a BinaryFormatter object to perform the serialization
Dim bf As BinaryFormatter = New BinaryFormatter
' Use the BinaryFormatter object to serialize the data to the file
bf.Serialize(fs, data)
' Close the file
fs.Close
// C#
string data = "This must be stored in a file.";
// Create 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();
If you run the application and open the SerializedString.Data file in Notepad, you'll
see the contents of the string you stored surrounded by binary information (which
appears as garbage in Notepad), as shown in Figure 5-1. The .NET Framework stored
the string as ASCII text and then added a few more binary bytes before and after the
text to describe the data for the deserializer
If you just needed to store a single string in a file, you wouldn't need to use serialization—
you could simply write the string directly to a text file. Serialization becomes
useful when storing more complex information, such as the current date and time. As
the following code sample demonstrates, serializing complex objects is as simple as
serializing a string:
' VB
' Create file to save the data to
Dim fs As FileStream = New FileStream("SerializedDate.Data", _
FileMode.Create)
' Create a BinaryFormatter object to perform the serialization
Dim bf As BinaryFormatter = New BinaryFormatter
' Use the BinaryFormatter object to serialize the data to the file
bf.Serialize(fs, System.DateTime.Now)
' Close the file
fs.Close()
// C#
// Create file to save the data to
FileStream fs = new FileStream("SerializedDate.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, System.DateTime.Now);
// Close the file
fs.Close();
How to Deserialize an Object
Deserializing an object allows you to create a new object based on stored data. Essentially,deserializing restores a saved object. At a high level, the steps for deserializing an
object are as follows:
1. Create a stream object to read the serialized output.
2. Create a BinaryFormatter object.
3. Create a new object to store the deserialized data.
4. Call the BinaryFormatter.Deserialize method to deserialize the object, and cast it
to the correct type.
At the code level, the steps for deserializing an object are easy to implement. The following
console application—which requires the System.IO, System.Runtime.Serialization,
and System.Runtime.Serialization.Formatters.Binary namespaces—demonstrates how to
read and display the serialized string data saved in an earlier example:
' VB
' Open file to read the data from
Dim fs As FileStream = New FileStream("SerializedString.Data", _
FileMode.Open)
' Create a BinaryFormatter object to perform the deserialization
Dim bf As BinaryFormatter = New BinaryFormatter
' Create the object to store the deserialized data
Dim data As String = ""
' Use the BinaryFormatter object to deserialize the data from the file
data = CType(bf.Deserialize(fs),String)
' Close the file
fs.Close
' Display the deserialized string
Console.WriteLine(data)
// C#
// Open file to read the data from
FileStream fs = new FileStream("SerializedString.Data", FileMode.Open);
// Create a BinaryFormatter object to perform the deserialization
BinaryFormatter bf = new BinaryFormatter();
// Create the object to 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);
Deserializing a more complex object, such as DateTime, works exactly the same. The
following code sample displays the day of the week and the time stored by a previous
code sample:
' VB
' Open file to read the data from
Dim fs As FileStream = New FileStream("SerializedDate.Data", FileMode.Open)
' Create a BinaryFormatter object to perform the deserialization
Dim bf As BinaryFormatter = New BinaryFormatter
' Create the object to store the deserialized data
Dim previousTime As DateTime = New DateTime
' Use the BinaryFormatter object to deserialize the data from the file
previousTime = CType(bf.Deserialize(fs),DateTime)
' Close the file
fs.Close
' Display the deserialized time
Console.WriteLine(("Day: " _
+ (previousTime.DayOfWeek + (", Time: " _
+ previousTime.TimeOfDay.ToString))))
// C#
// Open file to read the data from
FileStream fs = new FileStream("SerializedDate.Data", FileMode.Open);
// Create a BinaryFormatter object to perform the deserialization
BinaryFormatter bf = new BinaryFormatter();
// Create the object to store the deserialized data
DateTime previousTime = new DateTime();
// Use the BinaryFormatter object to deserialize the data from the file
previousTime = (DateTime) bf.Deserialize(fs);
// Close the file
fs.Close();
// Display the deserialized time
Console.WriteLine("Day: " + previousTime.DayOfWeek + ", _
Time: " + previousTime.TimeOfDay.ToString());
As these code samples demonstrate, storing and retrieving objects requires only a few
lines of code, no matter how complex the object is.
How to Create Classes That Can Be Serialized
You can serialize and deserialize custom classes by adding the Serializable attribute to
the class. This is important to do so that you, or other developers using your class, can
easily store or transfer instances of the class. Even if you do not immediately need serialization,
it is good practice to enable it for future use.
If you are satisfied with the default handling of the serialization, no other code besides
the Serializable attribute is necessary. When your class is serialized, the runtime serializes
all members, including private members.
You can also control serialization of your classes to improve the efficiency of your class
or to meet custom requirements. The sections that follow discuss how to customize
how your class behaves during serialization.
How to Disable Serialization of Specific Members
Some members of your class, such as temporary or calculated values, might not need
to be stored. For example, consider the following class, ShoppingCartItem:
' VB
<Serializable()> Class ShoppingCartItem
Public productId As Integer
Public price As Decimal
Public quantity As Integer
Public total As Decimal
Public Sub New(ByVal _productID As Integer, ByVal _price As Decimal, _
ByVal _quantity As Integer)
MyBase.New
productId = _productID
price = _price
quantity = _quantity
total = (price * quantity)
End Sub
End Class
// C#
[Serializable]
class ShoppingCartItem
{
public int productId;
public decimal price;
public int quantity;
public decimal total;
public ShoppingCartItem(int _productID, decimal _price, int _quantity)
{
productId = _productID;
price = _price;
quantity = _quantity;
total = price * quantity;
}
}
The ShoppingCartItem includes three members that must be provided by the application
when the object is created. The fourth member, total, is dynamically calculated by
multiplying the price and quantity. If this class were serialized as-is, the total would be
stored with the serialized object, wasting a small amount of storage. To reduce the size
of the serialized object (and thus reduce storage requirements when writing the serialized
object to a disk, and bandwidth requirements when transmitting the serialized
object across the network), add the NonSerialized attribute to the total member:
' VB
<NonSerialized()> Public total As Decimal
// C#
[NonSerialized] public decimal total;
Now, when the object is serialized, the total member will be omitted. Similarly, the
total member will not be initialized when the object is deserialized. However, the
value for total must still be calculated before the deserialized object is used.
To enable your class to automatically initialize a nonserialized member, use the
IDeserializationCallback interface, and then implement IDeserializationCallback
.OnDeserialization. Each time your class is deserialized, the runtime will call the
IDeserializationCallback.OnDeserialization method after deserialization is complete.
The following example shows the ShoppingCartItem class modified to not serialize the
total value, and to automatically calculate the value upon deserialization:
' VB
<Serializable()> Class ShoppingCartItem
Implements IDeserializationCallback
Public productId As Integer
Public price As Decimal
Public quantity As Integer
<NonSerialized()> Public total As Decimal
Public Sub New(ByVal _productID As Integer, ByVal _price As Decimal, _
ByVal quantity As Integer)
MyBase.New
productId = _productID
price = _price
quantity = _quantity
total = (price * quantity)
End Sub
Sub IDeserializationCallback_OnDeserialization(ByVal sender As Object)
Implements IDeserializationCallback.OnDeserialization
' After deserialization, calculate the total
total = (price * quantity)
End Sub
End Class
// C#
[Serializable]
class ShoppingCartItem : IDeserializationCallback {
public int productId;
public decimal price;
public int quantity;
[NonSerialized] public decimal total;
public ShoppingCartItem(int _productID, decimal _price, int _quantity)
{
productId = _productID;
price = _price;
quantity = _quantity;
total = price * quantity;
}
void IDeserializationCallback.OnDeserialization(Object sender)
{
// After deserialization, calculate the total
total = price * quantity;
}
}
With OnDeserialization implemented, the total member is now properly defined and
available to applications after the class is deserialized.
How to Provide Version Compatibility
You might have version compatibility issues if you ever attempt to deserialize an
object that has been serialized by an earlier version of your application. Specifically, if
you add a member to a custom class and attempt to deserialize an object that lacks
that member, the runtime will throw an exception. In other words, if you add a member
to a class in version 3.1 of your application, it will not be able to deserialize an
object created by version 3.0 of your application.
To overcome this limitation, you have two choices:
■ Implement custom serialization, as described in Lesson 3, that is capable of
importing earlier serialized objects.
■ Apply the OptionalField attribute to newly added members that might cause
version compatibility problems.
The OptionalField attribute does not affect the serialization process. During deserialization,
if the member was not serialized, the runtime will leave the member's value as
null rather than throwing an exception. The following example shows how to use the
OptionalField attribute:
' VB
<Serializable()> Class ShoppingCartItem
Implements IDeserializationCallback
Public productId As Integer
Public price As Decimal
Public quantity As Integer
<NonSerialized()> Public total As Decimal
<OptionalField()> Public taxable As Boolean
// C#
[Serializable]
class ShoppingCartItem : IDeserializationCallback
{
public int productId;
public decimal price;
public int quantity;
[NonSerialized] public decimal total;
[OptionalField] public bool taxable;
If you need to initialize optional members, either implement the IDeserialization-
Callback interface as described in the "How to Disable Serialization of Specific Members"
section earlier in this lesson, or respond to serialization events, as described in
The .NET Framework 2.0 is capable of deserializing objects that have unused members, so you can
still have the ability to deserialize an object if it has a member that has been removed since serialization.
This behavior is different from .NET Framework 1.0 and 1.1, which threw an exception if
additional information was found in the serialized object.
Best Practices for Version Compatibility
To ensure proper versioning behavior, follow these rules when modifying a custom
class from version to version:
■ Never remove a serialized field.
■ Never apply the NonSerializedAttribute attribute to a field if the attribute was not
applied to the field in a previous version.
■ Never change the name or type of a serialized field.
■ When adding a new serialized field, apply the OptionalFieldAttribute attribute.
■ When removing a NonSerializedAttribute attribute from a field that was not serializable
in a previous version, apply the OptionalFieldAttribute attribute.
■ For all optional fields, set meaningful defaults using the serialization callbacks
unless 0 or null as defaults are acceptable.
Choosing a Serialization Format
The .NET Framework includes two methods for formatting serialized data in the System
.Runtime.Serialization namespace, both of which implement the IRemotingFormatter
interface:
■
BinaryFormatter Located in the System.Runtime.Serialization.Formatters.Binary
namespace, this formatter is the most efficient way to serialize objects that will
be read by only .NET Framework–based applications.
■
SoapFormatter Located in the System.Runtime.Serialization.Formatters.Soap
namespace, this XML-based formatter is the most reliable way to serialize objects
that will be transmitted across a network or read by non–.NET Framework applications.
SoapFormatter is more likely to successfully traverse firewalls than
BinaryFormatter.
In summary, you should choose BinaryFormatter only when you know that all clients
opening the serialized data will be .NET Framework applications. Therefore, if you are
writing objects to the disk to be read later by your application, BinaryFormatter is perfect.
Use SoapFormatter when other applications might read your serialized data and
when sending data across a network. SoapFormatter also works reliably in situations
where you could choose BinaryFormatter, but the serialized object can consume three
to four times more space.
While SoapFormatter is XML-based, it is primarily intended to be used by SOAP Web
services. If your goal is to store objects in an open, standards-based document that
might be consumed by applications running on other platforms, the most flexible
way to perform serialization is to choose XML serialization. Lesson 2 in this chapter
discusses XML serialization at length.
How to Use SoapFormatter
To use SoapFormatter, add a reference to the System.Runtime.Serialization.Formatters
.Soap.dll assembly to your project. (Unlike BinaryFormatter, it is not included by
default.) Then write code exactly as you would to use BinaryFormatter, but substitute
the SoapFormatter class for the BinaryFormatter class.
While writing code for BinaryFormatter and SoapFormatter is very similar, the serialized
data is very different. The following example is a three-member object serialized
with SoapFormatter that has been slightly edited for readability:
<SOAP-ENV:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<SOAP-ENV:Body>
<a1:ShoppingCartItem id="ref-1">
<productId>100</productId>
<price>10.25</price>
<quantity>2</quantity>
</a1:ShoppingCartItem>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
How to Control SOAP Serialization
Binary serialization is intended for use only by .NET Framework–based applications.
Therefore, there is rarely a need to modify the standard formatting. However, SOAP
serialization is intended to be read by a variety of platforms. Additionally, you might
need to serialize an object to meet specific requirements, such as predefined SOAP
attribute and element names.
Guidelines for Serialization
Keep the following guidelines in mind when using serialization:
■ When in doubt, mark a class as Serializable. Even if you do not need to serialize
it now, you might need serialization later. Or another developer might need to
serialize a derived class.
■ Mark calculated or temporary members as NonSerialized. For example, if you
track the current thread ID in a member variable, the thread ID is likely to not be
valid upon deserialization. Therefore, you should not store it.
■ Use SoapFormatter when you require portability. Use Binary
Formatter for greatest
efficiency.