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# auto-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.