When you store an entity that contains value objects in MongoDB, the C# driver serializes the entire value object — including internal fields and type discriminators — rather than just the scalar value inside it. A custom IBsonSerializer tells the driver exactly how to read and write the underlying primitive.
The Value Object
The Email value object wraps a string, adding validation and structural equality. It needs both a parameterless constructor (for the expando deserializer) and a serialization constructor:
// Email value object — wraps a string with validation and equality semantics
public class Email : ValueObject<string>
{
public Email() { } // required by expando deserializer
public Email(SerializationInfo info, StreamingContext context)
{
this.GetObjectData(info, context);
}
public static Email Create(string value) => new Email { Value = value };
public override int CompareTo(ValueObject<string> other)
=> this.Value.CompareTo(other.Value);
protected override bool EqualsCore(object obj)
{
if (obj is ValueObject<string> vo) return this.Value == vo.Value;
return false;
}
protected override int GetHashCodeCore() => this.Value.GetHashCode();
protected override void Validators(ValidationBuilder<string, string> validators)
{
validators.Add(
x => !string.IsNullOrEmpty(x)
? ValidationResult<string>.Success()
: ValidationResult<string>.Fail("Email cannot be empty"),
priority: 0);
}
}The Problem
Without a custom serializer, the driver expands the value object's properties into the document — producing a nested object instead of a plain string field:
// Entity using the Email value object as a property
public class ContactDetails : BasicEntity
{
[BsonElement("Email")]
public Email Email { get; set; }
}
// Without a custom serializer, MongoDB serializes the full object tree:
// { "Email": { "Value": "[email protected]", "_t": "Email" } }
// — which is not what we want for a value object.Custom IBsonSerializer
Implement IBsonSerializer<Email> to read and write the raw string via the BSON reader/writer:
// Custom IBsonSerializer for Email — reads/writes the raw string value
public class EmailSerializer : IBsonSerializer<Email>
{
public Type ValueType => typeof(Email);
public Email Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var value = context.Reader.ReadString();
return Email.Create(value);
}
public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Email value)
{
context.Writer.WriteString(value.Value);
}
public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value)
{
if (value is Email email)
context.Writer.WriteString(email.Value);
else
throw new NotSupportedException("This is not an Email");
}
object IBsonSerializer.Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
return Email.Create(context.Reader.ReadString());
}
}Registering the Serializer
Decorate the value object class with [BsonSerializer(typeof(EmailSerializer))]. The driver picks it up automatically — no global registration needed:
// Decorate the value object class with the serializer attribute
[BsonSerializer(typeof(EmailSerializer))]
public class Email : ValueObject<string>
{
// ...
}
// Now MongoDB stores the value directly as a string:
// { "Email": "[email protected]" }
// — clean, no wrapper object.