A replacement for BinaryFormatter in .NET 8

In the old .NET Framework days, you could use the BinaryFormatter class to serialize and deserialize objects. This can be convenient for cloning or storing some session states. As the BinaryFormatter has some serious security concerns, the .NET team marked it as obsolete (as error) in .NET 7 and onwards.

Update: BinaryFormatter will be removed in .NET 9. Read the announcement. Read the amendment from myself (15th of April 2024).

The old way

Imagine we have an object or list that has an object like this:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public List<string> ProgrammingKeywords { get; set; } = [];
}

We can serialize and deserialize like this:

var formatter = new BinaryFormatter();
using var stream = new MemoryStream();
formatter.Serialize(stream, People);
stream.Position = 0; // Needed as the stream is at the end after serialization
return (List<Person>)formatter.Deserialize(stream);

Okay. That will not compile with .NET 7 and onwards as it will show an error.

SYSLIB0011: BinaryFormatter serialization is obsolete and should not be used. See https://aka.ms/binaryformatter for more information.

You can add the following property to your project file to make it compile again:

<EnableUnsafeBinaryFormatterSerialization>true</EnableUnsafeBinaryFormatterSerialization>

Again this is not recommended! Here is more about the flag and how it is handled.

The new way

There are many ways you can try to tackle that! The obvious choice is either System.Text.Json or Newtonsoft.Json. So we are serializing and deserializing into a JSON object. There are differences between those two. For a complete guide, check the official documentation.

System.Text.Json

var json = JsonSerializer.Serialize(People);
return JsonSerializer.Deserialize<List<Person>>(json);

System.Text.Json as UTF8 Bytes

Thanks to @VahidN for bringing up, that you could also use System.Text.Json to serialize directly into a byte stream:

var bytes = JsonSerializer.SerializeToUtf8Bytes(People);
return JsonSerializer.Deserialize<List<Person>>(bytes);

Newtonsoft.Json

var json = JsonConvert.SerializeObject(People);
return JsonConvert.DeserializeObject<List<Person>>(json);

The problem with the JSON approach is that it is not as fast, and it has way more limitations (for example, having cycles inside an object leads to exceptions with System.Text.Json).

For me, performance was crucial in my latest project, where I was migrating from .NET Framework 4.8 to .NET 8. The call was almost slower with JSON than with BinaryFormatter in .NET Framework 4.8. So, I had to find another solution.

MessagePack

Another option is to use MessagePack. It also utilizes binary serialization and is way faster than JSON. It is also more compact than JSON. The downside is that it is not human-readable.

Normally you have to annotate your types with an attribute to make it work. I did not want to do that as I am not under control of the types I want to serialize. That is where MessagePack can help you out:

var bytes = MessagePackSerializer.Serialize(People, ContractlessStandardResolver.Options);
return MessagePackSerializer.Deserialize<List<Person>>(bytes, ContractlessStandardResolver.Options);

The ContractlessStandardResolver.Options is the important part here. It will serialize and deserialize your object without the need of any attributes.

And I ended up using exactly that as a replacement for BinaryFormatter. The next one is even faster but needs more setup! And that is why, I didn't show approaches like protobuf-net or similar. They all need some kind of setup to make it work.

MemoryPack

I love source generators! And MemoryPack is using exactly that! To make it work, you have to add a marker attribute to the class you want to serialize and deserialize. When you compile your code, the Serialize and Deserialize methods will be generated for you.

[MemoryPackable]
public partial class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public List<string> ProgrammingKeywords { get; set; } = [];
}

Now we can use it:

var bytes = MemoryPackSerializer.Serialize(People);
return MemoryPackSerializer.Deserialize<List<Person>>(bytes);

Benchmark

Here the whole code

// As Benchmarks are hosted and run by BenchmarkDotNet, we need to add the flag to its execution
var config =
    DefaultConfig.Instance.With(Job.Default.WithArguments([
        new MsBuildArgument("/p:EnableUnsafeBinaryFormatterSerialization=true")
    ]));
