Custom Serialization

You can customize the serialization process by implementing the ISerializable interface on an object. This is particularly useful in cases where the value of a member variable is invalid after deserialization, but you need to provide the variable with a value in order to reconstruct the full state of the object. In addition, you should not use default serialization on a class that is marked with the Serializable attribute and has declarative or imperative security at the class level or on its constructors. Instead, these classes should always implement the ISerializable interface.

Implementing ISerializable involves implementing the GetObjectData method and a special constructor that is used when the object is deserialized. The sample code below shows how to implement ISerializable on the MyObject class from a previous section.

[Serializable]
public class MyObject : ISerializable 
{
  public int n1;
  public int n2;
  public String str;

  public MyObject()
  {
  }

  protected MyObject(SerializationInfo info, StreamingContext context)
  {
    n1 = info.GetInt32("i");
    n2 = info.GetInt32("j");
    str = info.GetString("k");
  }
[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter 
=true)]
  public virtual void GetObjectData(SerializationInfo info, StreamingContext context)
  {
    info.AddValue("i", n1);
    info.AddValue("j", n2);
    info.AddValue("k", str);
  }
}

When GetObjectData is called during serialization, you are responsible for populating the SerializationInfo provided with the method call. Simply add the variables to be serialized as name/value pairs. Any text can be used as the name. You have the freedom to decide which member variables are added to the SerializationInfo, provided that sufficient data is serialized to restore the object during deserialization. Derived classes should call the GetObjectData method on the base object if the latter implements ISerializable.

Note that serialization can allow other code to see or modify object instance data that would otherwise be inaccessible. Therefore, code performing serialization requires the SecurityPermission with the SerializationFormatter flag specified. Under default policy, this permission is not given to Internet-downloaded or intranet code; only code on the local computer is granted this permission. The GetObjectData method should be explicitly protected either by demanding the SecurityPermission with the SerializationFormatter flag specified or by demanding other permissions that specifically help protect private data.

If a private field stores sensitive information, you should demand the appropriate permissions on GetObjectData to protect the data. Remember that code that has been granted SecurityPermission with the SerializationFormatter flag specified can view and modify the data stored in private fields. A malicious caller granted this SecurityPermission can view data such as hidden directory locations or granted permissions and use the data to exploit a security vulnerability on the computer. For a complete list of the security permission flags you can specify, see the SecurityPermissionFlag Enumeration.

It is important to stress that when ISerializable is added to a class you must implement both GetObjectData and the special constructor. The compiler will warn you if GetObjectData is missing. However, because it is impossible to enforce the implementation of a constructor, no warning will be provided if the constructor is absent, and an exception will be thrown when an attempt is made to deserialize a class without the constructor.

The current design was favored above a SetObjectData method to get around potential security and versioning problems. For example, a SetObjectData method must be public if it is defined as part of an interface; thus users must write code to defend against having the SetObjectData method called multiple times. Otherwise, a malicious application that calls the SetObjectData method on an object in the process of executing an operation can cause potential problems.

During deserialization, SerializationInfo is passed to the class using the constructor provided for this purpose. Any visibility constraints placed on the constructor are ignored when the object is deserialized; so you can mark the class as public, protected, internal, or private. However, it is best practice to make the constructor protected unless the class is sealed, in which case the constructor should be marked private. The constructor should also perform thorough input validation. To avoid misuse by malicious code, the constructor should enforce the same security checks and permissions required to obtain an instance of the class using any other constructor. If you do not follow this recommendation, malicious code can preserialize an object, obtain control with the SecurityPermission with the SerializationFormatter flag specified and deserialize the object on a client computer bypassing any security that would have been applied during standard instance construction using a public constructor.

To restore the state of the object, simply retrieve the values of the variables from SerializationInfo using the names used during serialization. If the base class implements ISerializable, the base constructor should be called to allow the base object to restore its variables.

When you derive a new class from one that implements ISerializable, the derived class must implement both the constructor as well as the GetObjectData method if it has variables that need to be serialized. The code example below shows how this is done using the MyObject class shown previously.

[Serializable]
public class ObjectTwo : MyObject
{
  public int num;

  public ObjectTwo() : base()
  {
  }

  protected ObjectTwo(SerializationInfo si, StreamingContext context) : base(si,context)
  {
    num = si.GetInt32("num");
  }
[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter 
=true)]
  public override void GetObjectData(SerializationInfo si, StreamingContext context)
  {
    base.GetObjectData(si,context);
    si.AddValue("num", num);
  }
}

Do not forget to call the base class in the deserialization constructor; if this is not done, the constructor on the base class will never be called, and the object will not be fully constructed after deserialization.

Objects are reconstructed from the inside out; and calling methods during deserialization can have undesirable side effects, because the methods called might refer to object references that have not been deserialized by the time the call is made. If the class being deserialized implements the IDeserilizationCallback, the OnDeserialization method is automatically called when the entire object graph has been deserialized. At this point, all the child objects referenced have been fully restored. A hash table is a typical example of a class that is difficult to deserialize without using the event listener described above. It is easy to retrieve the key/value pairs during deserialization, but adding these objects back to the hash table can cause problems, because there is no guarantee that classes that derived from the hash table have been deserialized. Calling methods on a hash table at this stage is therefore not advisable.

See Also

Binary Serialization | Accessing Objects in Other Application Domains Using .NET Remoting | XML and SOAP Serialization | Security and Serialization | Code Access Security