We have multiple custom serializers in our codebase, many deriving from existing serializers. Now that these classes are sealed in version 3.0 of the C# drivers, we need to convert them.
Before we systematically convert them to use composition instead of inheritance, it might be useful for us to validate that there isn’t a simpler approach to each of our needs, which we either might have overlooked, or did not exist when we initially coded this.
First, a simple confirmation: We had many “Nullable” versions of serializers. We’ll switch to using NullableSerilizer.Create(baseSerializer)
. Is this the correct approach?
Second, what would be the correct way to have a value-type serializer use a specific value as the default when encountering a null value at deserialization? For example, we had a BooleanDefaultTrueSerializer
(the name says it all), and some EnumSerializer<T>
-derived serializers that mapped nulls to specific values. This is not to be confused with the default value when the field is absent. It’s really about having a default value when it’s null (legacy serialization that shouldn’t be null anymore, but which we want to continue to support).
Thanks.
Thanks @Martin_Plante for your questions.
NullableSerializer: Yes, its a nullable version for value serializers.
Custom value on null: There is no quick configuration option for that. The only way is to implement a custom serializer to handle the null values.
One way would the mentioned approach of having multiple serializers with hardcoded values like BooleanDefaultTrueSerializer
.
More complex and generic approach would be augmenting an existing value serializer and configuring it with a custom value. Custom value configuration could be done via an attribute, which can be also used to configure the default value on missing data. Partial example:
public class MyClass
{
[CustomSerializerOption<int>(DefaultValue = 11)]
[BsonSerializer(typeof(CustomValueOnNull<int>))]
public int A { get; set; }
[CustomSerializerOption<bool>(DefaultValue = true)]
[BsonSerializer(typeof(CustomValueOnNull<bool>))]
public bool T { get; set; }
}
public sealed class CustomSerializerOption<T> : Attribute, IBsonMemberMapAttribute
where T : struct
{
public CustomSerializerOption()
{
DefaultValue = default;
}
public T DefaultValue { get; set; }
public void Apply(BsonMemberMap memberMap)
{
var serializer = memberMap.GetSerializer();
var serializer2 = ApplySerializer(serializer);
memberMap.SetSerializer(serializer2);
memberMap.SetDefaultValue(DefaultValue);
}
private IBsonSerializer ApplySerializer(IBsonSerializer serializer)
{
if (serializer is CustomValueOnNull<T> customSerializer)
{
return customSerializer.WithDefaultValue(DefaultValue);
}
throw new InvalidOperationException();
}
}
public sealed class CustomValueOnNull<T> : IBsonSerializer<T>
where T : struct
{
private readonly IBsonSerializer<T> _valueSerializer;
private readonly T _defaultValue;
public CustomValueOnNull()
: this(default, BsonSerializer.SerializerRegistry)
{
}
public CustomValueOnNull(T defaultValue, IBsonSerializer<T> valueSerializer)
{
if (valueSerializer == null)
{
throw new ArgumentNullException(nameof(valueSerializer));
}
_valueSerializer = valueSerializer;
_defaultValue = defaultValue;
}
public T Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var bsonReader = context.Reader;
var bsonType = bsonReader.GetCurrentBsonType();
if (bsonType == BsonType.Null)
{
bsonReader.ReadNull();
return _defaultValue;
}
else
{
return _valueSerializer.Deserialize(context);
}
}
public CustomValueOnNull<T> WithDefaultValue(T defaultValue) =>
new(defaultValue, _valueSerializer);
....
}
Thanks Boris,
I’ll go with the 100% custom serializer approach since the entities are protobuf generated classes (so no attributes). I could apply an option in our class mapping code but I already implemented the custom serializers and I’m now testing them.
Thanks again for confirming.
Addendum to my questions: We have code that directly uses protobuf messages, where strings can’t be null (throws an exception), thus we also had a similar “default value when null” serializer that made sure that null ObjectId fields in documents were deserialized as an empty string. Again, with the latest drivers, do we still need to implement a dedicated serializer for this?
Yes, in this case as well “DefaultValueWhenNull” serializer is needed.
In our serialization the default value behavior applies to values that are missing, while null
is a valid value.