Some MongoDB aggregation stages return dynamic results — often arriving as an ExpandoObject when you project into a dynamic type. Deserializing that back into a typed class involves the same serializer the driver uses internally — just called explicitly. A look at the driver test source reveals the two-step process.
Target Model
The target class uses standard BSON annotations. Note the [BsonElement("LastName")] mapping — the deserialization respects attribute-based field renaming:
public class Courses
{
public string Name { get; set; }
}
public class Person
{
[BsonId(IdGenerator = typeof(GuidGenerator))]
[BsonIgnoreIfDefault]
public Guid UniqueId { get; set; }
[BsonElement("LastName")]
public string Name { get; set; }
public string FirstName { get; set; }
public List<int> Scores { get; set; }
public Courses Courses { get; set; }
}Solve — Two Lines That Do the Work
Step one: serialize the ExpandoObject to BSON bytes using a dynamic-type flag. Step two: deserialize those bytes into the target type using BsonSerializer with the ExpandoObject serializer as the dynamic document serializer:
// Two lines from the MongoDB C# driver source — serialize the ExpandoObject as BSON
// then deserialize with a dynamic document serializer so BsonSerializer can map it
public static TProjection Solve<TProjection>(ExpandoObject keyValuePairs)
where TProjection : class
{
// Serialize: tell the BSON encoder to treat ExpandoObject as a dynamic type
var bson = keyValuePairs.ToBson(
configurator: b => b.IsDynamicType = t => t == typeof(ExpandoObject));
// Deserialize: provide an ExpandoObject serializer to handle nested dynamic objects
var rehydrated = BsonSerializer.Deserialize<BsonDocument>(bson,
b => b.DynamicDocumentSerializer = BsonSerializer.LookupSerializer<ExpandoObject>());
return BsonSerializer.Deserialize<TProjection>(rehydrated);
}Test
Build an ExpandoObject manually to verify the mapping works end-to-end — including nested objects and BSON field rename via attribute:
// Build a test ExpandoObject that mirrors the Person model
IDictionary<string, object> personExpando = new ExpandoObject();
personExpando.Add("LastName", "Rami");
personExpando.Add("FirstName", "John");
personExpando.Add("Scores", new List<int> { 1, 2, 3 });
IDictionary<string, object> courses = new ExpandoObject();
courses.Add("Name", "Math");
personExpando.Add("Courses", courses);
personExpando.Add("_id", "b5c5cf38-e0c9-4b93-b8e5-b9c7f6c1839b");
Person person = Solve<Person>((ExpandoObject)personExpando);
// person.Name → "Rami" (BsonElement maps "LastName" → Name)
// person.FirstName → "John"
// person.Courses.Name → "Math"