Skip to content

Advanced

Csaba Nemes edited this page Mar 14, 2015 · 12 revisions

Advanced Topics

Location of custom deserializers

There are two possibilities to place custom deserializers.
You can put them in the same class, this way you have all your conversion methods next to the class they work on.

[DataContract]
[ShapeshifterRoot]
public class MyClass
{
    [DataMember]
    public int Id { get; set; }
    ...

    [Deserializer("MyClass", 5453422)]
    private static object TransformFromVersionOne(IShapeshifterReader reader)
    { ...  }
}

Shapeshifter will automatically detect these methods.
The other option is to create separate classes for each version change, grouping together changes from a specific version.

public class TransformFromVersionOne
{
    [Deserializer("MyClass", 5453422)]
    private static object TransformMyClass(IShapeshifterReader reader)
    { ...  }

    [Deserializer("OtherClass", 6534234)]
    private static object TransformOtherClass(IShapeshifterReader reader)
    { ...  }
}

When you choose this option you must tell Shapeshifter the location of the conversion methods by specifying them as well-known types.

new ShapeshifterSerializer<MyClass>(new [] {typeof(TransformFromVersionOne)})

##Static vs. instance custom deserializers In V1.0 it was only possible to mark static methods with the [Deserializer] attribute. From now on instance method in a class with a public default constructor can also have the [Deserializer] attribute. When running the deserialization the serializer will create a single instance of the class for the length of the run (When running the deserializer again a new instance will be created). This can be useful if you want to exchange information between different deserializer methods. You just write the data into a field and read it later on.

public class DeserializerClass
{
    private string _slotToPassOverData;

    [Deserializer("InnerClass", 849758624)]
    public object InnerClassDeserializer(IShapeshifterReader reader)
    {
        _slotToPassOverData = reader.Read<string>("Value");
        ...
    }

    [Deserializer("OtherInnerClass", 849758624)]
    public object OtherInnerDeserializer(IShapeshifterReader reader)
    {
        return new OtherInnerClass() { Value = _slotToPassOverData };
    }
}

There are two additional features related to instance custom deserializers. First if the class contains a public instance DeserializationEnding method without return value and parameter it will be called after the deserialized object tree is created. This can be a good place to run some fix-ups if necessary. To enable such fix-up the InstanceBuilder class can be configured to enable the modification of instance values after returning the instance.

private InstanceBuilder _builderSaved;

[Deserializer("SomeClass", 849758624)]
public SomeClass SomeClassDeserializer(IShapeshifterReader reader)
{
    var builder = new InstanceBuilder<SomeClass>(reader, true);        
    ...
    _builderSaved = builder;
    return builder.GetInstance();
}

public void DeserializationEnding()
{
    _builderSaved.SetMember("count", _count);
}

This example shows a scenario where the count property of the SomeClass instance is only set at the end.

##Creating custom serializers Usually it's better to let shapeshifter create the serializer for you based on attributes used, but there are some scenarios when it can be helpful to create custom serializers.

###Serialize a class without a DataContract attribute If for some reason you cannot apply [DataContract] attribute on a class you are still to serialize it with the help of custom serializers. Custom serializers are static methods with a specific signature and the [Serializer] attribute applied on them.

[Serializer(typeof(NonDataContractClass), 1)]
public static void Serializer(IShapeshifterWriter writer, NonDataContractClass itemToSerialize)
{
    writer.Write("Value", itemToSerialize.Value);
}

The serializer will call this method for each MyClass instance and pass over an IShapeshifterWriter. You must use the writer to serialize the class.

You must specify a version number as the serializer cannot calculate the version number. Also it is your responsibility to change the version number if the serialized structure changes. Only one serializer can exist at a given time.

The same approach also applies for the deserialization. The deserializer method gets a readed and must return the deserialized instance. You can specify multiple deserializers with different version numbers as multiple versions can exist on the storage. When specifying what to deserialize you either use the type or the name of the class.

[Deserializer(typeof(NonDataContractClass), 2)]
private static NonDataContractClass DeserializerVersion2(IShapeshifterReader reader)
{
    var val = reader.Read<int>("Value");
    return new NonDataContractClass() { Value = val.ToString() };
}

[Deserializer("NonDataContractClass", 1)]
private static NonDataContractClass DeserializerVersion1(IShapeshifterReader reader)
{
    var val = reader.Read<string>("Value");
    return new NonDataContractClass() { Value = val };
}

We relaxed the requirement of always having a version number for custom deserializers. If the deserializer defines the target type it might not specify a version. In such case the current version of the type will be used. This "feature" is mostly a convenience when you create serializer-deserializer pairs for types which not change.

[Serializer(typeof(TimeSpan))]
public static void TimeSpanSerialize(IShapeshifterWriter writer, TimeSpan timeSpan)
{
   writer.Write("value", timeSpan.Ticks);
}

[Deserializer(typeof(TimeSpan))]
public static object TimeSpanDeserialize(IShapeshifterReader reader)
{
   var valueAsTicks = reader.Read<long>("value");
   return new TimeSpan(valueAsTicks);
}

###Serialize a tree of classes having a common ancestor without specifying serializer for each class separately It might happen that you have a complex type hierarchy where you have a root type with all the serializable fields and a lot of child types where the difference is only behaviour. (Imagine a scenario where for type safety reasons we have a StringValueTypeBase type which wraps just a string value and a lot of child types like CustomerId, OrderId, CustomerName.) In such case you must handle the serialization and deserialization for each type separately. When it is done automatically it is not a problem. However if for some reason the root type is not serializable (missing DataContract attribute, etc.) you must create a custom serializer for each type separately. This is where the ForAllDescendants property of the [Serializer] and [Deserializer] attributes come in handy. When specified on one of these attributes the serializer will call the custom serializer/deserializer method for all descendants of the type specified.

[Serializer(typeof (MyBase), 1, ForAllDescendants = true)]
public static void SerializeAnyDescendant(IShapeshifterWriter writer, MyBase item)
{
    writer.Write("MyKey", item.BaseProperty);
}

This method will be called for all classes derived from MyBase.

The deserializer method signature is slightly different as the serializer passes also the target type to the method.

[Deserializer(typeof(MyBase), ForAllDescendants = true)]
public static object DeserializeAnyDescendant(IShapeshifterReader reader, Type targetType)
{
}

One last step is to specify for the serializer where to look for descendant types. It is needed as the serializer must know all descendant types in advance to build up the corresponding serialization/deserialization mechanisms.
This can be done by passing the list of assemblies which can contain a descendant when constructing the serializer.

var serializer = new ShapeshifterSerializer<T>(new[] {Assembly.GetExecutingAssembly()});

###Serialize a tree of classes having a common generic ancestor without specifying serializer for each class separately

The method described just above also works for generic classes. Here you must have a separate class for each specific type, but this is not a problem as usually the serializer and deserializer methods are defined on the generic type.

public class MyBase<T>
{
    [Serializer(typeof (MyBase<>), 1, ForAllDescendants = true)]
    public static void SerializeAnyDescendant(IShapeshifterWriter writer, MyBase<T> item)
    {
    }

    [Deserializer(typeof(MyBase<>), 1, ForAllDescendants = true)]
    public static object DeserializeAnyDescendant(IShapeshifterReader reader, Type targetType)
    {
    }
}

Clone this wiki locally