Fixing NServiceBus default databus serializer in .NET 6
Upgrading to .NET 6, updating all the packages, boosters turned on, launching testing.
Houston, we've got a problem.
System.NotSupportedException: BinaryFormatter serialization and deserialization are disabled within this application. See https://aka.ms/binaryformatter for more information.
Ouch! What just happened? There were no warnings, no obsolete messages, nothing on to the autopsy.
NServiceBus has a data bus (or a 'databus') feature. The feature implements the Claim Check pattern to allow messages to surpass the imposed maximum message size by the underlying messaging technology. The feature serializes the data internally, and the default DefaultDataBusSerializer
uses BinaryFormatter
. Nothing new; it has been used for years. Unfortunately, with .NET 5, BinaryFormatter
was deprecated due to a security risk it poses. And while you could skip .NET 5 and live with .NET Core 3.1, .NET 6 is breathing down the neck, and an upgrade is imminent.
There is only one option:
- Re-enable the binary formatter 🦨
- Work around the problem until Particular has an official solution
You read it right. Until an official fix, #2 is the only option that will be compliant with most environments.
The workaround can be summarized as the following:
- Pick serialization
- Replace the default data bus serializer with the custom version
- Deploy
Picking serialization
I've chosen to go with BSON. The naive implementation is the following:
public class BsonDataBusSerializer : IDataBusSerializer
{
public void Serialize(object databusProperty, Stream stream)
{
using var writer = CreateNonClosingStreamWriter(stream);
using var bsonBinaryWriter = new BsonBinaryWriter(stream);
BsonSerializer.Serialize(bsonBinaryWriter, databusProperty);
}
StreamWriter CreateNonClosingStreamWriter(Stream stream)
=> new(stream, Encoding.UTF8, bufferSize: 1024, leaveOpen: true);
public object Deserialize(Stream stream)
{
using var bsonBinaryReader = new BsonBinaryReader(stream);
return BsonSerializer.Deserialize<object>(bsonBinaryReader);
}
}
Replacing the default data bus serializer
Update: there's a cleaner option. Skip to the I Need a Better Option section below.
One of the things I wanted to avoid is sprinkling the code-base with the replacement code in various projects that use NServiceBus. So rather than going to the multiple places and having to register the workaround in the following way:
// TODO: required workaround for issue (link). Remove when fixed.
endpoint.AdvancedConfiguration.RegisterComponents(c =>
c.RegisterSingleton<IDataBusSerializer>(new BsonDataBusSerializer()));
A perfect candidate would be using an auto-registered features feature. A feature could be a part of the Shared solution that all endpoints are using and would automatically replace the data bus serializer w/o any endpoints having to do anything in the configuration code.
internal class BsonDataBusSerializerFeature : Feature
{
public BsonDataBusSerializerFeature()
{
DependsOn<NServiceBus.Features.DataBus>();
EnableByDefault();
}
protected override void Setup(FeatureConfigurationContext context)
{
if (context.Container.HasComponent<IDataBusSerializer>())
{
// ???. Remove(defaultDataBusSerializer);
}
context.Container.ConfigureComponent<IDataBusSerializer>(_ =>
new BsonDataBusSerializer(), DependencyLifecycle.SingleInstance);
}
}
Except there's no way to achieve that with NServiceBus today. The IServiceCollection
is adapted into NServiceBus ServiceCollectionAdapter
, which doesn't provide a way to remove any previously registered services as one can do with a plain IServiceCollection
. More details here.
Workaround for the workaround
This part might be a bit smelly, but it's the necessary evil. NServiceBus adapts IServiceCollection
and keeps a reference as a private member field. With some reflection, we can get hold of the service collection and purge the default IDataBusSerializer
implementation to ensure it's not registered and resolved first.
protected override void Setup(FeatureConfigurationContext context)
{
if (context.Container.HasComponent<IDataBusSerializer>())
{
var serviceCollection = context.Container.GetFieldValue<IServiceCollection>("serviceCollection");
if (serviceCollection is not null)
{
var defaultDataBusSerializer = serviceCollection.FirstOrDefault(descriptor =>
descriptor.ServiceType == typeof(IDataBusSerializer));
if (defaultDataBusSerializer is not null)
{
serviceCollection.Remove(defaultDataBusSerializer);
}
}
}
context.Container.ConfigureComponent<IDataBusSerializer>(_ =>
new BsonDataBusSerializer(), DependencyLifecycle.SingleInstance);
}
With a slight modification to the Setup
method, the feature is now ready to be used!
I Need a Better Option
And as was pointed out by Particular, there's an option to register a custom data bus serializer earlier than Core does it, removing the need in reflection. The feature could be replaced by an INeedInitialization
component, which is invoked before endpoint creation and initialization.
public class ReplaceDefaultDataBusSerializer : INeedInitialization
{
public void Customize(EndpointConfiguration endpointConfiguration)
{
endpointConfiguration.RegisterComponents(components =>
components.RegisterSingleton<IDataBusSerializer>(new BsonDataBusSerializer()));
}
}
Deploying
A word of caution for the solutions using one of these features in combination with data bus:
- Events
- Delayed messages
You will need to tread carefully. The migration is not a simple data bus serializer replacement in these scenarios. It has to cater to the fact that messages serialized with BinaryFormatter
could be processed by the endpoints converted to use the new serialization. Subscribing to the issue on this topic is probably a safe bet. Or at least toss a few ideas before you start. And no matter what, good luck!