In a previous article we discussed the benefits of using .Net's built-in serialization support in your applications. As you probably realize, the objects offered to us by .Net are quite powerful and useful. However, not every core class within .Net implements serialization. This means that sooner or later you're going to run into its limitations. The good news is that there's a solution, as .Net also allows us to implement our custom serialization provider.
Let's consider a quick scenario involving an application that, among other
things, connects to a SQL database. To support our application, we've
gone ahead and created a configuration class called MyConfig. Here is the
code for that class:
[Serializable]
public class MyConfig
{
public string CustomerName;
public SqlConnection ConnectionInfo =
new SqlConnection();
...
}
We go ahead and use the config object as appropriate and then try and serialize
it to store the configuration on the hard drive.
MyConfig config = new MyConfig();
config.ConnectionInfo.ConnectionString =
"Server=MyServer; Initial Catalog=MyDatabase;";
config.CustomerName = "NewCustomer";
...
BinaryFormatter bf = new BinaryFormatter();
FileStream fs = new FileStream("MyConfig.bin",
FileMode.Create);
bf.Serialize(fs, config);
fs.Close();
If you try to run this code, you'll see that the .Net Framework throws an
exception. The reason is that the SqlConnection class is not marked as
serializable. So when the formatter walks the object graph, it gets to an
object that it doesn't know how to handle and it throws an exception.
This is a good case for adding custom serialization to our class.
As mentioned at the beginning of the article, the .Net Framework allows us to
add custom serialization support to any class. We do this by implementing
the ISerializable interface. My first thought was to derive a class from
SqlConnection, but unfortunately SqlConnection is a sealed class, meaning that
no other class can derive from it. Since we can't derive from SqlConnection, we'll have to wrap it. We start with the following class
definition:
public class SerializableSqlConnection
{
public SqlConnection conn = new SqlConnection();
...
}
To use this class in our code we have to do a couple of things. First, we
have to modify the MyConfig class to use an object of type
SerializableSqlConnection instead of a regular SqlConnection. Second, we
add one more level of indirection to our use of the config object. The
resulting code for these changes would look like this:
[Serializable]
public class MyConfig
{
public string CustomerName;
public SerializableSqlConnection ConnectionInfo =
new SerializableSqlConnection();
...
}
and...
MyConfig config = new MyConfig();
config.ConnectionInfo.conn.ConnectionString =
"Server=MyServer; Initial Catalog=MyDatabase;";
config.CustomerName = "NewCustomer";
The changes are fairly minor and certainly easy for us to use in our code.
|
Related Reading .NET Framework Essentials |
Now we turn our attention to adding support for the ISerializable< interface to
the SerializableSqlConnection class. The interface requires us to add two
methods to our class. One to get the information from the object to the
formatter, and the other to get information from the formatter to the
class.
We get information from the class to the formatter using the GetObjectData
method, which takes two parameters. At its simplest you can think of the
first parameter, the SerializationInfo object, as the pipeline into the
formatter. As you'll see below, we make use of the AddValue method to add
an entry into the formatters managed list. This ensures that the
information we want stored within the serialization stream is saved
appropriately.
The second parameter, the StreamingContext, allows you to extract information
about the serialization or deserialization. For example, you can use this
object to figure out whether the stream is deserialized into the same machine,
app domain, or process that serialized it. This allows you to customize
your deserialization should the user migrate the binary file (MyConfig.bin) to
another machine. We don't use the StreamingContext in this particular
code sample, but you can imagine that it's quite useful for certain code bases.
The resulting code for the GetObjectData function is below.
// Serialization Function.
public void GetObjectData(SerializationInfo info,
StreamingContext context)
{
// Use one of the many overrided AddValue methods.
// In this case, to store a string.
info.AddValue("ConnectionString",
conn.ConnectionString);
}
At this point, we'll switch our attention to the pipeline that allows us to get
information out of the deserialization stream and into our class. At
first we might expect a SetObjectData function to exist, similar to GetObjectData.
The implication is that this function would be called after the
object is instantiated. However, since serialization and deserialization
support whole object graphs, we cannot always count on the object to be appropriately
initialized before the SetObjectData function is called. Further, a
SetObjectData method might subject the user to synchronization and threading
issues.
Clearly a well-designed and developed application could easily bypass these potential issues, as they are not guaranteed to show up. However, the .Net team chose to design a solution that eliminates the need for the developer to address this issue. This solution is composed of a custom overloaded constructor that is called any time an object is deserialized. This allows all the initialization and configuration to happen at the exact same time, ensuring that by the time a particular object in the graph is deserialized, it's also ready to use.
The resulting constructor is fairly straightforward and uses the GetValue method
of the SerializationInfo as you might expect were there to be a SetObjectData
method. Here is the code for the custom constructor we added to our class:
// Deserialization Constructor.
public SerializableSqlConnection (SerializationInfo info,
StreamingContext context)
{
conn.ConnectionString =
(string) info.GetValue("ConnectionString",
typeof(string));
}
The only other interesting side-effect is the need to create a default constructor, which we didn't previously need. That's because any instantiation of an object requires some constructor. Since our class had no explicitly defined constructor, the .Net Framework implicitly added the default constructor. The complete code for our class, including the two methods, the default constructor, and the Serializable attribute is included below:
[Serializable]
public class SerializableSqlConnection : ISerializable
{
public SqlConnection conn = new SqlConnection();
// Serialization Function.
public void GetObjectData(SerializationInfo info,
StreamingContext context)
{
info.AddValue("ConnectionString", conn.ConnectionString);
}
// Deserialization Constructor.
public SerializableSqlConnection (SerializationInfo info,
StreamingContext context)
{
conn.ConnectionString =
(string) info.GetValue("ConnectionString",
typeof(string));
}
// Default Constructor
public SerializableSqlConnection()
{}
}
Having achieved some simple custom serialization, we must consider expanding our
classes so they support object graphs. The changes required to our
classes are fairly simple. Let's extend our sample to allow the config
object to also store information about a TcpClient connection. An initial
pass at the code might suggest a similar solution to the one we used
above. Unfortunately, the TcpClient class does not allow us to easily and
retroactively access its configuration information. We therefore wrap
that information into our class ourselves, as seen below:
private TcpClient TcpInfo;
private string TcpInfoHostname;
private int TcpInfoPort;
public void SetTcpInfo(string hostname, int port)
{
TcpInfo = new TcpClient(hostname, port);
TcpInfoHostname = hostname;
TcpInfoPort = port;
}
We use this in our code by calling the SetTcpInfo method. Now all that
remains is making our class serializable. Since TcpClient is not marked
as serializable we have to implement the ISerializable interface
ourselves. Most of the implementation is self-evident after the sample
above. The only thing that remains is adapting the implementation to
support calling the serialization for its object graph, which in this case
contains the
SerializableSqlConnection object.
The resulting code with these changes is shown below. You should note the use of pass-through calling from the MyConfigCustom ISerializable interface to the SerializableSqlConnection ISerializable interface. This is the core change that allows support for full object graph serialization.
[Serializable]
public class MyConfigCustom : ISerializable
{
public string CustomerName;
public SerializableSqlConnection ConnectionInfo =
new SerializableSqlConnection();
private TcpClient TcpInfo;
private string TcpInfoHostname;
private int TcpInfoPort;
public void SetTcpInfo(string hostname, int port)
{
TcpInfo = new TcpClient(hostname, port);
TcpInfoHostname = hostname;
TcpInfoPort = port;
}
// Serialization Function.
public void GetObjectData(SerializationInfo info,
StreamingContext context)
{
info.AddValue("CustomerName", CustomerName);
info.AddValue("TcpInfoHostname", TcpInfoHostname);
info.AddValue("TcpInfoPort", TcpInfoPort);
// Call into the SerializableSqlConnection
// ISerializable interface
ConnectionInfo.GetObjectData(info, context);
}
// Deserialization Constructor.
public MyConfigCustom (SerializationInfo info,
StreamingContext context)
{
CustomerName = (string) info.GetValue("CustomerName",
typeof(string));
TcpInfoHostname = (string) info.GetValue("TcpInfoHostname",
typeof(string));
TcpInfoPort = (int) info.GetValue("TcpInfoPort",
typeof(int));
TcpInfo = new TcpClient(TcpInfoHostname, TcpInfoPort);
// Call into the SerializableSqlConnection
//ISerializable interface
ConnectionInfo = new SerializableSqlConnection(info,
context);
}
// Default Constructor.
public MyConfigCustom()
{}
}
In addition to the custom serialization support we discussed, the .Net Framework also supports the use of outside serialization controllers, called "surrogates." The intent is to allow the developer to create a single surrogate that supports one or more similar classes rather than implement the serialization support into every one of the classes. To read more on this support, you should research the ISerializationSurrogate and ISurrogateSelector interfaces.
By this point you should be able to do some very interesting things in your code using both the default and the custom serialization support. For example, the ability to use serialization to quickly and easily store the state of your application. However, you should always keep an eye on your class signature as any changes would render your serialized stream unreadable by a different version of the class. That said, some quick planning and architecture will help you evade those issues and maximize the value of this functionality.
Dan Frumin is a long-time technology executive, with over 10 years of experience in the industry.
Return to ONDotnet.com.
Copyright © 2009 O'Reilly Media, Inc.