Introduction
Serializing and deserializing an object, or an entire graph of connected objects, is a common task. Serialization as a concept is deeply built into the .NET Framework. Concrete formatters come in different flavors designed for different environments and their requirements.
That's the reason why the XmlSerializer
doesn't implement the same interfaces as the BinaryFormatter
and vice versa. The fun starts when one start to explore the different formatters and their related attributes and interfaces. I personally spent a noticeable time discovering System.Runtime.Serialization
and System.Xml.Serialization
namespaces. The provided formatters unfortunately have their limitations. For example, the XmlSerializer
does not support collections, the BinaryFormatter
and the SoapFormatter
serialize only classes marked as serializable. This makes it harder to learn and understand the serializing in general because it depends on which formatter one is currently using.
Background
After writing the boilerplate serialization code in different flavors again and again, doing basically the same just using different formatters or the like, I came up with the idea to design an extensible serialization "framework".
Prerequisites
Let me first introduce the DataStore
class used in the following code examples and in the provided demo console project as well.
Nothing fancy, just a class with two string
properties, except the Serializable
attribute for binary and soap formatter and the ISerializerCallback
interface implementation.
[Serializable]
public class DataStore :
Raccoom.Runtime.Serialization.ComponentModel.ISerializerCallback
{
public string Name { get; set; }
public string Location { get; set; }
#region ISerializeableObject Members
void Raccoom.Runtime.Serialization.ComponentModel.ISerializerCallback.Serializing()
{
Console.WriteLine("Serializing");
}
void Raccoom.Runtime.Serialization.ComponentModel.ISerializerCallback.Serialized()
{
Console.WriteLine("Serialized");
}
void Raccoom.Runtime.Serialization.ComponentModel.ISerializerCallback.Deserialized()
{
Console.WriteLine("Deserialized");
}
#endregion
}
Raw Like Sushi
This is the code needed to flush the DataStore
class instance to disk as Rijndael encrypted and deflate compressed XML content.
Just a lot of boilerplate code we're likely to copy'n'paste the next time we have to jot it down somewhere else.
System.Security.Cryptography.SymmetricAlgorithm symAlgo =
System.Security.Cryptography.Rijndael.Create();
System.Xml.Serialization.XmlSerializer xmlSerializer =
new System.Xml.Serialization.XmlSerializer(typeof(DataStore));
DataStore ds = new DataStore();
ds.Location = "HDD";
ds.Name = "IsolatedStorage";
using (Stream stream =
new System.IO.FileStream("test.xml", FileMode.Create, FileAccess.Write))
using (DeflateStream compressStream =
new DeflateStream(stream, CompressionMode.Compress))
using (CryptoStream cryptoStream = new CryptoStream(compressStream,
symAlgo.CreateEncryptor(),
System.Security.Cryptography.CryptoStreamMode.Write))
{
xmlSerializer.Serialize(cryptoStream, ds);
}
using (System.IO.Stream stream =
new System.IO.FileStream("test.xml", FileMode.Open, FileAccess.Read))
using (System.IO.Compression.DeflateStream compressStream =
new DeflateStream(stream, CompressionMode.Decompress))
using (CryptoStream cryptoStream = new CryptoStream(compressStream,
symAlgo.CreateDecryptor(),
System.Security.Cryptography.CryptoStreamMode.Read))
{
ds = xmlSerializer.Deserialize(cryptoStream) as DataStore;
}
The above example is writing encrypted and compressed XML to the file test.xml. So far so good, everything is up and running and the code works fine. But imagine you have to change the format from XML to binary or soap? Furthermore you don't need compression any more (1TB HDDs are coming our way these days). What matters now is how many code changes you have to apply and how predictable and reliable the resulting change will work, right?
Serializer Foundation
Initially started to ease serialization with a class that handles the boilerplate code to use different available formatters (custom formatters here on The Code Project[^]) in a unified way, I realized that there are more related topics that this class should cover. I already wrote specialized serialization classes to export datasets encrypted or export images compressed.
Fortunately encryption and compression features are implemented as System.IO.Stream
inherited classes in the .NET Framework. The serialization process itself uses streams as well and so it was just a matter of chaining those streams to get a serializer
class that supports compression and/or encryption.
Key Features
- Decouples the formatter from the serializer
- Client code programs against the stable
ISerializer
interface while formatters can change - You can implement the callback interface
ISerializerCallback
to notify the class instance about the serializing processing (Use case: Fixing BindingList<t> Deserialization[^]) - Factory makes it easy to create a ready to use serializer instance
- Extendable design approach with interfaces and factories, Open-Closed Principle
- Supports Deflate and GZip compression
- Cryptography feature supporting
SymmetricAlgorithm
- Three easy lines of code for a whole roundtrip (serialize/deserialize)
Remarks
Despite the unified handling, this serializer class provides for any type of formatter the .NET world comes up with, your class types still need to implement the underlying formatter specific requirements like attributes, interfaces and special constructors before they can be successfully serialized/deserialized.
Architecture
A full blown technical reference can be browsed here [^].
Building Blocks
The building blocks are the interfaces nested in the Raccoom.Runtime.Serialization.ComponentModel
namespace.
Name | Description | Pattern |
ISerializer<TType> | Decouples and hides the complexity of the serializer implementation. Facade consumed by the client code. Supports creating new instances of the given TType and (de)serialize instances of TType to a stream or a file. | Facade |
ISerializerCallback | Hollywood style "Don't call us, we call you", implement this interface to your types to run custom code at certain stages in the (de)serialization process.
| Strategy |
ISerializerFormatter | Decouples the formatter classes from the serializer and the client code. Is responsible to (de)serialize the given object graph into a stream. | Adapter |
SerializerException | Wraps exceptions occurring during (de)serialize execution. Allows a unified way of handling different type of formatter specific exceptions. | |
Implementations
The implementations nested in the Raccoom.Runtime.Serialization
and Raccoom.Runtime.Serialization.Formatter
namespaces
Name | Description | Pattern |
Serializer<TType, TSerializerFormatter> | Implements in a strongly typed way (TType ) the features to (de)serialize types against a given ISerializerFormatter implementation. | Strategy |
SerializerFactory | Is responsible to create ready to use ISerializer instances for any given operation | Factory |
Formatter | XmlFormatter , BinaryFormatter and SoapFormatter nested in the namespace Raccoom.Runtime.Serialization.Formatter completes the functionality needed to get the job done. | Adapter |
Using the Code
The following code sections show how to use the serializer. The DataStore
class is serialized in different formats in conjunction with compression and encryption. Except for the factory method call nothing fancy changes, compared to the above lines you can get a thumbnail sketch within seconds (maintainability).
Security goes first...
System.Security.Cryptography.SymmetricAlgorithm symAlgo
= System.Security.Cryptography.Rijndael.Create();
Create a Serializer
for GZip compressed XML, create a DataStore
instance and assign some values before serialize/deserialize:
ISerializer<DataStore> xmlGzip = SerializerFactory.CreateXmlSerializerGZip<DataStore>();
Console.WriteLine(xmlGzip);
DataStore dataStore = xmlGzip.CreateInstance();
dataStore.Name = "HDD";
dataStore.Location = "IsolatedStorage";
xmlGzip.Serialize("datastore.gip", dataStore, symAlgo);
dataStore = xmlGzip.Deserialize("datastore.gip", true, symAlgo);
Create a serializer
for Deflate compressed XML and serialize/deserialize:
ISerializer<DataStore> xmlDeflate =
SerializerFactory.CreateXmlSerializerDeflate<DataStore>();
Console.WriteLine(xmlDeflate);
xmlDeflate.Serialize("datastore.def", dataStore, symAlgo);
dataStore = xmlDeflate.Deserialize("datastore.def", true, symAlgo);
Create a binary serializer that doesn't support compression or encryption and serialize/deserialize:
ISerializer<DataStore> binary = SerializerFactory.CreateBinarySerializer<DataStore>();
Console.WriteLine(binary);
binary.Serialize("datastore.bin", dataStore);
dataStore = binary.Deserialize("datastore.bin", true);
Create a serializer for GZip compressed soap and serialize/deserialize:
ISerializer<DataStore> soapDeflate =
SerializerFactory.CreateSoapSerializerGZip<DataStore>();
Console.WriteLine(soapDeflate);
soapDeflate.Serialize("datastore.soap", dataStore, symAlgo);
dataStore = soapDeflate.Deserialize("datastore.soap", true, symAlgo);
Are there still the expected values?
System.Diagnostics.Debug.Assert(dataStore.Location == "IsolatedStorage");
System.DiSystem.Diagnostics.Debug.Assert(dataStore.Name == "HDD");
In fact, the above code does more than meet the eye.
Predictable, Reliable, Maintainable
- The callback interface methods get called independently of the underlying formatter, which now notify the
DataStore
instance about the serialization process allowing the class to suppress events or to re-attach event handlers after deserialization. - The client code is independent of currently used
Formatter
interface - Ready to use, flexible, extendable and tested infrastructure
- You don't "copy'n'paste" your boilerplate serialization code (-> bugs, changes)
- Just three lines of code aren't likely to contain more than one bug
Extensibility
As a proof of concept, I've implemented a custom formatter written by Patrick Boom [^] into the console demo assembly. The original class signature looks like this:
public sealed class XmlFormatter : IFormatter{}
The class must implement the interface ISerializerFormatter
and has to provide a public
constructor. The compiler checks for a public
parameterless constructor (generic constraint) which must be there in order to let the serializer instantiate the formatter.
public sealed class XmlFormatter :
IFormatter,Raccoom.Runtime.Serialization.ComponentModel.ISerializerFormatter
{
#region ISerializerFormatter Members
public XmlFormatter() { }
void ISerializerFormatter.Serialize(Stream stream, object instance)
{
this.Serialize(stream, instance);
}
object ISerializerFormatter.Deserialize(Stream stream, Type[] extraTypes)
{
return this.Deserialize(stream, extraTypes[0]);
}
#endregion
}
Basically the formatter would work, but for convenience we should provide a factory.
public sealed class XmlFormatterFactory
{
public static ISerializer<ttype> CreateXmlSerializer<ttype>()
where TType : new()
{
return new Serializer<ttype,>(CompressionAlgorithm.None);
}
public static ISerializer<ttype> CreateXmlSerializerDeflate<ttype>()
where TType : new()
{
return new Serializer<ttype,>(CompressionAlgorithm.Deflate);
}
public static ISerializer<ttype> CreateXmlSerializerZip<ttype>()
where TType : new()
{
return new Serializer<ttype,>(CompressionAlgorithm.GZip);
}
}
Points of Interest
Deployment
The software is also available as a ClickOnce setup. The technical reference can be browsed here.
Backstage
The whole project presented here was made in Visual Studio 2008 Beta 2. For coding purpose, it really worked like a RTM and never crashed even though it's a feature complete beta version. Only the HTML Editor to write this article sometimes crashed.