Mark Wilson I am the creator of TopXML. I am available for international and local (Australia) contracts. I am a Solution Architect/Business Analyst. I have worked in IT in several countries (NZ, Australia, South Africa, UK) building and training teams for government and very large non-governmental organizations. I am ex-Microsoft Consulting Services. I wrote the first book on Microsoft XML published in 2000 called XML Programming with VB and ASP. Most recently I have been building tools for the SEO industry. Ask me for a 37 point SEO health-checkup for your website.
Besides the SerializationInfo, GetObjectData() also receives a
context object that provides a hint about the destination of the serialized
object in the form of a StreamingContextStates value. With this piece of
information an object can choose the representation that is most efficient for
the environment where it will be deserialized. For example, if the object will
be re-created in another process currently running on the same machine, the
object can pass system-wide handles, to files for example, verbatim thus
avoiding extra overhead re-opening the file. If the object is intended to be
deserialized on another machine, where the handle is not valid, the object can
store a UNC path the receiving application can access instead of the handle.
Table 12.5 shows the possible values of the StreamingContextStates enumeration.
1.8
Table 12.3 The
values of the StreamingContextStates enumeration identify the source or the
destination of a serialized object. The values can be combined.
StreamingContextStates
Value
Description
All
The serialized data
has to be valid in all contexts.
Clone
The object graph is
cloned and stays within the same process. The cloned graph has access to the
same handles and unmanaged resources as the original graph.
CrossAppDomain
The source or the
destination is in a different AppDomain.
CrossMachine
The source or
destination is on a different computer. The serialized data must not be
machine specific.
CrossProcess
The source or the
destination is a different process on the same computer. Machine specific
data is permitted.
File
The source or the
destination is a file. The serialized data must not be transient, process or
machine specific.
Other
The source or the
destination is unknown.
Persistence
The source or the
destination is a persisted store, e.g a database, files, or another form of
storage. The serialized data must not be transient, process or machine
specific.
Remoting
The source or the
destination is accessed through remoting, but the location is unknown. The
serialized data must not be transient, process or machine specific.
When you looked at the definition of the ISerializable interface
in the previous paragraph you might have scratched your head and wondered why
it does not define a method to customize deserialization. What’s all the
flexibility worth if we do not have the deserialization counterpart to
GetObjectData()? If it’s not defined in the interface how does it work? These
are valid questions. Their answer lies only in the semantics of an interface
definition. There very much is a counterpart to the GetObjectData() method, but
the .NET team chose to require it in the form of a constructor, rather than
adding a SetObjectData() method to the interface. An interface cannot express
this requirement, because it can only define method signatures, not class
constructors. Implementating this functionality in a constructor helps avoiding
issues related to multiple, possibly even concurrent, calls to an interface
method. The downside of this design is that we cannot rely on the compiler to
detect a missing deserialization constructor. Instead, we have to guard
deserialization operations with an exception handler block to make sure ill
designed objects will not crash our applications.
You may feel somewhat uneasy to expose a constructor that allows
direct access to all members of the class. After all, this bypasses all
encapsulation and control mechanisms you carefully set up through overloaded
constructors. One step to protect ourselves from illegitimate use of this
constructor is to always declare the deserialization constructor protected
instead of public. At least protected access to the constructor prevents
explicit use of this constructor to instantiate objects. The serialization
framework is not affected by this access restriction because it calls the
constructor through the reflection API.
The signature of the deserialization constructor is identical to
the signature of the GetObjectData() method, it receives a SerializationInfo
object and a StreamingContext object. The SerializationInfo object contains the
same name-value pairs we added in GetObjectData().
This time the StreamingContext object, provides information about
the origin of the serialized data, i.e. whether it came from a live object
running on the same machine or if it was received over a network or from a
file. Once again we can leverage this information to optimize the overall
serialization process like we have already seen when we already seen when we
discussed implementing GetObjectData().
Figure 12.1 The
SerializationInfo object serves as a container for the data we want to
serialize. We fill the container in the GetObjectData) method of the
ISerializable interface. When the formatter deserializes the object it hands us
the container to retrieve the data we put into it.
The following example class in listing 12.1 below shows how we
can implement the ISerializable interface in order to persist a
non-serializable SqlConnection object. We wrap the SqlConnection object with a
SerializableSqlConnection class that implements ISerializable. The first choice
to make a non-serializable SqlConnection would be to derive a new class that
implements ISerializable, but unfortunately the SqlConnection connection class
is sealed and can not be extended. When we wrap a class we have to implement
pass-through methods for each public property and method of the class. We will
learn a better technique than wrapping a class in section 12.3, but for now we
take a look how we can implement ISerializable.
Our GetObjectData() implementation persists enough information to
create a new SqlConnection object that connects to the same database as the
serialized object. It calls the AddValue() method to add the connection string
and the connection state to a SerializationInfo object. The formatter in use
will write the two values to output stream. Later on, when we deserialize an
object from the persisted data, the formatter will populate a SerializationInfo
object with the name-value pairs the original object persisted in
GetObjectData(). It passes the new SerializationInfo to the deserialization
constructor. The constructor can retrieve the values by their name through a
number of type-safe Get methods exposed by the SerializationInfo class. Table
12.6 shows the complete list of these Get methods.
20
Listing 12.1 This class
serializes a SqlConnection object, which is not marked [Serializable]
using System;
using System.Runtime.Serialization; // for ISerializable
using System.Data.SqlClient; // for SqlConnection
using System.Data; // for ConnectionState
namespace Christoph.Simple
{
[Serializable] | #1
public class
SerializableSqlConnection : ISerializable
|
(annotation)
<#1 Mark the class serializable and declare that it handles its own
serialization.>
(annotation)
<#2 The SqlConnection class is not serializable.>
(annotation)
<#3 The deserialization constructor is called to restore all the serialized
members. The constructor retrieves the stored values from SerializationInfo
object to create a new SqlConnection object. If the original connection was
open at the time it was serialized, the new connection is opened. We can
declare the constructor protected to guard against explicit use.>
(annotation)
<#4 The GetObjectData() method stores enough information to restore the
SqlConnection.>
There is one more detail we have to know about in order to
correctly implement a deserialization constructor. We must not execute any
methods on any objects we retrieve from the SerializationInfo container. The
.NET Framework does not guarantee that these objects are fully constructed and
initialized when it is calling the deserialization constructor. In the example
above, we can call Open() on the connection object only because we instantiated
it ourselves, we did not retrieve it from the SerializationInfo.
1.9
Table 12.6 The
SerializationInfo exposes methods to add and retrieve name-value pairs to
describe an object.
Method
Description
public void
AddValue(
string name, XXX value
);
Adds a name-value
pair to the SerializationInfo. Several overloads are available to add all
types to the SerializationInfo
public bool
GetBoolean(
string name
);
Retrieves a Boolean
value from the SerializationInfo.
public byte
GetByte(
string name
);
Retrieves an 8-bit
unsigned integer value from the SerializationInfo.
public char
GetChar(
string name
);
Retrieves a Unicode
character value from the SerializationInfo.
public DateTime
GetDateTime(
string name
);
Retrieves a
DateTime value from the SerializationInfo.
public decimal
GetDecimal(
string name
);
Retrieves a Decimal
value from the SerializationInfo.
public double
GetDouble(
string name
);
Retrieves a
double-precision value from the SerializationInfo.
public
SerializationInfoEnumerator GetEnumerator(
string name
);
Returns an
SerializationInfoEnumerator to iterate over the name-value pairs in the
SerializationInfo.
public short
GetInt16(
string name
);
Retrieves a 16-bit
signed integer value from the SerializationInfo.
public int
GetInt32(
string name
);
Retrieves a 32-bit
signed integer value from the SerializationInfo.
public long
GetInt64(
string name
);
Retrieves a 64-bit
signed integer value from the SerializationInfo.
public sbyte
GetSByte(
string name
);
Retrieves an 8-bit
signed integer value from the SerializationInfo.
public float
GetSingle(
string name
);
Retrieves a
single-precision value from the SerializationInfo.
public string
GetString(
string name
);
Retrieves a String
value from the SerializationInfo.
public ushort
GetUInt16(
string name
);
Retrieves a 16-bit
unsigned integer value from the SerializationInfo.
public uint
GetUInt32(
string name
);
Retrieves a 32-bit
unsigned integer value from the SerializationInfo.
public ulong
GetUInt64(
string name
);
Retrieves a 64-bit
unsigned integer value from the SerializationInfo.
public object
GetValue(
string name
);
Retrieves a value
of any type from the SerializationInfo.
public void
SetType(
Type type
)
Sets the Type to
appear in the serialized output. This type is instantiated when the object is
deserialized.