Extensibility

Extensibility

Custom Serialization

By default, MemDb uses System.Text.Json.JsonSerializer. Providing a custom serializer avoids JSON overhead and can significantly improve throughput in performance-critical scenarios.

Implement IMemDbSerializer<T>, which handles both stream-based (BinaryReader/BinaryWriter) and span-based paths:

public class PersonSerializer : IMemDbSerializer<Person>
{
    public void Serialize(Person record, BinaryWriter to)
    {
        to.Write(record.Id);
        to.Write(record.FirstName);
        to.Write(record.MiddleName is null);
        if (record.MiddleName is not null)
            to.Write(record.MiddleName);
        to.Write(record.LastName);
        to.Write(record.BirthDate.ToBinary());
        to.Write(record.Ssn);
        to.Write(record.CreditScore);
        to.Write(record.NetWorth);
    }

    public byte[] Serialize(Person record)
    {
        using var ms = new MemoryStream();
        using var writer = new BinaryWriter(ms, Encoding.UTF8, leaveOpen: true);
        Serialize(record, writer);
        return ms.ToArray();
    }

    public Person Deserialize(BinaryReader from, int length)
    {
        var record = new Person();
        record.Id = from.ReadInt64();
        record.FirstName = from.ReadString();
        if (!from.ReadBoolean()) record.MiddleName = from.ReadString();
        record.LastName = from.ReadString();
        record.BirthDate = DateTime.FromBinary(from.ReadInt64());
        record.Ssn = from.ReadString();
        record.CreditScore = from.ReadInt32();
        record.NetWorth = from.ReadDecimal();
        return record;
    }

    public Person Deserialize(ReadOnlySpan<byte> from)
    {
        using var ms = new MemoryStream(from.ToArray());
        using var reader = new BinaryReader(ms);
        return Deserialize(reader, from.Length);
    }
}

Register the serializer during configuration:

MemDb.ConfigureFor<Person>("people", _dbDirectory)
    .SerializeWith(() => new PersonSerializer())
    .Register();
Serialization is a commitment made at database creation time. Switching serializers later requires reading all records out of the old database (with the old serializer) and re-inserting them into a new database (with the new serializer).

Adding JsonConverters to the default serializer

For niche types that System.Text.Json can’t handle out of the box — custom date formats, value-object wrappers, polymorphism without [JsonDerivedType] — you can attach a JsonConverter<U> to MemDb’s default JSON serializer without writing a full IMemDbSerializer<T>. Resolve the per-type singleton via MemDbJsonSerializer<T>.GetInstance() and call ApplyConverterFor<U>:

MemDbJsonSerializer<Person>
    .GetInstance()
    .ApplyConverterFor<TaxId>(new TaxIdJsonConverter());

MemDb.ConfigureFor<Person>("people", _dbDirectory)
    .Register();

ApplyConverterFor clones the serializer’s options, removes any converter targeting the same type, adds the new one, and hot-swaps the options instance. Call it once at startup, before the first MemDb.Open — it is not thread-safe.

Custom Cloning

By default, MemDb deep-copies records via a round-trip through whichever serializer is registered (custom or the default JSON serializer). Providing an IMemDbCloner<T> implementation bypasses serialization entirely and performs a direct field copy — significantly faster for large datasets.

The cloner is also the right place to normalize or sanitize values at the cache boundary — for example, coalescing null to a sentinel value on properties used in applied indexes (which cannot hold null keys).

public class PersonCloner : IMemDbCloner<Person>
{
    public Person DeepCopy(Person value)
    {
        var record = new Person();
        record.Id = value.Id;
        record.FirstName = value.FirstName;
        record.MiddleName = value.MiddleName;
        record.LastName = value.LastName;
        record.BirthDate = value.BirthDate;
        record.Ssn = value.Ssn ?? string.Empty;  // null coalesced — Ssn is an applied index
        record.CreditScore = value.CreditScore;
        record.NetWorth = value.NetWorth;
        return record;
    }

    public Person[] DeepCopy(IList<Person> values)
    {
        int count = values?.Count ?? 0;
        if (count == 0)
            return Array.Empty<Person>();

        var result = new Person[count];
        for (int i = 0; i < count; i++)
            result[i] = DeepCopy(values[i]);
        return result;
    }
}

Register the cloner during configuration:

MemDb.ConfigureFor<Person>("people", _dbDirectory)
    .SerializeWith(() => new PersonSerializer())
    .CloneWith(() => new PersonCloner())
    .Register();

Unlike serialization, cloning operates entirely in RAM and can be added or changed at any time without affecting persisted data.

See Core Concepts — Data Integrity for the full cloning priority chain.