What causes an object version change with an object serialized with BinaryFormatter?
-
28-10-2019 - |
Question
Per this question I have an object that I'm serializing with BinaryFormatter
. For various reasons we've implemented a poor man's version handling like this with a try-catch block at the bottom for the fields that are in the newer version but not in the older version:
private void readData(FileStream fs, SymmetricAlgorithm dataKey)
{
CryptoStream cs = null;
try
{
cs = new CryptoStream(fs, dataKey.CreateDecryptor(),
CryptoStreamMode.Read);
BinaryFormatter bf = new BinaryFormatter();
string string1 = (string)bf.Deserialize(cs);
// do stuff with string1
bool bool1 = (bool)bf.Deserialize(cs);
// do stuff with bool1
ushort ushort1 = (ushort)bf.Deserialize(cs);
// do stuff with ushort1
// etc. etc. ...
// this field was added later, so it may not be present
// in the serialized binary data. Check for it, and if
// it's not there, do some default behavior
NewStuffIncludedRecently newStuff = null;
try
{
newStuff = (NewStuffIncludedRecently)bf.Deserialize(cs);
}
catch
{
newStuff = null;
}
_newStuff = newStuff != null ?
new NewStuffIncludedRecently(newStuff) :
new NewStuffIncludedRecently();
}
catch (Exception e)
{
// ...
}
finally
{
// ...
}
}
I stepped through the code on my machine and this seems to work. When I read an old serialized object, the innermost try-catch handles the part that's missing as I ask it to.
When I go to a colleague's machine to try to read an old version of the object, a SerializationException gets thrown at the first Deserialize() call at the top:
Binary stream '220' does not contain a valid BinaryHeader. Possible causes are invalid stream or object version change between serialization and deserialization.
Hence my question: What causes the version of the object to change? When I go back and forth on my box between the two versions of the object (commenting/uncommenting the new fields) there's no problem, but on another person's box the first Deserialize() bombs. I'm not sure even where to start looking, though I did try making the version checking more permissive like so:
bf.AssemblyFormat =
System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple;
Solution
I haven't tried it with the BinaryFormatter (we use SoapFormatter), but the way we approached this problem was to implement manual serialization and deserializaton of our classes so that we could skip the problem properties.
For example, each of our serializable classes contains the following (pardon the VB code; I can convert if needed):
' Serialization constructor
Protected Sub New(ByVal info As SerializationInfo, ByVal context As StreamingContext)
' This only needs to be called if this class inherits from a serializable class
'Call MyBase.New(info, context)
Call DeserializeObject(Me, GetType(ActionBase), info)
End Sub
' Ensure that the other class members that are not part of the NameObjectCollectionBase are serialized
Public Overridable Sub GetObjectData(ByVal info As System.Runtime.Serialization.SerializationInfo, ByVal context As System.Runtime.Serialization.StreamingContext) Implements ISerializable.GetObjectData
' This only needs to be called if this class inherits from a serializable class
'MyBase.GetObjectData(info, context)
Call SerializeObject(Me, GetType(ActionBase), info)
End Sub
Then, the serialization and deserialization is performed in a standard method for all classes:
''' <summary>
''' This method is used to deserialize an object within the serialization constructor.
''' This is used when the caller does not want to explicitly and manually add every property to the
''' SerializationInfo object
''' </summary>
''' <param name="theObject"></param>
''' <param name="theType"></param>
''' <param name="theInfo"></param>
''' <remarks></remarks>
Public Sub DeserializeObject(ByVal theObject As Object, ByVal theType As Type, ByVal theInfo As SerializationInfo)
' Exceptions are handled by the caller
' Manually deserialize these items
With theType
If theInfo.MemberCount > 0 Then
For Each theField As FieldInfo In .GetFields(BindingFlags.DeclaredOnly Or BindingFlags.Instance Or BindingFlags.NonPublic)
Try
' Don't deserialize items that are marked as non-serialized
If Not theField.IsNotSerialized Then
If theField.FieldType.IsEnum Then
Try
theField.SetValue(theObject, System.Enum.Parse(theField.FieldType, CStr(theInfo.GetValue(theField.Name, theField.FieldType))))
Catch
theField.SetValue(theObject, TypeDescriptor.GetConverter(theField.FieldType).ConvertFrom(theInfo.GetInt32(theField.Name)))
End Try
Else
theField.SetValue(theObject, theInfo.GetValue(theField.Name, theField.FieldType))
End If
End If
Catch
End Try
Next
End If
End With
End Sub
''' <summary>
''' This method is used to serialize an object within the GetObjectData serialization method.
''' This is used when the caller does not want to explicitly and manually add every property to the
''' SerializationInfo object
''' </summary>
''' <param name="theObject"></param>
''' <param name="theType"></param>
''' <param name="theInfo"></param>
''' <remarks></remarks>
Public Sub SerializeObject(ByVal theObject As Object, ByVal theType As Type, ByVal theInfo As SerializationInfo)
' Exceptions are handled by the caller
' Manually serialize these items
With theType
For Each theField As FieldInfo In .GetFields(BindingFlags.DeclaredOnly Or BindingFlags.Instance Or BindingFlags.NonPublic)
' Don't serialize items that are marked as non-serialized
If Not theField.IsNotSerialized Then
theInfo.AddValue(theField.Name, theField.GetValue(theObject))
End If
Next
End With
End Sub