BinaryFormatter functionality reference
The BinaryFormatter was first introduced with the initial release of .NET Framework in 2002. To understand how to replace usage of BinaryFormatter, it helps to know how BinaryFormatter works.
BinaryFormatter can serialize any instance of any type that's annotated with [Serializable]
or implements the ISerializable interface.
Member names
In most common scenario, the type is annotated with [Serializable]
and the serializer uses reflection to serialize all fields (both public and non-public) except those that are annotated with [NonSerialized]
. By default, the serialized member names will match the type's field names. This historically led to incompatibilities when even private fields are renamed on [Serializable]
types. During migrations away from BinaryFormatter, it becomes necessary to understand how serialized field names were handled and overridden.
C# auto properties
For C# automatically implemented properties ({ get; set; }
), BinaryFormatter will serialize the backing fields that are generated by the C# compiler, not the properties. The names of those serialized backing fields contain illegal C# characters and cannot be controlled. A C# decompiler (such as https://sharplab.io/ or ILSpy) can demonstrate how C# auto properties are presented to the runtime.
[Serializable]
internal class PropertySample
{
public string Name { get; set; }
}
The previous class is translated by the C# compiler to:
[Serializable]
internal class PropertySample
{
private string <Name>k__BackingField;
public string Name
{
get
{
return <Name>k__BackingField;
}
set
{
<Name>k__BackingField = value;
}
}
}
In this case, <Name>k__BackingField
is the name of the member that BinaryFormatter
uses in the serialized payload. It's not possible to use nameof
or any other C# operator to get this name.
The ISerializable interface comes with GetObjectData method that allows the users to control the names, by using one of the AddValue methods.
// Note lack of any special attribute.
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Name", this.Name);
}
If such customization has been applied, the information needs to be provided during deserialization as well. That's possible by using the serialization constructor, where all values are read from SerializationInfo with one of the Get
methods it provides.
private PropertySample(SerializationInfo info, StreamingContext context)
{
this.Name = info.GetString("Name");
}
Note
The nameof
operator was purposely not used here, as the payload can be persisted and the property can get renamed at a later time. So even if it gets renamed (say to FirstName
because you decide to also introduce a LastName
property), to remain backward compatible, the serialization should still use the old name that could have been persisted somewhere.
Serialization binder
It's recommended to use SerializationBinder to control class loading and mandate what class to load. That minimizes security vulnerabilities (so only allowed types get loaded, even if the attacker modifies the payload to deserialize and load something else).
Using this type requires inheriting from it and overriding the BindToType method.
Ideally the list of serializable types is closed set because it means you know which types can be instantiated which will help reduce security vulnerabilities.