Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / XML

Extensible Strongly Typed (De)serializer Foundation Supporting Encryption and Compression

3.40/5 (8 votes)
10 Mar 2008GPL36 min read 1   237  
(De)serialize your objects encrypted and/or compressed with 3 lines of code

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.

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

C#
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

class diagram

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.

NameDescriptionPattern
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
SerializerExceptionWraps 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

NameDescriptionPattern
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 operationFactory
FormatterXmlFormatter, 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...

C#
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:

C#
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:

C#
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:

C#
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:

C#
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?

C#
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:

C#
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.

C#
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.

C#
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.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)