BinaryFormatter
Library
BinaryFormatter - Binary Serialization Library for .NET
Overview
BinaryFormatter is a binary serialization mechanism for .NET objects, converting objects to binary format for storage or transmission. Originally designed for easy and transparent object serialization, it automatically handles complex object graphs and .NET-specific type information. However, Microsoft now strongly recommends against its use due to security vulnerabilities and encourages migration to safer alternatives.
Details
BinaryFormatter was a cornerstone of .NET serialization for nearly two decades, providing seamless binary serialization for any .NET type. It automatically handles object references, preserving graph structures without duplication, and supports serialization of private fields and complex inheritance hierarchies. The binary format is compact and optimized for .NET-to-.NET communication.
However, critical security issues led to its deprecation. BinaryFormatter allows arbitrary type instantiation during deserialization, creating remote code execution vulnerabilities. This "deserialization of untrusted data" attack vector has been exploited in numerous security breaches. The format is also tightly coupled to .NET type systems, making it unsuitable for cross-platform scenarios.
Microsoft deprecated BinaryFormatter in .NET 5.0 and plans complete removal in future versions. The official guidance recommends using JSON serialization (System.Text.Json), XML serialization, or protocol buffers for new development. For binary scenarios requiring .NET-specific features, alternatives like MessagePack or custom binary protocols are suggested.
Advantages and Disadvantages
Advantages
- Automatic Serialization: Handles complex object graphs without manual configuration
- Full Type Fidelity: Preserves complete .NET type information and private fields
- Reference Preservation: Maintains object identity and handles circular references
- Compact Format: Binary representation is more space-efficient than text formats
- Built-in Support: Integrated into .NET Framework with no external dependencies
- Version Tolerance: Supports some degree of type versioning through surrogates
Disadvantages
- Security Vulnerabilities: Susceptible to remote code execution attacks
- Platform Dependency: Binary format is .NET-specific, not interoperable
- Deprecation: Officially deprecated and will be removed from future .NET versions
- Type Coupling: Requires exact type matching between serialization endpoints
- No Human Readability: Binary format cannot be inspected or edited manually
- Breaking Changes: Type changes can break deserialization compatibility
References
- BinaryFormatter Security Guide - Microsoft Docs
- Migrating Away from BinaryFormatter
- .NET Serialization Guidance
- BinaryFormatter Obsoletion Strategy
Code Examples
1. Basic Serialization (Legacy Example)
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
[Serializable]
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
private string ssn; // Private fields are also serialized
public Person(string name, int age, string ssn)
{
Name = name;
Age = age;
this.ssn = ssn;
}
}
// WARNING: This code is for educational purposes only
// Do not use BinaryFormatter in production
#pragma warning disable SYSLIB0011 // Type or member is obsolete
class Program
{
static void Main()
{
var person = new Person("John Doe", 30, "123-45-6789");
// Serialize
using (var stream = new FileStream("person.dat", FileMode.Create))
{
var formatter = new BinaryFormatter();
formatter.Serialize(stream, person);
}
// Deserialize
using (var stream = new FileStream("person.dat", FileMode.Open))
{
var formatter = new BinaryFormatter();
var deserialized = (Person)formatter.Deserialize(stream);
Console.WriteLine($"Name: {deserialized.Name}, Age: {deserialized.Age}");
}
}
}
#pragma warning restore SYSLIB0011
2. Migrating to System.Text.Json
using System;
using System.IO;
using System.Text.Json;
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
// Note: Private fields require special handling in System.Text.Json
}
class ModernSerialization
{
static void Main()
{
var person = new Person { Name = "John Doe", Age = 30 };
// Serialize to JSON
var options = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNameCaseInsensitive = true
};
string jsonString = JsonSerializer.Serialize(person, options);
File.WriteAllText("person.json", jsonString);
// Deserialize from JSON
string readJson = File.ReadAllText("person.json");
var deserialized = JsonSerializer.Deserialize<Person>(readJson, options);
Console.WriteLine($"Name: {deserialized.Name}, Age: {deserialized.Age}");
}
}
3. Object Graph with References (Legacy)
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
[Serializable]
public class Department
{
public string Name { get; set; }
public List<Employee> Employees { get; set; } = new List<Employee>();
}
[Serializable]
public class Employee
{
public string Name { get; set; }
public Department Department { get; set; }
}
#pragma warning disable SYSLIB0011
class ObjectGraphExample
{
static void Main()
{
// Create circular reference
var dept = new Department { Name = "Engineering" };
var emp1 = new Employee { Name = "Alice", Department = dept };
var emp2 = new Employee { Name = "Bob", Department = dept };
dept.Employees.Add(emp1);
dept.Employees.Add(emp2);
// BinaryFormatter handles circular references automatically
using (var stream = new MemoryStream())
{
var formatter = new BinaryFormatter();
formatter.Serialize(stream, dept);
stream.Position = 0;
var deserialized = (Department)formatter.Deserialize(stream);
// Circular references are preserved
Console.WriteLine(deserialized.Employees[0].Department == deserialized); // True
}
}
}
#pragma warning restore SYSLIB0011
4. Custom Serialization with ISerializable
using System;
using System.Runtime.Serialization;
[Serializable]
public class SecureData : ISerializable
{
public string PublicData { get; set; }
private string sensitiveData;
public SecureData(string publicData, string sensitive)
{
PublicData = publicData;
sensitiveData = sensitive;
}
// Custom serialization constructor
protected SecureData(SerializationInfo info, StreamingContext context)
{
PublicData = info.GetString("public");
// Decrypt sensitive data during deserialization
var encrypted = info.GetString("sensitive");
sensitiveData = DecryptData(encrypted);
}
// Custom serialization method
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("public", PublicData);
// Encrypt sensitive data before serialization
info.AddValue("sensitive", EncryptData(sensitiveData));
}
private string EncryptData(string data) => Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(data));
private string DecryptData(string data) => System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(data));
}
5. Serialization Surrogates for Version Compatibility
using System;
using System.Runtime.Serialization;
// Old version of the class
[Serializable]
public class PersonV1
{
public string Name { get; set; }
}
// New version with additional field
public class PersonV2
{
public string Name { get; set; }
public int Age { get; set; }
}
// Surrogate to handle version differences
public class PersonSurrogate : ISerializationSurrogate
{
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
var person = (PersonV2)obj;
info.AddValue("Name", person.Name);
info.AddValue("Age", person.Age);
}
public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
{
var person = (PersonV2)obj;
person.Name = info.GetString("Name");
// Handle missing Age field from old version
try
{
person.Age = info.GetInt32("Age");
}
catch (SerializationException)
{
person.Age = 0; // Default value for old data
}
return person;
}
}
// Usage with surrogate selector
class SurrogateExample
{
static void UseSurrogate()
{
var selector = new SurrogateSelector();
var context = new StreamingContext(StreamingContextStates.All);
selector.AddSurrogate(typeof(PersonV2), context, new PersonSurrogate());
#pragma warning disable SYSLIB0011
var formatter = new BinaryFormatter();
formatter.SurrogateSelector = selector;
#pragma warning restore SYSLIB0011
// Now formatter can handle version differences
}
}
6. Safe Alternative Using MessagePack
using MessagePack;
using System;
using System.IO;
[MessagePackObject]
public class Person
{
[Key(0)]
public string Name { get; set; }
[Key(1)]
public int Age { get; set; }
[Key(2)]
public DateTime BirthDate { get; set; }
}
class SafeAlternative
{
static void Main()
{
var person = new Person
{
Name = "John Doe",
Age = 30,
BirthDate = new DateTime(1993, 5, 15)
};
// Serialize with MessagePack (safe alternative)
byte[] bytes = MessagePackSerializer.Serialize(person);
File.WriteAllBytes("person.msgpack", bytes);
// Deserialize
byte[] data = File.ReadAllBytes("person.msgpack");
var deserialized = MessagePackSerializer.Deserialize<Person>(data);
Console.WriteLine($"Name: {deserialized.Name}");
Console.WriteLine($"Age: {deserialized.Age}");
Console.WriteLine($"Birth Date: {deserialized.BirthDate:yyyy-MM-dd}");
// MessagePack advantages:
// - No arbitrary type instantiation
// - Cross-platform compatible
// - Smaller binary size
// - Faster serialization
// - Still supports complex types
}
}