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.[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");}
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.public object Deserialize(Stream sourceStream) { BinaryReader binaryDeserializationStream = new BinaryReader(sourceStream); ObjectReader reader = new ObjectReader(typeInformationStore); object result = reader.Deserialize(binaryDeserializationStream); return result;}
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.internal class ObjectReader { public ObjectReader(AssemblyTypeInformation typeInformationStore) { this.typeInformationStore = typeInformationStore; } public object Deserialize(BinaryReader deserializationStream) { return ReadSerializedObject(deserializationStream); }
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 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;}
ReadFields is a simple enough method. It just loops through each field in the type, and calls the ReadObject() method on it.private void ReadFields(BinaryReader deserializationStream, Type graphType, object instance) { foreach (FieldInfo field in typeInformationStore.GetFieldsFromType(graphType)) { ReadObject(deserializationStream, field.FieldType, instance, field); }}
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 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);}
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.private static object ReadPrimitive(BinaryReader deserializationStream, MethodInfo deserializationMethod) { return deserializationMethod.Invoke(deserializationStream, null);}
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.