Section 9.5. Custom Serialization


9.5. Custom Serialization

Sometimes, the default automatic serialization provided by the Serializable attribute is insufficient. Perhaps the object state contains sensitive information, such as a credit card number. In that case, you may want to encrypt the state instead of using a plain by-value serialization. Some other examples that might require custom serialization solutions are if you have some internal knowledge of how to serialize the event subscribers, if which members get serialized depends on the state of the object, or if you want to perform additional proprietary initialization steps during deserialization.

9.5.1. The ISerializable Interface

.NET provides an easy-to-use mechanism for custom serialization that extends the serialization infrastructure. To provide custom serialization and deserialization behavior, you need to implement the ISerializable interface, defined in the System.Runtime.Serialization namespace:

     public interface ISerializable     {        void GetObjectData(SerializationInfo info,StreamingContext context);     }

Every time a client serializes an object, .NET reflects the object's metadata to see whether the serializable object implements ISerializable. If it does, .NET calls GetObjectData( ) to retrieve the object's state. At this point, it's up to the object to provide the state information in whichever way it wants. You will see an example of implementing ISerializable shortly.

To support the matching custom deserialization, the object must provide a special parameterized custom deserialization constructor with this signature:

     <Class Name>(SerializationInfo info,StreamingContext context);

.NET calls this constructor during deserialization. The constructor can (and should) be defined as protected to prevent normal clients from calling it. .NET uses reflection to invoke the custom deserialization constructor and thus isn't impeded by the constructor being protected. If the class implements the ISerializable interface but doesn't provide a custom deserialization constructor, the compiler doesn't warn you. Instead, during deserialization, .NET throws an exception of type SerializationException.

In .NET, interfaces aren't allowed to have any implementation details and therefore can't define constructors. The design decision the architects of .NET serialization took was to force a runtime check for the custom deserialization constructor, instead of a compile-time check. However, I believe a better design decision would have been to provide a SetObjectData( ) method on ISerializable and, during deserialization, to use reflection to set the fields of a new object.


Note that the client isn't required to treat an object that implements ISerializable any differently from an object that uses automatic serialization. Custom serialization is purely a component-side facility. The client uses the formatters and streams, as with automatic serialization.

9.5.2. Implementing ISerializable

Both GetObjectData( ) and the custom deserialization constructor accept a parameter of type SerializationInfo called info. SerializationInfo provides methods for getting or adding field values. Each field is identified by a string. Because SerializationInfo was defined before generics were available, SerializationInfo has type-safe methods for most of the CLR-defined types, such as int and string. For each such type, SerializationInfo provides two methods in this form:

     void AddValue(string name, <Type> value);     <Type> Get<Type>(string name);

