On .Net Serialization, Part 10

Published April 04, 2005
Advertisement
Well, if you ran your tests yesterday, after all of that work, then you will have recieved a nasty shock, one of them is failing! In fact, it's failing with a serialization exception, reading: TestCase 'Kent.Serialization.Network.Tests.NetworkFormatterTests.TestSerialization' failed: System.Runtime.Serialization.SerializationException : Type (System.String) is not from assembly: Kent.Serialization.Network, Version=1.0.1920.32023, Culture=neutral, PublicKeyToken=null. Doing some debuging we note that we have the following line, in the ObjectWriter.WriteObject() method: MethodInfo serializationMethod = typeof(BinaryWriter).GetType().GetMethod("Write", new Type[] { graphType });. Note the bolded part, that should NOT be there. Why? well, quite simply: We are fetching the Type of the BinaryWriter, then asking for the Type of that Type, which is a Type, and then asking it for a Write method. Obviously, the Type class does not have a write method, hence our exception.

[Test]public void TestDeserialization() {	MemoryStream testStream = new MemoryStream();	NetworkFormatter formatter = GetFormatter();	formatter.Serialize(testStream, new SerializableObject(3, "Washu"));	testStream.Position = 0;	object o = formatter.Deserialize(testStream);	SerializableObject so = (SerializableObject)o;	Assert.AreEqual(so.Value, 3);	Assert.AreEqual(so.Name, "Washu");}
Alright, time to get on with the next part, which is writing the deserialization part of our NetworkFormatter class. Slapping together a quick test, we get the code we see in the code on the right. The idea is simple enough really, if we serialize a SerializableObject to the stream, then we should be able to deserialize it from that stream (note that we need to reset the position pointer to the start of the stream, otherwise we will be attempting to read past the end of the stream.) Then we assert that the values in the structure are what we would expect them to be (aka, the original values). As usual, compiling this will fail so you need to write up a simple Deserialization() method in our NetworkFormatter class.

public object Deserialize(Stream sourceStream) {	BinaryReader binaryDeserializationStream = new BinaryReader(sourceStream);	ObjectReader reader = new ObjectReader(typeInformationStore);	object result = reader.Deserialize(binaryDeserializationStream);	return result;}
This matches our Serialize() method pretty closely, it just creates a BinaryReader, an ObjectReader (which we will write), and then calls the Deserialize() method of the ObjectReader class. The method will return an object which will actually be an instance of the serialized type, with all of the fields filled in, that object is then returned back to the callee. One thing to note, both with the Serialize() and Deserialize() methods, is that I do not dispose of the BinaryWriter/BinaryReader. The reasoning behind this is fairly simple: If I disposed of the two streams then the underlying stream would also be closed, thus preventing it's reuse. By not disposing of it, the finalizer will be called, which will call Dispose(), however it will not close the base stream, as that is a managed resource.

internal class ObjectReader {	public ObjectReader(AssemblyTypeInformation typeInformationStore) {		this.typeInformationStore = typeInformationStore;	}	public object Deserialize(BinaryReader deserializationStream) {		return ReadSerializedObject(deserializationStream);	}
As we can see, the constructor and Deserialize() method are deceptively simple. The Deserialize() method simply calls the ReadSerializedObject() and passes the result of that method back to the Serialize() method in the NetworkFormatter class.






private object ReadSerializedObject(BinaryReader deserializationStream) {	int id = deserializationStream.ReadInt32();	Type graphType = typeInformationStore.GetTypeFromId(id);	object instance = graphType.Assembly.CreateInstance(graphType.FullName);	ReadFields(deserializationStream, graphType, instance);	return instance;}
Moving on, in the code on the right we see the ReadSerializedObject method. Quite simply it just gets the ID of the object from the BinaryReader, looks it up using the GetTypeFromId() method in the AssemblyTypeInformation class. It then creates an instance of that type, passing that instance to the ReadFields() method, and finaly it returns the created instance to the callee.

private void ReadFields(BinaryReader deserializationStream, Type graphType, object instance) {	foreach (FieldInfo field in typeInformationStore.GetFieldsFromType(graphType)) {		ReadObject(deserializationStream, field.FieldType, instance, field);	}}
ReadFields is a simple enough method. It just loops through each field in the type, and calls the ReadObject() method on it.




private void ReadObject(BinaryReader deserializationStream, Type graphType, object instance, FieldInfo field) {	object result;	MethodInfo deserializationMethod = typeof(BinaryReader).GetMethod("Read" + graphType.Name);	if (deserializationMethod != null) {		result = ReadPrimitive(deserializationStream, deserializationMethod);	} else {		result = ReadSerializedObject(deserializationStream);	}	field.SetValue(instance, result);}
Here's the real workhorse, it works much like the WriteObject() method in the ObjectWriter class. First it tries to find an appropriate read method inside of the BinaryReader. Then, if the method is found it calls the ReadPrimitive() method, storing the result. If it doesn't find a read method in the BinaryReader then it calls ReadSerializedObject(), which we saw above. Finally, it sets the field value to the object obtained from the previous conditional.

private static object ReadPrimitive(BinaryReader deserializationStream, MethodInfo deserializationMethod) {	return deserializationMethod.Invoke(deserializationStream, null);}
WHAT!!?!?!?!? Another one line method! Yep, it sure is, like I said, sometimes it's required just to keep the code at the same level, and to keep it clean and readable. This just calls the read method we reflected from the BinaryReader class, passing the result back to the callee.

So, that's about it. We've finally got a NetworkFormatter which can easily serialize and deserialize types from an assembly. The use of it is damn simple too. Some things though that you might want to consider for your NetworkFormatter: byte arrays should be length prefixed, this can be done by inheriting from BinaryWriter and BinaryReader and providing your own ReadBytes() and Write() methods. You should use the 7 bit encoded integer methods for the lenght, as to save space. The same is also true of character arrays (not strings). The other thing you should consider figuring out how to handle are null values in objects (this requires exactly two if statements and a mock object to solve in my system). Well, good luck.
0 likes 3 comments

Comments

choffstein
I really wish I had started reading your journal from the beginning. I have 0 idea what is going on...though I find your posts fascinating!
April 04, 2005 09:57 PM
Mushu
You know, nothing is stopping you from reading from the beginning.

(my shitty OS is stopping me from actually implementing any of this. Shame on me [sad])
April 04, 2005 10:25 PM
Saruman
Hopefully tomorrow will be a better day and I will be able to get this implemented with Async sockets :)

Thanks again for the article! :)
April 04, 2005 11:20 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement