Serializer
DMediatR uses typed binary serialization with pluggable custom serializers to
transmit MediatR IRequest/IResponse messages over gRPC. Custom serializers
for specific types can be added to the service collection. There are two
internal custom serializers: One for serializing X509Certificate2 objects and
one for tracing purposes counting the number of times the object has been
serialized.
Injecting Custom Serializers
DMediatR leverages keyed dependency injection, introduced in .NET 8, to look up
custom serializers for a particular type. This excerpt from the
ServiceCollectionExtension registers, along with the required infrastructure,
the two custom serializers SerializationCountSerializer and
X509CertificateSerializer:
services.TryAddSingleton<ISerializer, Serializer>();
services.TryAddSingleton<TypedSerializer>();
services.TryAddKeyedSingleton<ISerializer, BinarySerializer>(typeof(object)); // recursion base case for TypedSerializer
services.TryAddKeyedSingleton<ISerializer, SerializationCountSerializer>(SerializationCountSerializer.Type);
services.TryAddKeyedSingleton<ISerializer, X509CertificateSerializer>(X509CertificateSerializer.Type);
services.TryAddKeyedSingleton<ISerializedInterface, ILockISerializedInterface>(typeof(ILock));
The ILockSerializedInterface is a special case, as it is declared for the
ILock interface and not a concrete class, as multiple class hierarchies can
implement the same interface.
Custom Serializer Implementation
Custom serializers inherit from the generic class CustomSerializer<T> with the class to
register the serializer for as type parameter. They can override one or both of the
Serialize and Deserialize methods. This can be used e.g. for dehydrating an object by
nulling out non-serializable members before serialization and then for rehydrating it after
deserialization by setting the members again with instances from DI on the destination node.
The SerializationCountSerializer is used to trace the number of node hops a
DMediatR message (IRequest or INotification) has taken by incrementing the
object's Count property:
namespace DMediatR
{
internal class SerializationCountSerializer : CustomSerializer<SerializationCountMessage>
{
public SerializationCountSerializer(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
public override byte[] Serialize(Type type, object obj)
{
CheckType(type);
((SerializationCountMessage)obj).Count++;
return base.Serialize(type, obj, checkType: false);
}
}
}
The X509CertificateSerializer needs the injected password from configuration
to decrypt the .pfx binary for deserialization and uses plain byte[]
serialization for the data exported by the X509Certificate2 object:
using Microsoft.Extensions.Options;
using System.Security.Cryptography.X509Certificates;
namespace DMediatR
{
internal class X509CertificateSerializer : CustomSerializer<X509Certificate2>
{
private readonly PasswordOptions _options;
public X509CertificateSerializer(IServiceProvider serviceProvider,
IOptions<PasswordOptions> options) : base(serviceProvider)
{
_options = options.Value;
}
public override byte[] Serialize(Type type, object obj)
{
CheckType(type);
var cert = (X509Certificate2)obj;
var bytes = cert.Export(X509ContentType.Pkcs12, _options.Password);
return base.Serialize(typeof(byte[]), bytes, checkType: false);
}
public override object Deserialize(Type type, byte[] bytes)
{
CheckType(type);
var rawData = (byte[])base.Deserialize(typeof(byte[]), bytes, checkType: false);
var cert = new X509Certificate2(rawData, _options.Password, X509KeyStorageFlags.Exportable);
return cert;
}
}
}
When de- resp. rehydrating is required by an interface requiring a
non-serializable member, a CustomSerializer<T> based on a class hierarchy is
not appropriate, as interface custom serialization is orthogonal to the class
hierarchy: Serializable classes can implement multiple interfaces, which in turn
require e.g. specific members which must be dehydrated before serialization for
each interface implemented.
The ILockSerializedInterface is defined for the
interface ILock and overrides the two Dehydrate/Rehydrate hooks
called by the general Serializer class:
namespace DMediatR
{
public class ILockISerializedInterface : SerializedInterface<ILock>
{
protected override void Dehydrate(ILock obj)
{
obj.HasLocked = null; // SemaphoreSlim is not serializable
}
protected override void Rehydrate(ILock obj)
{
obj.HasLocked = [];
}
}
}
Serialization Classes
Context for serializing Ping objects
This diagram exemplifies the context for serializing of the Ping class
deriving from SerializationCountSerializer:
Serialization Sequence for Ping
The corresponding sequence diagram hints at the serialization class dispatch mechanism: