Wednesday, November 10, 2010

XML Serializable Immutable Objects and Lists

In my current project, I'm using a client/server architecture, where, among other things, the client tells the server what its configuration should be. Because multiple clients can change the server's configuration, I found it necessary to maintain the history of all configurations that have been applied to the server. I modeled the workflow after Subversion, where clients have the ability to update and commit (but no branching or merging). In order to keep things simple, I decided to store the history as a flat file - an XML serialization of the classes that represent the history. This file looks something like this:

<History>
<Version>
<VersionInfo Number="1" Timestamp="2010-02-11 13:38:04" />
<Commit>
<User>MyComputerName\MyUsername</User>
<Name>Brian</Name>
<Notes>Initial commit.</Notes>
</Commit>
<Configuration>
<Something>Initial Value</Something>
</Configuration>
</Version>
<Version>
<VersionInfo Number="2" Timestamp="2010-02-11 14:16:52" />
<Commit>
<User>MyComputerName\MyUsername</User>
<Name>Brian</Name>
<Notes>Changed something.</Notes>
</Commit>
<Configuration>
<Something>Changed Value</Something>
</Configuration>
</Version>
</History>

The original version of these classes look as you would expect (versions were stored in a generic list). The client would serialize a Commit object and a changed Configuration object and send them to the server. The server would create a Version object, give it a VersionInfo object with a Number and a Timestamp, add it to the History, then save it to disk. The client would then update its history.

This would have been fine and dandy if it weren't for my paranoia. I feared that some future developer would modify the history after it was committed. This completely violates the idea of a history. So I set about devising a way to prevent that. What I needed were mostly-immutable objects - objects, whose properties could be set only once.

What I came up with was a pair of generic classes: Immutable<T> and ImmutableList<T>.

public class Immutable<T>
{
private bool isSet;
private T value;

public T Value
{
get
{
return this.value;
}
set
{
if (isSet)
{
throw new InvalidOperationException();
}

this.value = value;
isSet = true;
}
}
}

public class ImmutableList<T> : IList<T>
{
private readonly List<T> list = new List<T>();

public void Add(T item)
{
this.list.Add(item);
}

public void Clear()
{
throw new InvalidOperationException();
}

public bool Remove(T item)
{
throw new InvalidOperationException();
}

public void Insert(int index, T item)
{
this.list.Insert(index, item);
}

public void RemoveAt(int index)
{
throw new InvalidOperationException();
}

public T this[int index]
{
get
{
return this.list[index];
}
set
{
throw new InvalidOperationException();
}
}

<snip>
}

Immutable<T> only lets you set its value once, and ImmutableList<T> only lets you add or insert items - never clearing, removing, or replacing. If you attempt to violate these rules, a big, nasty InvalidOperationException is thrown.

public class History
{
private readonly Immutable<ImmutableList<Version>> versions = new Immutable<ImmutableList<Version>>();

public ImmutableList<Version> Versions
{
get
{
return versions.Value;
}
set
{
versions.Value = value;
}
}
}

As you can see, History can only have its Versions property set once, which is all you need when XML deserializing. You'll also only be able to add to a Version - never remove or replace.

public class Version
{
private readonly Immutable<VersionInfo> versionInfo = new Immutable<VersionInfo>();

private readonly Immutable<Commit> commit = new Immutable<Commit>();

private readonly Immutable<Config> configuration = new Immutable<Config>();

public VersionInfo VersionInfo
{
get
{
return this.versionInfo.Value;
}
set
{
this.versionInfo.Value = value;
}
}

public Commit Commit
{
get
{
return this.commit.Value;
}
set
{
this.commit.Value = value;
}
}

public Configuration Configuration
{
get
{
return this.configuration.Value;
}
set
{
this.configuration.Value = value;
}
}
}

One downside to this is I can't use my beloved automatic properties for dumb data object like these. But, I suppose they're not quite as dumb now, are they?