Mar 20 2011

Fluent XML Serialization–Part 2: Compiling Serialization

Category: FluentlyXMLMatt @ 13:40

The design and implementation of Fluently-XML’s domain-specific language was covered in my last post.  The language builds up configuration information that must then be transformed into a form that’s useful for performing serialization and deserialization at runtime.  In this post, I’ll show you how that translation occurs.

The Configuration Data

As I illustrated in the previous post, Fluently-XML’s domain-specific language (DSL) does little more than translate calls into configuration data.  The most complicated translation is the DetermineIdentityBy operation, which is really just three lines of code:

/// <summary> </summary>
internal class TypeDeserializationSpec<T> : ITypeDeserializationSpec<T>
{
    private readonly IDeserializationConfig _config;

    public TypeDeserializationSpec(IDeserializationConfig config)
    {
        _config = config;
    }

    /// <summary> </summary>
    public ITypeDeserializationSpec<T> DetermineIdentityBy(Func<T, object> identitySelector)
    {
        //Selector must be converted for use with the Reflection-based core deserialization process. 
        Func<object, object> wrappedSelector = o => identitySelector((T)o);
        _config.SetIdentityFunction(typeof(T), wrappedSelector);
        return this;
    }
    ...
}

(For more about the “magic” that’s going on, see the last post.)

The underlying deserialization configuration data is stored in IDeserializationConfig.  There is a corresponding interface, ISerializationConfig, that exists for storing configuration information about performing object-to-XML serialization.  Here are both interfaces:

internal interface ISerializationConfig
{
    void SerializePropertyAsArray(Type target, PropertyInfo property);
    void SetIgnoredProperty(Type target, PropertyInfo property);
    void MarkTypeAsSealed(Type type);
    void SerializePropertyAsElement(Type target, PropertyInfo property, string elementName);
    void SerializePropertyAsElementUsing(Type target, PropertyInfo property, string elementName, Func<object, object> converter);
}

internal interface IDeserializationConfig
{
    void SetIdentityFunction(Type target, Func<object, object> selector);
    void SetPropertyDeserializer(Type target, PropertyInfo property, PropertyDeserializer propertyDeserializer);
    void AddPostDeserializationCallback(Type target, Action<object> callback);
}

Both interfaces are currently implemented by a single class, FluentSerializationConfig.  This class exists primarily for merging of configuration from multiple sources.  This is important since you could potentially have multiple WhenSerializing<T> calls for a specific type, and in such cases the configuration data needs to aggregated in order to produce the correct serializer at runtime.

Building Up Serializers

Internally, FluentSerializationConfig builds up configuration for each type that requires either custom serialization or deserialization behavior.  This information is stored in TypeSerializationConfig and TypeDeserializationConfig respectively.  Prior to actually performing serialization or deserialization, Fluently-XML “compiles” FluentSerializationConfig into a new class, RuntimeSerializationConfig, that contains ITypeSerializer and ITypeDeserializer instances.  Here’s the high-level transformation:

internal class FluentSerializationConfig : ISerializationConfig, IDeserializationConfig
{
    ...
    
    public RuntimeSerializationConfig Compile()
    {
        var runtimeConfig = new RuntimeSerializationConfig();

        foreach (var serializerConfig in _typeSerializationConfig.Values)
        {
            var serializer = serializerConfig.BuildSerializer();
            runtimeConfig.AddSerializer(serializer);
        }

        foreach (var deserializerConfig in _typeDeserializationConfig.Values)
        {
            var deserializer = deserializerConfig.BuildDeserializer();
            runtimeConfig.AddDeserializer(deserializer);
        }

        return runtimeConfig;
    }

}

These instances are only created for types that were configured using the DSL.  However, Fluently-XML is still able to serialize and deserialize types that were not configured by dynamically creating new instances of ITypeSerializer or ITypeDeserializer on demand.

ITypeSerializer and ITypeDeserializer sport a par of methods, one for determining if the instance applies to a type, and another to perform serialization/deserialization:

internal interface ITypeSerializer
{
    bool CanSerialize(Type type);
    XElement Serialize(Type type, object obj, ISerializationContext context);
}

internal interface ITypeDeserializer
{
    bool CanDeserialize(Type type);
    object Deserialize(Type target, XElement element, IDeserializationContext context);
    object GetIdentityFor(object obj);
}

(You’ll notice that ITypeDeserializer actually has an extra method, GetIdentityFor.  This is a bit of cruft that I plan to refactor, I just haven’t gotten around to it.  It exists to support the deserialization of complex object hierarchies in cases where the same object may appear at multiple locations within the object graph. )

These interfaces aren’t exposed externally.  Instead, Fluently-XML exposes IFluentSerializer and IFluentDeserializer, which are capable of serializing and deserializing any type, respectively. 

Performing Serialization and Deserialization

In this post, I gave you an overview of how configuration data built up by Fluently-XML’s DSL is converted into types that can perform serialization and deserialization at runtime.  I didn’t get into too many details, but I plan to go more in-depth in the future once I’ve covered the high-level design.  In the next post, I’ll show you the classes that implement IFluentSerializer and IFluentDeserializer and how they leverage the types that are “compiled” from the DSL’s configuration data.

Questions or comments?  Please comment below or feel free to contact me through my site!

Tags:

blog comments powered by Disqus