Persistence is a core feature of a type. It's one of those basic elements that no one notices until you neglect to support it. If your type does not support serialization properly, you create more work for all developers who intend to use your types as a member or base class. When your type does not support serialization, they must work around it, adding their own implementation of a standard feature. It's unlikely that clients could properly implement serialization for your types without access to private details in your types. If you don't supply serialization, it's difficult or impossible for users of your class to add it.
Instead, prefer adding serialization to your types when practical. It should be practical for all types that do not represent UI widgets, windows, or forms. The extra perceived work is no excuse. .NET Serialization support is so simple that you don't have any reasonable excuse not to support it. In many cases, adding the Serializable attribute is enough:
[Serializable]
public class MyType
{
private string _label;
private int _value;
}
Adding the Serializable attribute works because all the members of this type are serializable: string and int both support NET serialization. The reason it's important for you to support serialization wherever possible becomes obvious when you add another field of a custom type:
[Serializable]
public class MyType
{
private string _label;
private int _value;
private OtherClass _object;
}
The Serializable attribute works here only if the OtherClass type supports .NET serialization. If OtherClass is not serializable, you get a runtime error and you have to write your own code to serialize MyType and the OtherClass object inside it. That's just not possible without extensive knowledge of the internals defined in OtherClass.
.NET serialization saves all member variables in your object to the output stream. In addition, the .NET serialization code supports arbitrary object graphs: Even if you have circular references in your objects, the serialize and deserialize methods will save and restore each actual object only once. The .NET Serialization Framework also will recreate the web of references when the web of objects is deserialized. Any web of related objects that you have created is restored correctly when the object graph is deserialized. A last important note is that the Serializable attribute supports both binary and SOAP serialization. All the techniques in this item will support both serialization formats. But remember that this works only if all the types in an object graph support serialization. That's why it's important to support serialization in all your types. As soon as you leave out one class, you create a hole in the object graph that makes it harder for anyone using your types to support serialization easily. Before long, everyone is writing their own serialization code again.
Adding the Serializable attribute is the simplest technique to support serializable objects. But the simplest solution is not always the right solution. Sometimes, you do not want to serialize all the members of an object: Some members might exist only to cache the result of a lengthy operation. Other members might hold on to runtime resources that are needed only for in-memory operations. You can manage these possibilities using attributes as well. Attach the [NonSerialized] attribute to any of the data members that should not be saved as part of the object state. This marks them as nonserializable attributes:
[Serializable]
public class MyType
{
private string _label;
[NonSerialized]
private int _cachedValue;
private OtherClass _object;
}
Nonserialized members add a little more work for you, you, the class designer. The serialization APIs do not initialize nonserialized members for you during the deserialization process. None of your types' constructors is called, so the member initializers are not executed, either. When you use the serializable attributes, nonserialized members get the default system-initialized value: 0 or null. When the default 0 initialization is not right, you need to implement the IDeserializationCallback interface to initialize these nonserializable members. IDeserializationCallback contains one method: OnDeserialization. The framework calls this method after the entire object graph has been deserialized. You use this method to initialize any nonserialized members in your object. Because the entire object graph has been read, you know that any function you might want to call on your object or any of its serialized members is safe. Unfortunately, it's not fool-proof. After the entire object graph has been read, the framework calls OnDeserialization on every object in the graph that supports the IDeserializationCallback interface. Any other objects in the object graph can call your object's public members when processing OnDeserialization. If they go first, your object's nonserialized members are null, or 0. Order is not guaranteed, so you must ensure that all your public methods handle the case in which nonserialized members have not been initialized.
So far, you've learned about why you should add serialization to all your types: Nonserializable types cause more work when used in types that should be serialized. You've learned about the simplest serialization methods using attributes, including how to initialize nonserialized members.
Serialized data has a way of living on between versions of your program. Adding serialization to your types means that one day you will need to read an older version. The code generated by the Serializable attribute throws exceptions when it finds fields that have been added or removed from the object graph. When you find yourself ready to support multiple versions and you need more control over the serialization process, use the ISerializable interface. This interface defines the hooks for you to customize the serialization of your types. The methods and storage that the ISerializable interface uses are consistent with the methods and storage that the default serialization methods use. That means you can use the serialization attributes when you create a class. If it ever becomes necessary to provide your own extensions, you then add support for the ISerializable interface.
As an example, consider how you would support MyType, version 2, when you add another field to your type. Simply adding a new field produces a new format that is incompatible with the previously stored versions on disk:
[Serializable]
public class MyType
{
private string _label;
[NonSerialized]
private int _value;
private OtherClass _object;
// Added in version 2
// The runtime throws Exceptions
// with it finds this field missing in version 1.0
// files.
private int _value2;
}
You add support for ISerializable to address this behavior. The ISerializable interface defines one method, but you have to implement two. ISerializable defines the GetObjectData() method that is used to write data to a stream. In addition, you must provide a serialization constructor to initialize the object from the stream:
private MyType( SerializationInfo info,
StreamingContext cntxt );
The serialization constructor in thefollowing class shows how to read a previous version of the type and read the current version consistently with the default implementation generated by adding the Serializable attribute:
using System.Runtime.Serialization;
using System.Security.Permissions;
[Serializable]
public sealed class MyType : ISerializable
{
private string _label;
[NonSerialized]
private int _value;
private OtherClass _object;
private const int DEFAULT_VALUE = 5;
private int _value2;
// public constructors elided.
// Private constructor used only by the Serialization
framework.
private MyType( SerializationInfo info,
StreamingContext cntxt )
{
_label = info.GetString( "_label" );
_object = ( OtherClass )info.GetValue( "_object", typeof
( OtherClass ));
try {
_value2 = info.GetInt32( "_value2" );
} catch ( SerializationException e )
{
// Found version 1.
_value2 = DEFAULT_VALUE;
}
}
[SecurityPermissionAttribute(SecurityAction.Demand,
SerializationFormatter =true)]
void ISerializable.GetObjectData (SerializationInfo inf,
StreamingContext cxt)
{
inf.AddValue( "_label", _label );
inf.AddValue( "_object", _object );
inf.AddValue( "_value2", _value2 );
}
}
The serialization stream stores each item as a key/value pair. The code generated from the attributes uses the variable name as the key for each value. When you add the ISerializable interface, you must match the key name and the order of the variables. The order is the order declared in the class. (By the way, this fact means that rearranging the order of variables in a class or renaming variables breaks the compatibility with files already created.)
Also, I have demanded the SerializationFormatter security permission. GetObjectData could be a security hole into your class if it is not properly protected. Malicious code could create a StreamingContext, get the values from an object using GetObjectData, serialize modified versions to another SerializationInfo, and reconstitute a modified object. It would allow a malicious developer to access the internal state of your object, modify it in the stream, and send the changes back to you. Demanding the SerializationFormatter permission seals this potential hole. It ensures that only properly trusted code can access this routine to get at the internal state of the object .
But there's a downside to implementing the ISerializable interface. You can see that I made MyType sealed earlier. That forces it to be a leaf class. Implementing the ISerializable interface in a base class complicates serialization for all derived classes. Implementing ISerializable means that every derived class must create the protected constructor for deserialization. In addition, to support nonsealed classes, you need to create hooks in the GetObjectData method for derived classes to add their own data to the stream. The compiler does not catch either of these errors. The lack of a proper constructor causes the runtime to throw an exception when reading a derived object from a stream. The lack of a hook for GetObjectData() means that the data from the derived portion of the object never gets saved to the file. No errors are thrown. I'd like the recommendation to be "implement Serializable in leaf classes."
I did not say that because that won't work. Your base classes must be serializable for the derived classes to be serializable. To modify MyType so that it can be a serializable base class, you change the serializable constructor to protected and create a virtual method that derived classes can override to store their data:
using System.Runtime.Serialization;
using System.Security.Permissions;
[Serializable]
public class MyType : ISerializable
{
private string _label;
[NonSerialized]
private int _value;
private OtherClass _object;
private const int DEFAULT_VALUE = 5;
private int _value2;
// public constructors elided.
// Protected constructor used only by the Serialization
framework.
protected MyType( SerializationInfo info,
StreamingContext cntxt )
{
_label = info.GetString( "_label" );
_object = ( OtherClass )info.GetValue( "_object", typeof
( OtherClass ));
try {
_value2 = info.GetInt32( "_value2" );
} catch ( SerializationException e )
{
// Found version 1.
_value2 = DEFAULT_VALUE;
}
}
[ SecurityPermissionAttribute( SecurityAction.Demand,
SerializationFormatter =true ) ]
void ISerializable.GetObjectData(
SerializationInfo inf,
StreamingContext cxt )
{
inf.AddValue( "_label", _label );
inf.AddValue( "_object", _object );
inf.AddValue( "_value2", _value2 );
WriteObjectData( inf, cxt );
}
// Overridden in derived classes to write
// derived class data:
protected virtual void
WriteObjectData(
SerializationInfo inf,
StreamingContext cxt )
{
}
}
A derived class would provide its own serialization constructor and override the WriteObjectData method:
public class DerivedType : MyType
{
private int _DerivedVal;
private DerivedType ( SerializationInfo info,
StreamingContext cntxt ) :
base( info, cntxt )
{
_DerivedVal = info.GetInt32( "_DerivedVal" );
}
protected override void WriteObjectData(
SerializationInfo inf,
StreamingContext cxt )
{
inf.AddValue( "_DerivedVal", _DerivedVal );
}
}
The order of writing and retrieving values from the serialization stream must be consistent. I've chosen to read and write the base class values first because I believe it is simpler. If your read and write code does not serialize the entire hierarchy in the exact same order, your serialization code won't work.
The .NET Framework provides a simple, standard algorithm for serializing your objects. If your type should be persisted, you should follow the standard implementation. If you don't support serialization in your types, other classes that use your type can't support serialization, either. Make it as easy as possible for clients of your class. Use the default methods when you can, and implement the ISerializable interface when the default attributes don't suffice.