BenchmarkRunner.Run<Benchmarks>(config);

[MemoryDiagnoser]
public class Benchmarks
{
    private static readonly List<Person> People =
    [ new Person { Name = "John", Age = 20, ProgrammingKeywords = [ "C#", "F#" ] },
        new Person { Name = "Mary", Age = 25, ProgrammingKeywords = [ "C#", "VB.NET" ] },
        new Person { Name = "Bob", Age = 30, ProgrammingKeywords = [ "C#", "C++" ] },
        new Person { Name = "Alice", Age = 35, ProgrammingKeywords = [ "C#", "Java" ] },
        new Person { Name = "Mark", Age = 40, ProgrammingKeywords = [ "C#", "JavaScript" ] },
        new Person { Name = "Carol", Age = 45, ProgrammingKeywords = [ "C#", "Python" ] },
        new Person { Name = "Peter", Age = 50, ProgrammingKeywords = [ "C#", "Go" ] },
        new Person { Name = "Alex", Age = 55, ProgrammingKeywords = [ "C#", "Ruby" ] },
        new Person { Name = "Simon", Age = 60, ProgrammingKeywords = [ "C#", "PHP" ] },
        new Person { Name = "Jane", Age = 65, ProgrammingKeywords = [ "C#", "Perl" ] } ];
    
    // We have to create a new list so that the "partial" class generated by MemoryPack
    // does not interfer with other Serializers
    private static readonly List<PersonForMemoryPack> PeopleMemoryPack =
    [ new PersonForMemoryPack { Name = "John", Age = 20, ProgrammingKeywords = [ "C#", "F#" ] },
        new PersonForMemoryPack { Name = "Mary", Age = 25, ProgrammingKeywords = [ "C#", "VB.NET" ] },
        new PersonForMemoryPack { Name = "Bob", Age = 30, ProgrammingKeywords = [ "C#", "C++" ] },
        new PersonForMemoryPack { Name = "Alice", Age = 35, ProgrammingKeywords = [ "C#", "Java" ] },
        new PersonForMemoryPack { Name = "Mark", Age = 40, ProgrammingKeywords = [ "C#", "JavaScript" ] },
        new PersonForMemoryPack { Name = "Carol", Age = 45, ProgrammingKeywords = [ "C#", "Python" ] },
        new PersonForMemoryPack { Name = "Peter", Age = 50, ProgrammingKeywords = [ "C#", "Go" ] },
        new PersonForMemoryPack { Name = "Alex", Age = 55, ProgrammingKeywords = [ "C#", "Ruby" ] },
        new PersonForMemoryPack { Name = "Simon", Age = 60, ProgrammingKeywords = [ "C#", "PHP" ] },
        new PersonForMemoryPack { Name = "Jane", Age = 65, ProgrammingKeywords = [ "C#", "Perl" ] } ];
    
    [Benchmark(Baseline = true)]
    public List<Person> SerializeAndDeserializeBinaryFormatter()
    {
        var formatter = new BinaryFormatter();
        using var stream = new MemoryStream();
        formatter.Serialize(stream, People);
        stream.Position = 0;
        return (List<Person>)formatter.Deserialize(stream);
    }
    
    [Benchmark]
    public List<Person> SerializeAndDeserializeSystemTextJson()
    {
        var json = JsonSerializer.Serialize(People);
        return JsonSerializer.Deserialize<List<Person>>(json);
    }

    [Benchmark]
    public List<Person> SerializeAndDeserializeNewtonsoftJson()
    {
        var json = JsonConvert.SerializeObject(People);
        return JsonConvert.DeserializeObject<List<Person>>(json);
    }
    
    [Benchmark]
    public List<Person> SerializeAndDeserializeMessagePack()
    {
        var bytes = MessagePackSerializer.Serialize(People, ContractlessStandardResolver.Options);
        return MessagePackSerializer.Deserialize<List<Person>>(bytes, ContractlessStandardResolver.Options);
    }
    
