The MongoDB C# driver's BSON serializer can handle entity classes that Newtonsoft JSON cannot deserialize without a custom JsonConverter — non-parameterless constructors and private setters included. That makes it an easy, correct deep-clone mechanism when you are already using the driver.
The Entities
Two derived classes — both use constructors with parameters that do not match the property names (done intentionally to show the serializer's capability), and all setters are private:
// Base entity with a Clone method stub
public class Entity<T> where T : Entity<T>
{
public T Clone()
{
// To be defined
}
}
// Both derived classes use non-parameterless constructors and private setters
public class Card : Entity<Card>
{
public int Value { get; private set; }
public Card(int v) => Value = v;
}
public class Deck : Entity<Deck>
{
public List<Card> Cards { get; private set; }
private Deck(Card[] c) => Cards = c.ToList() ?? new List<Card>();
internal static Deck Create(params Card[] cards) => new Deck(cards);
}Visual Studio will suggest removing the private setters since the properties are only set in the constructor, but the BSON deserialization process needs them to assign values during rehydration — keep them.
Using Newtonsoft on these classes fails silently — the deserialized object has default values because JSON.NET cannot find a matching parameterless constructor or inject through the private setters without a custom converter.
BsonSerializer Clone
Serialize the instance to a MemoryStream using BsonBinaryWriter, then deserialize the bytes back into a fresh instance of T:
// Clone using BsonSerializer — handles non-parameterless constructors and private setters
public class Entity<T> where T : Entity<T>
{
public T Clone()
{
byte[] serialized;
using (var stream = new MemoryStream())
using (var writer = new BsonBinaryWriter(stream))
{
BsonSerializer.Serialize(writer, typeof(T), this);
stream.Seek(0, SeekOrigin.Begin);
serialized = stream.ToArray();
}
return BsonSerializer.Deserialize<T>(serialized);
}
}
// Works even with:
// - Non-parameterless constructors (BsonSerializer matches by convention or attribute)
// - Private setters (BSON bypasses accessibility via reflection)
// - Nested complex types
var original = Deck.Create(new Card(1), new Card(2), new Card(3));
Deck copy = original.Clone();
// copy is a deep clone — different object references, same values