BinarySerializationFormatter Class
Serializes and deserializes objects in binary format.
See the
online help for a more detailed description with examples.
Namespace: KGySoft.Serialization.BinaryAssembly: KGySoft.CoreLibraries (in KGySoft.CoreLibraries.dll) Version: 9.0.0
public sealed class BinarySerializationFormatter : IFormatter
Public NotInheritable Class BinarySerializationFormatter
Implements IFormatter
public ref class BinarySerializationFormatter sealed : IFormatter
[<SealedAttribute>]
type BinarySerializationFormatter =
class
interface IFormatter
end
- Inheritance
- Object BinarySerializationFormatter
- Implements
- IFormatter
The fundamental goal of binary serialization is to store the bitwise content of an object, hence in general case (when custom types
are involved) it relies on field values, including private ones that can change from version to version. Therefore, binary serialization is recommended
only if your serialized objects purely consist of natively supported types (see them below). If you need to serialize custom types, then it is recommended
to do it for in-process purposes only, such as deep cloning or undo/redo, etc. If it is known that a type will be deserialized in another environment and
it can be completely restored by its public members, then a text-based serialization (see also
XmlSerializer) can be a better choice.
If the serialization stream may come from an untrusted source (e.g. remote service, file or database) make sure you enable
the SafeMode option. It prevents loading assemblies during the deserialization, denies resolving unexpected natively not supported types by name,
does not allow instantiating natively not supported types that are not serializable, and guards against some attacks that may cause OutOfMemoryException.
When using SafeMode all of the natively not supported types, whose assembly qualified names are
stored in the serialization stream must be explicitly declared as expected types in the deserialization methods, including apparently innocent types such as enums.
Please also note that in safe mode some system types are forbidden to use even if they are serializable and are specified as expected types.
In the .NET Framework there are some serializable types in the fundamental core assemblies that can be exploited for several attacks (causing unresponsiveness,
StackOverflowException or even files to be deleted). Starting with .NET Core these types are not serializable anymore and some of them have been moved to separate NuGet packages anyway,
but the BinaryFormatter class in the .NET Framework is still vulnerable against such attacks. When using the SafeMode flag,
the BinarySerializationFormatter is protected against the known security issues on all platforms but of course it cannot guard you against every potentially harmful type if
you explicitly specify them as expected types in the deserialization methods.
Please also note that the IFormatter infrastructure has other security flaws as well but some of these can be reduced by the serializable types themselves.
Most serializable types do not validate the incoming data. All serializable types that can have an invalid state regarding the field values
should implement ISerializable and should throw a SerializationException from their serialization constructor if validation fails.
The BinarySerializationFormatter wraps every other exception thrown by the constructor into a SerializationException.
Not even the core .NET types have such validation, which is one reason why this library supports so many types natively. And for custom types
see the example at the Example: How to implement a custom serializable type section.
Starting with version 8.0.0 in safe mode the Binder property can only be or a ForwardedTypesSerializationBinder instance if you set
its SafeMode to . Furthermore, in safe mode it is not allowed to set any surrogate selectors in the SurrogateSelector property.
Please note that this library also contains a sort of serialization binders and surrogates, most of them have their own SafeMode property
(e.g. WeakAssemblySerializationBinder, CustomSerializerSurrogateSelector or NameInvariantSurrogateSelector), still,
not even they are allowed to be used when the SafeMode option is enabled. It's because their safe mode just provide some not too strict general protection,
instead of being able to filter a specific set of predefined types.
If you must disable SafeMode for some reason, then use binary serialization in-process only, or apply some cryptographically secure encryption
to the serialization stream.
BinarySerializationFormatter aims to serialize objects effectively where the serialized data is almost always more compact than the results produced by the BinaryFormatter class.
BinarySerializationFormatter natively supports all the primitive types and a sort of other simple types, arrays, generic and non-generic collections.
The following example demonstrates the length difference produced by the
BinarySerializationFormatter and
BinaryFormatter classes. Feel free to change the generated type.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using KGySoft.CoreLibraries;
using KGySoft.Serialization.Binary;
public static class Example
{
public static void Main()
{
IFormatter formatter;
// feel free to change the type in NextObject<>
var instance = ThreadSafeRandom.Instance.NextObject<Dictionary<int, List<string>>>();
Console.WriteLine("Generated object: " + Dump(instance));
using (var ms = new MemoryStream())
{
// serializing by KGy SOFT version:
formatter = new BinarySerializationFormatter();
formatter.Serialize(ms, instance);
// deserialization:
ms.Position = 0L;
object deserialized = formatter.Deserialize(ms);
Console.WriteLine("Deserialized object " + Dump(deserialized));
Console.WriteLine("Length by BinarySerializationFormatter: " + ms.Length);
}
using (var ms = new MemoryStream())
{
// serializing by System version:
formatter = new BinaryFormatter();
formatter.Serialize(ms, instance);
Console.WriteLine("Length by BinaryFormatter: " + ms.Length);
}
}
private static string Dump(object o)
{
if (o == null)
return "<null>";
if (o is IConvertible convertible)
return convertible.ToString(CultureInfo.InvariantCulture);
if (o is IEnumerable enumerable)
return $"[{enumerable.Cast<object>().Select(Dump).Join(", ")}]";
return $"{{{o.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Select(p => $"{p.Name} = {Dump(p.GetValue(o))}").Join(", ")}}}";
}
}
// This code example produces a similar output to this one:
// Generated object: [{Key = 1418272504, Value = [aqez]}, {Key = 552276491, Value = [addejibude, yifefa]}]
// Deserialized object [{Key = 1418272504, Value = [aqez]}, {Key = 552276491, Value = [addejibude, yifefa]}]
// Length by BinarySerializationFormatter: 50
// Length by BinaryFormatter: 2217
Serialization of natively supported types produce an especially compact result because these types are not serialized by traversing and storing the fields of the object graph recursively.
This means not just better performance and improved security for these types but also prevents compatibility issues between different platforms because these types are not encoded by assembly identity and type name.
Serialization of natively not supported types can be somewhat slower for the first time than by
BinaryFormatter but the serialized result is almost always shorter than the one by
BinaryFormatter,
especially when generic types are involved.
For the most compact result and to avoid using the obsoleted serialization infrastructure in .NET 8.0 and above it is recommended to implement the
IBinarySerializable interface.
See the
Remarks section of the
IBinarySerializable interface for details and examples.
The following example shows how to apply validation for a serializable class that does not implement ISerializable so it will be serialized by its fields.
using System;
using System.Runtime.Serialization;
[Serializable]
public class Example1
{
public int IntProp { get; set; }
public string StringProp { get; set; }
// Regular validation when constructing the class normally
public Example1(int intValue, string stringValue)
{
if (intValue <= 0)
throw new ArgumentOutOfRangeException(nameof(intValue));
if (stringValue == null)
throw new ArgumentNullException(nameof(stringValue));
if (stringValue.Length == 0)
throw new ArgumentException("Value is empty", nameof(stringValue));
IntProp = intValue;
StringProp = stringValue;
}
// The validation for deserialization. This is executed once the instance is deserialized.
// Another way for post-validation if you implement the IDeserializationCallback interface.
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
if (IntProp <= 0 || String.IsNullOrEmpty(StringProp))
throw new SerializationException("Invalid serialization stream");
}
}
The example above may not be applicable if you need to validate the values in advance just like in the constructor.
In that case you can implement the ISerializable interface and do the validation in the special serialization constructor:
using System;
using System.Runtime.Serialization;
[Serializable]
public class Example2 : ISerializable
{
public int IntProp { get; set; }
public string StringProp { get; set; }
// Regular validation when constructing the class normally
public Example2(int intValue, string stringValue)
{
if (intValue <= 0)
throw new ArgumentOutOfRangeException(nameof(intValue));
if (stringValue == null)
throw new ArgumentNullException(nameof(stringValue));
if (stringValue.Length == 0)
throw new ArgumentException("Value is empty", nameof(stringValue));
IntProp = intValue;
StringProp = stringValue;
}
// Deserialization constructor with validation
private Example2(SerializationInfo info, StreamingContext context)
{
// GetValue throws SerializationException internally if the specified name is not found
if (info.GetValue(nameof(IntProp), typeof(int)) is not int i || i <= 0)
throw new SerializationException("IntProp is invalid");
if (info.GetValue(nameof(StringProp), typeof(string)) is not string s || s.Length == 0)
throw new SerializationException("StringProp is invalid");
IntProp = i;
StringProp = s;
}
// Serialization
void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue(nameof(IntProp), IntProp);
info.AddValue(nameof(StringProp), StringProp);
}
}
The examples above are compatible also with
BinaryFormatter.
Still, it is not recommended to use
BinaryFormatter because it is vulnerable at multiple levels.
See the security notes at the top of the page for more details.
Even if a type is not marked to be serializable by the SerializableAttribute, then you can use the RecursiveSerializationAsFallback
option to force its serialization. Please note though that such types might not be able to be deserialized when SafeMode is enabled. Alternatively, you can implement
the IBinarySerializable interface, which can be used to produce a more compact custom serialization than the one provided by implementing the ISerializable interface.
As BinarySerializationFormatter implements IFormatter it fully supports SerializationBinder and ISurrogateSelector implementations,
though for security they are mainly disabled in safe mode. See security notes above for more details.
A SerializationBinder can be used to deserialize types of unmatching assembly identity and to specify custom type-name mappings in both directions.
Though BinarySerializationFormatter automatically handles TypeForwardedToAttribute and TypeForwardedFromAttribute (see also
the IgnoreTypeForwardedFromAttribute option), you can use also the ForwardedTypesSerializationBinder, especially for types without a defined forwarding.
The WeakAssemblySerializationBinder can also be general solution if you need to ignore the assembly version or the complete assembly identity on resolving a type.
If the name of the type has also been changed, then the CustomSerializationBinder can be used.
See also the Remarks section of the Binder property for more details.
An ISurrogateSelector can be used to customize serialization and deserialization. It can be used for types that cannot be handled anyway for some reason.
For example, if you need to deserialize types, whose field names have been renamed you can use the CustomSerializerSurrogateSelector.
Or, if the produced raw data has to be compatible with the obfuscated version of a type, then it can be achieved by the NameInvariantSurrogateSelector.
There are three ways to serialize/deserialize an object. To serialize into a byte array use the Serialize method.
Its result can be deserialized by the Deserialize methods.
Additionally, you can use the SerializeToStream/DeserializeFromStream methods to dump/read the result
to and from a Stream, and the the SerializeByWriter/DeserializeByReader
methods to use specific BinaryWriter and BinaryReader instances for serialization and deserialization, respectively.
In .NET Framework almost every type was serializable by
BinaryFormatter. In .NET Core this principle has been
radically changed. Many types are just simply not marked by the
SerializableAttribute anymore (e.g.
MemoryStream,
CultureInfo,
Encoding), whereas some types, which still implement
ISerializable now
throw a
PlatformNotSupportedException from their
GetObjectData.
Binary serialization of these types is not recommended anymore. If you still must serialize or deserialize such types
see the
Remarks section of the
CustomSerializerSurrogateSelector for more details.
Following types are natively supported. When these types are serialized, no type name is stored and there is no recursive traversal of the fields:
- Serializing Enum types will end up in a longer result raw data than serializing their numeric value, though the result will be still shorter than the one produced by BinaryFormatter.
Please note that when deserializing in safe mode enums must be specified among the expected custom types. It's because though enums themselves are harmless, their type must be resolved just like any other type
so a manipulated serialization stream may contain some altered type identity that could be resolved to a harmful type.
- If a KeyValuePairTKey, TValue contains natively not supported type arguments or DictionaryEntry has natively not supported keys an values,
then for them recursive serialization may occur. If they contain non-serializable types, then the RecursiveSerializationAsFallback option should be enabled.
The same applies also for Tuple and ValueTuple types.
Following generic collections are natively supported. When their generic arguments are one of the simple types or other supported collections, then no recursive traversal of the fields occurs:
- Array of element types above or compound of other supported collections
- ListT
- CircularListT
- LinkedListT
- HashSetT
- QueueT
- StackT
- ThreadSafeHashSetT
- ArraySegmentT
- ArraySectionT
- Array2DT
- Array3DT
- CastArrayTFrom, TTo
- CastArray2DTFrom, TTo
- CastArray3DTFrom, TTo
- SortedSetT (in .NET Framework 4.0 and above)
- ConcurrentBagT (in .NET Framework 4.0 and above)
- ConcurrentQueueT (in .NET Framework 4.0 and above)
- ConcurrentStackT (in .NET Framework 4.0 and above)
- DictionaryTKey, TValue
- SortedListTKey, TValue
- SortedDictionaryTKey, TValue
- CircularSortedListTKey, TValue
- AllowNullDictionaryTKey, TValue
- ConcurrentDictionaryTKey, TValue (in .NET Framework 4.0 and above)
- ImmutableArrayT (in .NET Core 2.0 and above)
- ImmutableArrayTBuilder (in .NET Core 2.0 and above)
- ImmutableListT (in .NET Core 2.0 and above)
- ImmutableListTBuilder (in .NET Core 2.0 and above)
- ImmutableHashSetT (in .NET Core 2.0 and above)
- ImmutableHashSetTBuilder (in .NET Core 2.0 and above)
- ImmutableSortedSetT (in .NET Core 2.0 and above)
- ImmutableSortedSetTBuilder (in .NET Core 2.0 and above)
- ImmutableQueueT (in .NET Core 2.0 and above)
- ImmutableStackT (in .NET Core 2.0 and above)
- ImmutableDictionaryTKey, TValue (in .NET Core 2.0 and above)
- ImmutableDictionaryTKey, TValueBuilder (in .NET Core 2.0 and above)
- ImmutableSortedDictionaryTKey, TValue (in .NET Core 2.0 and above)
- ImmutableSortedDictionaryTKey, TValueBuilder (in .NET Core 2.0 and above)
- MemoryT (in .NET Core 2.1 and above)
- ReadOnlyMemoryT (in .NET Core 2.1 and above)
- Vector64T (in .NET Core 3.0 and above)
- Vector128T (in .NET Core 3.0 and above)
- Vector256T (in .NET Core 3.0 and above)
- Vector512T (in .NET 8.0 and above)
- FrozenSetT (in .NET 8.0 and above)
- FrozenDictionaryTKey, TValue (in .NET 8.0 and above)
- OrderedDictionaryTKey, TValue (in .NET 9.0 and above)
- Arrays can be single- and multidimensional, jagged (array of arrays) and don't have to be zero index-based. Arrays and other generic collections can be nested.
- If a collection uses an unsupported IEqualityComparerT or IComparerT implementation, then it is possible that the type cannot be serialized without enabling
RecursiveSerializationAsFallback option, unless the comparer is decorated by SerializableAttribute or implements the IBinarySerializable interface.
- If an Array has Object element type or Object is used in generic arguments of the collections above and an element is not a natively supported type, then recursive serialization of fields
may occur. For non-serializable types the RecursiveSerializationAsFallback option might be needed to be enabled.
- Even if a generic collection of Object contains natively supported types only, the result will be somewhat longer than in case of a more specific element type.
The shortest result can be achieved by using classes or value types as array base types and generic parameters.
Following non-generic collections are natively supported. When they contain only other natively supported elements, then no recursive traversal of the fields occurs:
BinarySerializationFormatter supports calling methods decorated by OnSerializingAttribute, OnSerializedAttribute,
OnDeserializingAttribute and OnDeserializedAttribute as well as calling IDeserializationCallback.OnDeserialization method.
Attributes should be used on methods that have a single StreamingContext parameter.
Please note that if a value type was serialized by the
CompactSerializationOfStructures option, then the method of
OnDeserializingAttribute can be invoked
only after restoring the whole content so fields will be already restored.
Deserialize(Byte, IEnumerableType) |
Deserializes a byte array into an object.
See the Remarks section of the DeserializeT(Byte, Int32, Type) overload for details.
|
Deserialize(Byte, Int32) |
Deserializes the specified part of a byte array into an object. If SafeMode is enabled
in Options and rawData contains natively not supported types by name, then you should use
the other overloads to specify the expected types.
See the Remarks section of the DeserializeT(Byte, Int32, Type) overload for details.
|
Deserialize(Byte, Type) |
Deserializes a byte array into an object.
See the Remarks section of the DeserializeT(Byte, Int32, Type) overload for details.
|
Deserialize(Byte, Int32, IEnumerableType) |
Deserializes the specified part of a byte array into an object.
See the Remarks section of the DeserializeT(Byte, Int32, Type) overload for details.
|
Deserialize(Byte, Int32, Type) |
Deserializes the specified part of a byte array into an object.
See the Remarks section of the DeserializeT(Byte, Int32, Type) overload for details.
|
DeserializeT(Byte, IEnumerableType) |
Deserializes a byte array into an instance of T.
See the Remarks section of the DeserializeT(Byte, Int32, Type) overload for details.
|
DeserializeT(Byte, Type) |
Deserializes a byte array into an instance of T.
See the Remarks section of the DeserializeT(Byte, Int32, Type) overload for details.
|
DeserializeT(Byte, Int32, IEnumerableType) |
Deserializes the specified part of a byte array into an instance of T.
See the Remarks section of the DeserializeT(Byte, Int32, Type) overload for details.
|
DeserializeT(Byte, Int32, Type) |
Deserializes the specified part of a byte array into an instance of T.
|
DeserializeByReader(BinaryReader) |
Deserializes the content of a serialization stream wrapped by the specified reader from its current position into an object.
If SafeMode is enabled in Options and the stream
contains natively not supported types by name, then you should use the other overloads to specify the expected types.
See the Remarks section of the DeserializeByReaderT(BinaryReader, Type) overload for details.
|
DeserializeByReader(BinaryReader, IEnumerableType) |
Deserializes the content of a serialization stream wrapped by the specified reader from its current position into an object.
See the Remarks section of the DeserializeByReaderT(BinaryReader, Type) overload for details.
|
DeserializeByReader(BinaryReader, Type) |
Deserializes the content of a serialization stream wrapped by the specified reader from its current position into an object.
See the Remarks section of the DeserializeByReaderT(BinaryReader, Type) overload for details.
|
DeserializeByReaderT(BinaryReader, IEnumerableType) |
Deserializes the content of a serialization stream wrapped by the specified reader from its current position
into an instance of T.
See the Remarks section of the DeserializeByReaderT(BinaryReader, Type) overload for details.
|
DeserializeByReaderT(BinaryReader, Type) |
Deserializes the content of a serialization stream wrapped by the specified reader from its current position
into an instance of T.
|
DeserializeFromStream(Stream) |
Deserializes the content of the specified serialization stream from its current position into an object.
If SafeMode is enabled in Options and stream
contains natively not supported types by name, then you should use the other overloads to specify the expected types.
See the Remarks section of the DeserializeFromStreamT(Stream, Type) overload for details.
|
DeserializeFromStream(Stream, IEnumerableType) |
Deserializes the content of the specified serialization stream from its current position into an object.
See the Remarks section of the DeserializeFromStreamT(Stream, Type) overload for details.
|
DeserializeFromStream(Stream, Type) |
Deserializes the content of the specified serialization stream from its current position into an object.
See the Remarks section of the DeserializeFromStreamT(Stream, Type) overload for details.
|
DeserializeFromStreamT(Stream, IEnumerableType) |
Deserializes the content of the specified serialization stream from its current position into an instance of T.
See the Remarks section of the DeserializeFromStreamT(Stream, Type) overload for details.
|
DeserializeFromStreamT(Stream, Type) |
Deserializes the content of the specified serialization stream from its current position into an instance of T.
|
Serialize |
Serializes an object into a byte array.
|
SerializeByWriter |
Serializes the given data by using the provided writer.
|
SerializeToStream |
Serializes the given data into a stream.
|