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: