AddThis Social Bookmark Button

Print

Serialization in .NET, Part 2

by Dan Frumin
03/01/2004

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:

[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.

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:

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.

.NET Framework Essentials

Related Reading

.NET Framework Essentials
By Thuan L. Thai, Hoang Lam

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()
  {}
}

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:


  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()
  {}
}

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.


Return to ONDotnet.com.