As discussed earlier in this chapter, the best way to control the serialization and deserialization process is to use OnSerializing, OnSerialized, OnDeserializing, OnDeserialized, NonSerialized, Optionalfield and other attribute. However, in some rare cases, these attribute do not provide all the control you want. In addition, the formatter uses reflection internally, and the reflection is slower, which increases the time that the object is serialized and deserialized. For full control of the serialized/deserialized data, and to avoid using reflection, your type implements the ISerializable interface:
public interface ISerializable
{
void GetObjectData (SerializationInfo info, StreamingContext context);
}
This interface has only one method, that is, GetObjectData, but most of the types that implement this interface implement a special constructor, which is described in detail later.
Important: The big problem with the ISerializable interface is that once the type implements it, all derived types must implement him, and the derived type must be guaranteed to call the base class's GetObjectData method and the special constructor. In addition, once the type implements the interface, it can never be deleted, otherwise it loses compatibility with the derived type. The sealed type implements the ISerializable interface always feasible.
Important: The ISerializable interface and special constructors are intended to be used by the formatter. However, other code may call GetObjectData, which may return sensitive data. In addition, other code might construct an object and pass in the corrupted data, so it is recommended that the following attribute be applied to the GetObjectData method and special instructions
[SecurityPermissionAttribute (Securityaction.demand,serializationformatter=true]
Each object is checked when the formatter serializes an object graph. If you find that the type of an object implements the ISerializable interface, the formatter ignores all custom attribute, which constructs a new SerializationInfo object that contains a collection of values to actually serialize the object.
When constructing a SerializationInfo object, the formatter passes two parameters: type and iformatterconverter. The type parameter identifies the object to serialize. To uniquely identify a type, two pieces of information are required: the string name of the type and the identity of the Assembly (including the assembly name, version, language culture, and public key). When a SerializationInfo object is constructed, it contains the full name of the type (by querying the FullName property of the type) and storing the amount string in a private field. To get the full name of the type, query the Fulltypename property of the SerializationInfo. Similarly, the constructor gets the definition assembly of the type and stores the string in a private field. In order to obtain this assembly identity, you can query the AssemblyName property of the SerializationInfo.
After the SerializationInfo object is constructed and initialized, the formatter invokes the GetObjectData method of the type, passing it a reference to the SerializationInfo object. The GetObjectData method is responsible for determining what information is required to serialize the object and adding that information to the SerializationInfo object. GetObjectData calls one of the many overloaded versions of the AddValue method provided by the SerializationInfo type to specify the information to serialize. The addvalue is called once for each data that is added.
The following code shows how the dictionary<tkey,tvalue> type implements the ISerializable and Ideserializationcallback interfaces to control serialization and deserialization:
[Serializable] Public classDictionary<tkey, tvalue>: ISerializable, Ideserializationcallback {PrivateSerializationInfo M_siinfo;//only for reflection//special constructors for controlling deserialization[SecurityPermissionAttribute (SecurityAction.Demand, SerializationFormatter =true)] protectedDictionary (SerializationInfo info, StreamingContext context) {//save SerializationInfo for ondeserialization during deserializationM_siinfo =info; } //methods for controlling serialization[SecurityCritical] Public Virtual voidGetObjectData (SerializationInfo info, StreamingContext context) {info. AddValue ("Version", m_version); Info. AddValue ("Comparer", M_comparer,typeof(iequalitycomparer<tkey>)); Info. AddValue ("hashsize", (m_buckets = =NULL) ?0: M_buckets. Length); if(M_buckets! =NULL) {KeyValuePair<tkey, tvalue> array =NewKeyvaluepair<tkey, tvalue>(count); CopyTo (Array,0); Info. AddValue ("Keyvaluepairs", Array,typeof(Keyvaluepair<tkey, tvalue>[])); } } //methods that are called after all Key/value objects are deserialized Public Virtual voidideserializationcallback.ondeserialization (Object sender) {if(M_siinfo = =NULL)return; Int32 Num= M_siinfo.getint32 ("Version"); Int32 num2= M_siinfo.getint32 ("hashsize"); M_siinfo.getvalue ("Comparer",typeof(iequalitycomparer<tkey>)); if(Num2! =0) {m_buckets=Newint32[num2]; for(Int32 i =0; I < M_buckets. Length; i++) M_buckets[i] =-1; M_entries=NewEntry<tkey,tvalue>[num2]; M_freelist= -1; KeyValuePair<tkey, tvalue>[] Pairarray = (Keyvaluepair<tkey, tvalue>[]); M_siinfo.getvalue ("Keyvaluepairs",typeof(Keyvaluepair<tkey, tvalue>[])); ..... } }
Each AddValue method goes back to a string name and some data. Data is generally a simple value type, such as Boolen,char,byte,sbyte,int16,uint32,int64,uint64,single,double,decimal, DateTime. However, you can also pass a reference to an object to the AddValue when you call it. GetObjectData after all the serialized information has been added, it is returned to the formatter.
The formatter now gets all the values that have been added to the SerializationInfo object and serializes them into the stream. Note that we also passed another parameter to the GetObjectData method, that is, a reference to a StreamingContext object. Most getobjectdata will ignore this parameter. Talk about it later.
Once you know how to set the information required for serialization, take a look at deserialization. When the formatter reads an object from the stream, it allocates memory (Formatterservices.getuninitializedobject) for the new object. Initially, all fields of this object are set to 0 or null. The formatter then checks to see if the type implements the ISerializable interface. If this interface exists, the formatter attempts to invoke a special constructor whose parameters are exactly the same as the GetObjectData method.
If your class is a sealed class, it is strongly recommended to declare this special constructor private. This prevents any code from inadvertently invoking it, which can improve security. If it is not a sealed class, you should declare this special constructor as protect, making sure that the derived class can call it later. Note that the formatter can invoke this particular constructor, regardless of how it is declared.
The constructor gets a reference to a SerializationInfo object. In this SerializationInfo object, contains all the values that were added when the object was serialized. Special constructors can call Getboolean,getchar,getbtye,getsbyte,getuint16,getint32,getuint32,getint64,getuint64,getsingle, Any method, such as getdouble,getdecimal,getdatetime,getstring and GetValue, passes a string to it that uses the name pair used to serialize a value. The values returned by each of these methods are then used to initialize the individual fields of the new object.
When deserializing an object field, a get method should be called and matched to the type of the value passed to the AddValue method when the object is serialized. In other words, if a Int32 value is passed when the GetObjectData method calls AddValue, the GetInt32 method should be called for the same value when deserializing the object. If the type in the value re-flow does not match the type you are trying to get, the formatter attempts to transform the value in the stream into the type you specified with a Iformatterconverter object.
As mentioned earlier, when you construct a SerializationInfo object, you implement a Iformatterconverter interface object to which the type is passed. Since the formatter is responsible for constructing the SerializationInfo object, there is a type of iformatterconverter that he chooses to choose. Microsoft's Binaryformatter,soapformatter type always constructs an instance of the Formatterconverter type, and the Microsoft Formatter does not provide any way for you to choose a different iformatterconverter type.
The Formatterconverter type calls the various static methods of the System.Convert class to convert values between different core types, such as converting a Int32 to Int64. However, in order to convert a value between any other type, Formatterconverter calls the ChangeType method of convert to transform the serialized type into a iconvertible interface, calling the appropriate interface method. Therefore, to allow an object of a serializable type to be deserialized into a different type, consider having your own type implement the IConvertible interface. Note that only a GET method is called when deserializing an object, but the Formatterconverter object is used only if it finds its type and the type of the value in the stream is not a character.
Special constructors can also call GetEnumerator instead of invoking the individual get methods listed above. The method returns a System.Runtime.Serialization.SerializationInfoEnumerator object that can be used to traverse all the values contained in the SerializationInfo object. Each value of the enumeration is a System.Runtime.Serialization.SerializationEntry.
Of course, you can definitely define one of your own types, allowing him to derive from a property that implements the ISerializable GetObjectData method and a special constructor. If your type also implements ISerializable, then in your implementation of the GetObjectData method and the special constructor, you must call the method of the same name in the base class to ensure that the object can be serialized and deserialized correctly, which is important to remember, Otherwise, the object cannot be serialized and deserialized correctly.
If you do not have any extra fields in your derived class and therefore do not have special serialization and deserialization requirements, you do not have to implement ISerializable at all. Like all interface members, GetObjectData is virtual and called to serialize the object correctly. In addition, the formatter has virtualized the special constructor. In other words, during deserialization, the formatter checks the type to instantiate. If the type does not provide a special constructor, the formatter scans the base class and knows that it finds a class that implements the special constructor.
Important: Code in a special constructor generally extracts fields from the SerializationInfo object that is passed to him. After extracting a field, the object cannot be guaranteed to be fully serialized, and the code in all special constructors should not attempt to manipulate the object it extracts.
If your type must access members of an extracted object (such as a calling method), it is recommended that your type provide a method that applies the ondeserialized. or let your type implement the Ideserializationcallback.ondeserialization. When this method is called, the fields of all objects are set, however, for multiple objects, the order in which their ondeserialized or ondeserialization methods are called is not guaranteed. So, although the field may have been initialized, you still don't know if the object being applied is fully serialized.
How to define a type that implements the base class without implementing ISerializable
As mentioned earlier, the ISerializable interface is a powerful feature that allows a type to have full control over how instances of the type are serialized and deserialized. However, this feature is at a cost; now, this type is still the serialization of all fields that are responsible for its base type. If the base type also implements the ISerializable interface, it is easy to serialize a field of the base type. You only need to call the GetObjectData method of the base type.
But someday, you may want to define a type to control its serialization, but its base class does not implement the ISerializable interface, in which case the derived class must manually serialize the fields of the base class, specifically to get their values, and add these values to the SerializationInfo collection. Then, in your special constructor, you must also remove these values from the Union and set the fields of the base class in some way. This is all easy to implement if the base class's fields are public or protected fields. However, if the base class field is a private field, it is difficult or impossible to implement.
The following code demonstrates the correct implementation of the GetObjectData method of the ISerializable interface and its underlying constructor, so that the fields of the base class are serialized:
[Serializable]Internal classBase {protectedString M_name ="Jeff"; PublicBase () {}} [Serializable]Internal classDerived:base, ISerializable {PrivateDateTime m_date =DateTime.Now; PublicDerived () {} [SecurityPermission (SecurityAction.Demand, SerializationFormatter=true)] PrivateDerived (SerializationInfo info, StreamingContext context) {//get a serializable set of members for our classes and base classesType BaseType = This. GetType (). BaseType; Memberinfo[] Mi=formatterservices.getserializablemembers (baseType, context); //deserializing a base class field from an Info object for(Int32 i =0; I < mi. Length; i++) {FieldInfo fi=(FieldInfo) mi[i]; Fi. SetValue ( This, info. GetValue (Basetype.fullname +"+"+fi. Name, FI. FieldType)); } m_date= info. GetDateTime ("Date"); } [SecurityPermission (SecurityAction.Demand, SerializationFormatter=true)] Public Virtual voidGetObjectData (SerializationInfo info, StreamingContext context) {//The value that you want to serialize for this typeInfo. AddValue ("Date", m_date); Type BaseType= This. GetType (). BaseType; Memberinfo[] Mi=formatterservices.getserializablemembers (baseType, context); for(Int32 i =0; I < mi. Length; i++) {info. AddValue (Basetype.fullname+"+"+ Mi[i]. Name, ((FieldInfo) mi[i]). GetValue ( This)); } } Public Override stringToString () {returnString.Format ("Name={0},date={1}", M_name, m_date); } }
In the above code, there is a base class called base, which is identified only with serializable custom attribute. Derived from base is the derived class, which shows the application of the serializable identity, and also implements the ISerializable interface. To make the situation more interesting, two classes define a string field for the M_name. When you call SerializationInfo's AddValue method, you cannot add more than one value with the same name. In the above code, the solution to this problem is to identify each field by appending its class name prefix to the field names. For example, when the GetObjectData method calls AddValue to serialize the M_name field of base, the name of the write value is base+m_name.
Section Fifth: Control of serialized and deserialized data