Table of Contents

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:

MessagePackSerializerTypelessSerialize(obj)Deserialize(bytes)ISerializerSerialize(obj)Serialize(type, obj)Deserialize<T>(bytes)Deserialize(type, bytes)Serializer_typedSerializerSerialize(obj)Serialize(type, obj)Deserialize<T>(bytes)Deserialize(type, bytes)BinarySerializerSerialize(obj)Serialize(type, obj)Deserialize<T>(bytes)Deserialize(type, bytes)TypedSerializer_serviceProviderSerialize(type, obj)Deserialize(type, bytes)GetSerializer(type)CustomSerializerTTypeCheckType(givenType)SerializationCountSerializerSerialize(type, obj)IRequestTResponseINotificationSerializationCountMessageCountPingMessagePongMessageBingMessage

Serialization Sequence for Ping

The corresponding sequence diagram hints at the serialization class dispatch mechanism:

SerializerTypedSerializerSerializationCountSerializerBinarySerializerRemoteExtensionSerializerTypedSerializerSerializationCountSerializerBinarySerializerRemoteExtensionRemoteExtensionSerializerSerializerTypedSerializerTypedSerializerSerializationCountSerializerSerializationCountSerializerBinarySerializerBinarySerializerSerializerTypedSerializerSerializationCountSerializerBinarySerializerSerialize(obj)Serialize(type, obj)Serialize(type, obj)GetSerializer(type)Serialize(type, obj)GetSerializer(type)Serialize(type, obj)Serialize(type, obj)GetSerializer(type)Serialize(type, obj)byte[]byte[]byte[]byte[]