Serialization in .NET, Part 2

Overview

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.

Serializing Database Information

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:

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.

Custom Serialization Basics

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:

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:

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:

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:

Extending Custom Serialization for Object Graphs

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:

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.

Additional Reading

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.

Summary

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.