MongoDB doesn’t serialise C# read-only properties

C# 6 introduced getter-only auto-properties, which are a great way of conveying immutability for your classes. However, by default MongoDB ignores these properties from its class maps, as it can’t deserialise them back into a class.

To automatically map read-only properties, you can use a custom convention:

/// <summary>
/// A convention to ensure that read-only properties are automatically mapped (and therefore serialised).
/// </summary>
public class MapReadOnlyPropertiesConvention : ConventionBase, IClassMapConvention
{
    private readonly BindingFlags _bindingFlags;

    public MapReadOnlyPropertiesConvention() : this(BindingFlags.Instance | BindingFlags.Public) {}

    public MapReadOnlyPropertiesConvention(BindingFlags bindingFlags)
    {
        _bindingFlags = bindingFlags | BindingFlags.DeclaredOnly;
    }

    public void Apply(BsonClassMap classMap)
    {
        var readOnlyProperties = classMap
            .ClassType
            .GetTypeInfo()
            .GetProperties(_bindingFlags)
            .Where(p => IsReadOnlyProperty(classMap, p))
            .ToList();

        foreach (var property in readOnlyProperties)
        {
            classMap.MapMember(property);
        }
    }

    private static bool IsReadOnlyProperty(BsonClassMap classMap, PropertyInfo propertyInfo)
    {
        if (!propertyInfo.CanRead) return false;
        if (propertyInfo.CanWrite) return false; // already handled by default convention
        if (propertyInfo.GetIndexParameters().Length != 0) return false; // skip indexers

        var getMethodInfo = propertyInfo.GetMethod;

        // skip overridden properties (they are already included by the base class)
        if (getMethodInfo.IsVirtual && getMethodInfo.GetBaseDefinition().DeclaringType != classMap.ClassType) return false;

        return true;
    }
}

And then register the convention on startup:

var conventionPack = new ConventionPack
{
    new MapReadOnlyPropertiesConvention()
};

ConventionRegistry.Register("Conventions", conventionPack, _ => true);

This will ensure that read-only properties are serialised, which is all we needed. However, to then deserialise properties back into a class, you may need a constructor. You can find an extended example of this at https://stackoverflow.com/questions/39604820/serialize-get-only-properties-on-mongodb (which this code is based on) – we had constructor chains that this didn’t seem to work with, but it might help guide you further.