Here's an example:

     public sealed class SerializationInfo     {        public void AddValue(string name, short value);        public void AddValue(string name, int value);        //Other AddValue(  ) methods        public int    GetInt32(string name);        public string GetString(string name);        //Other Get<Type>(  ) methods        //Other methods and properties     }

For all other field types, SerializationInfo provides methods like these to add or get an object:

     public void AddValue(string name, object value);     public object GetValue(string name, Type type);

The second parameter that both GetObjectData( ) and the custom deserialization constructor accept is the context parameter, of type StreamingContext. Example 9-15 demonstrates both ISerializable and the custom deserialization constructor. The way the class in Example 9-15 implements custom serialization has no advantage over automatic serialization; it simply shows how to provide custom serialization. It's up to you to provide the required custom steps.

Example 9-15. Implementing ISerializable
 [Serializable] public class MyClass : ISerializable {    int m_Number;    string m_SomeString;    public virtual void GetObjectData(SerializationInfo info,                                      StreamingContext context)    {       info.AddValue("m_Number",m_Number);       info.AddValue("m_SomeString",m_SomeString);    }    protected MyClass(SerializationInfo info,StreamingContext context)    {       m_Number = info.GetInt32("m_Number");       m_SomeString = info.GetString("m_SomeString");    }    public MyClass(  )    {} }

When you implement ISerializable, the type must still be decorated with the Serializable attribute. Otherwise, .NET considers the type non-serializable and ignores ISerializable.


If the serialized class is a generic class, you need to use the AddValue( ) and GetValue( ) methods that accept an object while specifying the type, as shown in Example 9-16.

Example 9-16. Custom serialization of a generic class
 [Serializable] public class MyClass<T> : ISerializable {    T m_T;    public MyClass(  )    {}    public void GetObjectData(SerializationInfo info,StreamingContext ctx)    {       info.AddValue("m_T",m_T,typeof(T));    }    protected MyClass(SerializationInfo info,StreamingContext context)    {       m_T = (T)info.GetValue("m_T",typeof(T));    } }

9.5.2.1 GenericSerializationInfo

The introduction of generics in .NET 2.0 allows you to improve on the available SerializationInfo and shield the client code from the type retrieval and explicit casting.

Example 9-17 presents the GenericSerializationInfo utility class, which exposes generic AddValue( ) and GetValue( ) methods. GenericSerializationInfo encapsulates a regular SerializationInfo object, passed to it as a construction parameter.

Example 9-17. The GenericSerializationInfo utility class
 public class GenericSerializationInfo {    SerializationInfo m_SerializationInfo;    public GenericSerializationInfo(SerializationInfo info)    {       m_SerializationInfo = info;    }    public void AddValue<T>(string name,T value)    {       m_SerializationInfo.AddValue(name,value,value.GetType(  ));    }    public T GetValue<T>(string name)    {       object obj = m_SerializationInfo.GetValue(name,typeof(T));       return (T)obj;    } }

Example 9-18 shows the same custom serialization code as Example 9-16, except it uses GenericSerializationInfo. Note the use of type inference in the call to AddValue( ).

Example 9-18. Using GenericSerializationInfo
 [Serializable] public class MyClass<T> : ISerializable {    T m_T;    public MyClass(  )    {}    public void GetObjectData(SerializationInfo info,StreamingContext ctx)    {       GenericSerializationInfo genericInfo = new GenericSerializationInfo(info);       genericInfo.AddValue("m_T",m_T); //Using type inference    }    protected MyClass(SerializationInfo info,StreamingContext context)    {       GenericSerializationInfo genericInfo = new GenericSerializationInfo(info);       m_T = genericInfo.GetValue<T>("m_T");    } }

GenericSerializationInfo is a cleaner way of using custom serialization, even on non-generic-type class members. For example, using GenericSerializationInfo in the class MyClass from Example 9-15, you could write:

     genericInfo.AddValue("m_SomeString",m_SomeString);

and:

     m_SomeString = genericInfo.GetValue<string>("m_SomeString");

9.5.2.2 Custom serialization and IDeserializationCallback

Implementing IDeserializationCallback allows a type to be notified after deserialization takes place and to perform additional, custom deserialization steps. You can implement both IDeserializationCallback and ISerializable, but when you implement ISerializable there is really no need for IDeserializationCallback, because you can place the custom steps in the custom deserialization constructor.

9.5.2.3 Custom serialization and serialization events

It is technically possible to use the serializing and deserializing events for custom serialization and avoid implementing ISerializable; however, this will come at a high price in programming model and code readability and maintainability. You will need to have a separate set of member variables, all marked as non-serializable. The type itself will use those members, and the serialization events will perform the custom steps (such as encryption or decryption) on the non-serializable members and then copy them to the set of serializable member variables, whose sole purpose will be to be serialized and deserialized. Needless to say, such an approach is cumbersome and error-prone. It is better to stick with the dedicated ISerializable standard mechanism.

9.5.3. Constraining Serialization

A generic class that has generic type parameters as members can still be marked for serialization:

     [Serializable]     public class MyClass<T>     {        T m_T;     }

However, in such cases the generic class is serializable only if the generic type parameter specified is serializable. Consider this code:

     public class SomeClass     {}     MyClass<SomeClass> obj;

obj is not serializable, because the type parameter SomeClass is not serializable. Consequently, MyClass<T> may or may not be serializable, depending on the generic type parameter used. This may result in a run-time loss of data or system corruption, because the client application may not be able to persist the state of the object.

To make things even worse, the type parameter itself might be a generic type, whose own type parameters might not be serializable, and so on:

     [Serializable]     public class MyClass<T>     {       T m_T;     }     [Serializable]     public class SomeClass<T>     {}     public class SomeOtherClass     {}     //Will not work:     MyClass<SomeClass<SomeOtherClass>> obj;

Presently, .NET does not provide a mechanism for constraining a generic type parameter to be serializable. However, there are three workarounds to guarantee deterministic serialization behavior. The first is to mark all member variables of the generic type parameter as non-serializable:

     [Serializable]     public class MyClass<T>     {        [NonSerialized]        T m_T;     }

Of course, this may seriously damage the generic class MyClass<T>'s ability to function properly, in the case where you do need to serialize the state of members of a generic type. The second workaround is to place a constraint on the generic type parameter to implement ISerializable:

     [Serializable]     public class MyClass<T> where T : ISerializable     {        T m_T;     }

This ensures that all instances of MyClass<T>, regardless of the type parameter, are serializable, but it places the burden of implementing custom serialization on all generic type parameters used. The third and best solution is to perform a single runtime check before any use of the type MyClass<T> and abort the use immediately, before any damage can take place. The trick is to place the runtime verification in the C# static constructor. Example 9-19 demonstrates this technique.

Example 9-19. Runtime enforcement of generic type parameter serialization
 [Serializable] class MyClass<T> {    T m_T;    static MyClass()    {       SerializationUtil.ConstrainType(typeof(T));    } } public static class SerializationUtil {    public static void ConstrainType(Type type)    {       bool serializable = type.IsSerializable;       if(serializable == false)       {          string message = "The type " + type + " is not serializable";          throw new SerializationException(message);       }       bool genericType = type.IsGenericType;       if(genericType)       {          Type[] typeArguments = type.GetGenericArguments();          Debug.Assert(typeArguments.Length >= 1);          Array.ForEach(typeArguments,ConstrainType);       }    }    //Rest of SerializationUtil }

The C# static constructor is invoked exactly once per type per app domain, upon the first attempt to instantiate an object of that type. In Example 9-19, the static constructor calls the static helper method ConstrainType() of SerializationUtil. ConstrainType() then verifies that the specified type is serializable by checking the IsSerializable property of the type. If the type is not serializable, ConstrainType() throws a SerializationException, thus aborting any attempt to use the type.

To deal with the issue of having generic types as type parameters, ConstrainType checks if the type in question is a generic type. If so, it obtains an array of all its type parameters, and recursively calls itself verifying that each type parameter down the declaration chain is serializable.

Performing the constraint verification in the static constructor is a technique applicable to any constraint that you cannot enforce at compile time yet have some programmatic way of determining and enforcing at runtime.




Programming. NET Components
Programming .NET Components, 2nd Edition
ISBN: 0596102070
EAN: 2147483647
Year: 2003
Pages: 145
Authors: Juval Lowy

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