-
Notifications
You must be signed in to change notification settings - Fork 1
Basics
Define your serializable object graph using DataContract and DataMember attributes.
[DataContract]
[ShapeshifterRoot]
public class Customer
{
[DataMember]
public string Name {get; set;}
}
Mark your root classes with ShapeshifterRoot attribute. You only need to mark those classes that you feed into the serializer directly. Classes which are part of the serialization hierarchy required to have the DataContract attribute only.
Create a Shapeshifter serializer and use the Serialize and Deserialize method.
public void Example(Customer customer)
{
var serializer = new ShapeshifterSerializer<Customer>();
var storedFormat = serializer.Serialize(customer);
// ....
var deserializedCustomer = serializer.Deserialize(storedFormat);
}
That's it.
Once you release your application a snapshot of the Shapeshifter serializable object structure must be taken. This will serve as a baseline for subsequent changes. (You should only take a snapshot when you know that there will be stored live(!) data using that version. Otherwise you end-up writing converters for interim test and preview releases.) Taking snapshot is easy with the command line tool snapshot.exe (There's a public API to reach the same functionality, see advanced scenarios for details.)
snapshot.exe add -N:V1 -i:bin\debug\*.dll
This command parses all dll files in the bin\debug folder and looks for types marked with ShapeshifterRoot attribute as roots for serialization (Types referenced within the root type does not need the ShapeshifterRoot attribute). It creates a snapshot of their structure and stores it in the history file. Using the switch -w will calculate the snapshot, but it will not add to the history file. Using the switch -f lets you specify the history file.
Now you have a baseline and you can start to develop a new release probably with structural changes, renames, etc. Running the tool in compare mode will let you known what are the differences compared to the base release, so you can handle them.
snapshot.exe compare -i:bin\debug\*.dll
When you run the command above a list of differences will be presented:
Compared to snapshot V1 the deserializer for type Customer with version 3353377449 is missing.
If you ran the command with the -v verbose switch the original structure will be displayed as well.
Now you have to write a custom deserializer for this old version in order to be able to read old records. Let us assume that the original Customer contained the Name, Address and the list of Orders.
[ShapeshifterRoot]
[DataContract]
public class Customer
{
[DataMember] public string Name { get; set; }
[DataMember] public Address HomeAddress { get; set; }
[DataMember] public List<Order> Orders { get; set; }
}
In version 2 you figured out that you need to differentiate between individual and company clients. So you made Customer abstract and created IndividualCustomer class as a child of Customer.
[ShapeshifterRoot]
[DataContract]
public class IndividualCustomer : Customer
{
[DataMember] public string Name { get; set; }
[DataMember] public Address HomeAddress { get; set; }
}
The custom deserializer is a method conforming to a signature and holding the Deserializer attribute.
[Deserializer("Customer", 3353377449)]
public static object DeserializerForOldVersion(IShapeshifterReader reader)
{
var builder = new InstanceBuilder<IndividualCustomer>();
builder.SetMember("Name", reader.Read<string>("Name"));
builder.SetMember("HomeAddress", reader.Read<Address>("HomeAddress"));
builder.SetMember("Orders", reader.Read<List<Order>>("Orders"));
return builder.GetInstance();
}
Here through IShapeshifterReader you can access the individual fields/properties of the original Customer. Also with the help of InstanceBuilder class you can create an instance of the new IndividualCustomer and set its members. In this scenario actual data holder properties have a public setter, so it is ok to create a new instance, set them directly and return the instance. The InstanceBuilder is just a helper as it can set private fields as well. Also you can pass the IShapeshifterReader directly to the builder which will fill fields/properties with matching names. Having this custom deserializer ensures that you will be able to read serialized records in the old format. Please note that serialization always occurs using the latest structure so reading the old record and saving back actually upgrades it. In the Advanced section you can find more on how and where to define custom deserializers.
Shapeshifter supports and detects many type of changes. Let's see them:
Shapeshifter refers to types by their name without the namespace so if you move your class around to a different namespace there's nothing to do, it will properly deserialize the old format. (As always choices has a consequences. Here you cannot have two Shapeshifter serializable class with the same name in different namespaces, which is a. a good trade of (at least in our experience) b. can be handled anyway (see advanced topics)).
A custom deserializer should be written for the old name and version like:
[Deserializer("Customer", 3353377449)]
public static object DeserializerForOldVersion(IShapeshifterReader reader)
{
var builder = new InstanceBuilder<IndividualCustomer>(reader);
return builder.GetInstance();
}
As no field name changes took place InstanceBuilder can automatically fill the new instance.
Within the custom deserializer you can read back the value using the old name and set it using the new name
var builder = new InstanceBuilder<NewClass>();
builder.SetMember("NewName", reader.Read<string>("Name"));
Within the custom deserializer you can read back the value using the old name do your conversion and set it using the new name
var oldValue = reader.Read<string>("Age");
var newValue = Int32.Parse(oldValue);
builder.SetMember("Age", newValue);
Let's assume that in the old structure there was a type called Address which was used in Customer but now removed and replaced with a string representing the address (I know, I know) As the structure of Customer changed (HomeAddress field is gone) you must write a custom deserializer for it. Within the deserializer it is possible to get a IShapeshifterReader for a contained element, so you can access the fields of the Address type even if it is not there anymore:
[Deserializer("Customer", 3353377449)]
public static object DeserializerForOldVersion(IShapeshifterReader reader)
{
var builder = new InstanceBuilder<IndividualCustomer>();
var addressReader = reader.GetReader("HomeAddress");
var city = addressReader.Read<string>("City");
var zip = addressReader.Read<string>("Zip");
...
builder.SetMember("FlatAddress", city + " " + zip + " " + street);
return builder.GetInstance();
}