How to make object immutable yet serializable with System.Xml.Serialization in C#?

230 Views Asked by At

In my app I have a lot of XMLs to serialize from, so I have created quite a lot of models, to serialize to. I use System.Xml.Serialization, so it needs parameterless constructor and public access to properties to write deserialized data to.

Now, I want to implement Memento Pattern to provide Undo and Redo functionality, and in that case it would be good to be able to have immutable objects, so there would not be any bug related to that I changed some property instead of creating new instance.

The problem is I don't see any solution that would provide me both solutions at once - being able to deserialize objects and then provide immutability for Memento pattern (well, there is popsicle immutability but I am not sure about it). Creating separate objects (records or so) that my deserialized objects would be converted to after program is done with deserialization seems like solution, but it has a flaw o introducing a lot of new types that consume time and increase upkeep cost later.

Do you guys have other ideas or patterns that I could use?

1

There are 1 best solutions below

4
Tyler Harbert On

I was able to create a solution with the help of this article. The only caveat is it requires DataContractSerializer instead of System.Xml.Serialization. Here is a dotnetfiddle of my implementation.

public class DataContractXmlSerializer<T>
{
    private readonly DataContractSerializer _dataContractSerializer;

    public DataContractXmlSerializer()
    {
        _ = Attribute.GetCustomAttribute(typeof(T), typeof(DataContractAttribute))
        ?? throw new InvalidOperationException($"Type {typeof(T)} does not contain Attribute of type {typeof(DataContractAttribute)}.");

        _dataContractSerializer = new DataContractSerializer(typeof(T));
    }

    public T Deserialize(string xml)
    {
        var xmlData = Encoding.UTF8.GetBytes(xml);
        var xmlStream = new MemoryStream(xmlData);

        using (XmlDictionaryReader reader = XmlDictionaryReader.CreateTextReader(xmlStream, Encoding.UTF8, new XmlDictionaryReaderQuotas(), null))
        {
            return (T)_dataContractSerializer.ReadObject(reader);
        }
    }

    public string Serialize(T obj)
    {
        using (MemoryStream memoryStream = new MemoryStream())
        {
            _dataContractSerializer.WriteObject(memoryStream, obj);
            return Encoding.UTF8.GetString(memoryStream.ToArray());
         }
    }
}

Update If you must use System.Xml.Serialization, I recommended using a layered approach to convert between your mutable and immutable models. Here is my dotnetfiddle of this approach.

public class ModelXmlSerializer : ImmutableXmlSerializer<ImmutableModel, Model>
{
    protected override ImmutableModel ConvertToImmutable(Model mutable)
    {
        return new ImmutableModel(mutable);
    }
    
    protected override Model ConvertToMutable(ImmutableModel immutable)
    {
        return new Model(immutable);
    }
}

public abstract class ImmutableXmlSerializer<TImmutable, TMutable>
{
    private readonly MyXmlSerializer<TMutable> _xmlSerializer;

    public ImmutableXmlSerializer()
    {
        _xmlSerializer = new MyXmlSerializer<TMutable>();
    }

    public TImmutable Deserialize(string xml)
    {
        return ConvertToImmutable(_xmlSerializer.Deserialize(xml));
    }

    public string Serialize(TImmutable obj)
    {
        return _xmlSerializer.Serialize(ConvertToMutable(obj));
    }
    
    protected abstract TImmutable ConvertToImmutable(TMutable mutable);
    protected abstract TMutable ConvertToMutable(TImmutable immutable);
}

public class MyXmlSerializer<T>
{
    private readonly XmlSerializer _xmlSerializer;

    public MyXmlSerializer()
    {
        _xmlSerializer = new XmlSerializer(typeof(T));
    }

    public T Deserialize(string xml)
    {
        using (TextReader reader = new StringReader(xml))
        {
            return (T)_xmlSerializer.Deserialize(reader);
        }
    }

    public string Serialize(T obj)
    {
        var builder = new StringBuilder();
        using (TextWriter writer = new StringWriter(builder))
        {
            _xmlSerializer.Serialize(writer, obj);
        }
        
        return builder.ToString();
    }
}

This method is less generic as it will require you to create the mapping logic for each model type. You could simplify that process with something like AutoMapper, but I prefer to do this manually.