    [Benchmark]
    public List<PersonForMemoryPack> SerializeAndDeserializeMemoryPack()
    {
        var bytes = MemoryPackSerializer.Serialize(PeopleMemoryPack);
        return MemoryPackSerializer.Deserialize<List<PersonForMemoryPack>>(bytes);
    }

    [Benchmark]
    public List<Person> SerializeAndDeserializeSystemTextJsonUtf8Bytes()
    {
        var bytes = JsonSerializer.SerializeToUtf8Bytes(People);
        return JsonSerializer.Deserialize<List<Person>>(bytes);
    }
}


[Serializable]
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public List<string> ProgrammingKeywords { get; set; } = [];
}

[MemoryPackable]
public partial class PersonForMemoryPack
{
    public string Name { get; set; }
    public int Age { get; set; }
    public List<string> ProgrammingKeywords { get; set; } = [];
}

Then we get the following results (given exactly the code as above). And of course, take that with a grain of salt. The object might not represent something you need in your domain! Also, you might need one direction more than the other. So you have to decide for yourself what is the best option for you.

BenchmarkDotNet v0.13.12, macOS Sonoma 14.2.1 (23C71) [Darwin 23.2.0]
Apple M2 Pro, 1 CPU, 12 logical and 12 physical cores
.NET SDK 8.0.100
  [Host]     : .NET 8.0.0 (8.0.23.53103), Arm64 RyuJIT AdvSIMD
  Job-ZIWEVL : .NET 8.0.0 (8.0.23.53103), Arm64 RyuJIT AdvSIMD

Arguments=/p:EnableUnsafeBinaryFormatterSerialization=true  

| Method                                         | Mean      | Error     | StdDev    | Ratio | Gen0   | Gen1   | Allocated | Alloc Ratio |
|----------------------------------------------- |----------:|----------:|----------:|------:|-------:|-------:|----------:|------------:|
| SerializeAndDeserializeBinaryFormatter         | 27.831 us | 0.5395 us | 0.7015 us |  1.00 | 5.2185 | 0.2441 |  42.86 KB |        1.00 |
| SerializeAndDeserializeSystemTextJson          |  5.699 us | 0.0930 us | 0.0870 us |  0.21 | 0.5875 |      - |   4.84 KB |        0.11 |
| SerializeAndDeserializeNewtonsoftJson          |  7.876 us | 0.1536 us | 0.1828 us |  0.28 | 1.2665 | 0.0153 |  10.39 KB |        0.24 |
| SerializeAndDeserializeMessagePack             |  2.137 us | 0.0116 us | 0.0109 us |  0.08 | 0.3624 |      - |   2.98 KB |        0.07 |
| SerializeAndDeserializeMemoryPack              |  1.480 us | 0.0031 us | 0.0026 us |  0.05 | 0.3624 | 0.0019 |   2.97 KB |        0.07 |
| SerializeAndDeserializeSystemTextJsonUtf8Bytes |  5.465 us | 0.0983 us | 0.0919 us |  0.20 | 0.5112 |      - |   4.23 KB |        0.10 |

Conclusion

There are "plenty" of options you can choose from. Verify what you need for your specific code. For my purposes, MessagePack gave me the best results (performance and stuff I had to refactor).

Update 15th of April - The last straw

There are rare cases where you still have to rely on the BinaryFormatter. As written in the beginning the BinaryFormatter will be removed from .net9. So if there is really no other way and you know the risk, you can still copy the BinaryFormatter into your own solution as a last resort. You can find the most current version here: https://github.com/dotnet/runtime/blob/v8.0.4/src/libraries/System.Runtime.Serialization.Formatters/src/System/Runtime/Serialization/Formatters/Binary/BinaryFormatter.cs

Basically, making BinaryFormatter part of your domain! Again, please be very careful about that and assess your situation in-depth before doing so!

6
An error has occurred. This application may no longer respond until reloaded. Reload x