Recently, I had the pleasure of attending a MongoDB Dev Day, and it was a fantastic reminder of how powerful and versatile MongoDB is. It also, quite vividly, brought back memories of some “aha!” moments I’ve had while working with it – those little quirks that catch you off guard but teach you a valuable lesson. I’ve got a couple of these stories to share, but for this post, I want to dive into the very first one that stumped me as a .NET developer.
The Power of Schema Flexibility: A Double-Edged Sword
One of MongoDB’s most touted features, and rightly so, is its schema-flexible nature. Unlike traditional relational databases (think SQL Server or PostgreSQL), where you meticulously define tables with fixed columns and data types upfront, MongoDB allows you to store documents in a collection without a predefined, rigid schema.
This flexibility offers immense benefits, especially in today’s agile development environments and for handling diverse data:
- Rapid Iteration: You can quickly adapt your data model as your application requirements evolve, without the need for complex schema migrations. Adding a new field to your data model is often as simple as adding a property to your C# class.
- Handling Diverse and Unstructured Data: It’s particularly well-suited for applications dealing with varied or unstructured data. Imagine logs, sensor data, or user-generated content where different entries might have wildly different sets of fields. MongoDB effortlessly accommodates these variations within the same collection, allowing you to store and query highly disparate data points without forcing them into a rigid structure. (You can read more about this in https://www.mongodb.com/resources/basics/unstructured-data/schemaless)
- Scalability: This adaptable structure can significantly simplify scaling, as you’re not constrained by rigid table structures when distributing data across multiple servers.
This ability to adapt and scale with our application requirements, and to gracefully handle data that doesn’t fit a fixed mold, is a huge win, often leading to faster development cycles. However, as I discovered, this very flexibility can sometimes lead to unexpected behavior if you’re not aware of its nuances.
The Gotcha: Adding a Property and the Unexpected Error Across Services
Let’s illustrate my first gotcha with a simplified example that mirrors a real-world scenario. Imagine we have a DeliveryDetail
POCO class, and we’re storing DeliveryDetail
as subdocument in Order
document in MongoDB collection. Our solution has two separate .NET projects that both reference a shared project containing this POCO model:
- Project A (Updater): Responsible for creating and updating
DeliveryDetail
information for theOrder
in MongoDB. - Project B (Reader): Responsible for reading
Order
documents from MongoDB and displaying them.
public class DeliveryDetail
{
public string DeliveryAddress { get; set; }
public DateTime DeliveryDate { get; set; }
public string PostCode { get; set; }
public bool Delivered { get; set; }
}
We’ve been happily saving and retrieving Order
documents for a while. Now, imagine a new feature request comes in: we need to track when the last time the DeliveryDetail
was updated. Naturally, as .NET developers, our team responsible for Project A (Updater) adds a new property to the DeliveryDetail
class in the shared project:
public class DeliveryDetail
{
public string DeliveryAddress { get; set; }
public DateTime DeliveryDate { get; set; }
public string PostCode { get; set; }
public bool Delivered { get; set; }
// New Property
public DateTime LastUpdated { get; set; }
}
Seems innocent enough, right? The team deploys Project A and it starts saving new DeliveryDetail
into Order
documents, and existing documents are updated if delivery detail is updated.
However, in one fateful morning, Project B (Reader), which hasn’t been redeployed with the latest version of the shared project yet, suddenly starts throwing errors when it tries to read existing Order
documents from the MongoDB collection. The error looks something like this:
System.FormatException: 'An error occurred while deserializing the DeliveryDetail property of class MongoDbLearning.Common.Order: Element 'LastUpdated' does not match any field or property of class MongoDbLearning.Common.DeliveryDetail.'
Inner Exception
FormatException: Element 'LastUpdated' does not match any field or property of class MongoDbLearning.Common.DeliveryDetail.
The following is the difference between old and updated document:

This is where the “schema-flexible” aspect meets the .NET driver’s strong typing, particularly problematic in a distributed environment where different services might be running slightly different versions of the shared models. When Project A starts saving Order
documents where DeliveryDetail
now includes the LastUpdated
field, and Project B (Reader), which hasn’t been redeployed with the latest DeliveryDetail
class, tries to deserialize these documents, the MongoDB .NET driver encounters an element (LastUpdated
) in the incoming BSON document that doesn’t have a corresponding property in Project B‘s current DeliveryDetail
C# class. By default, the driver is strict and throws an exception, stating that the element LastUpdated
does not match any field or property of its DeliveryDetail
class as Project B knows it.
The Solution: Ensuring Compatibility Across Services with [BsonIgnoreExtraElements]
The fix for this is surprisingly simple, yet crucial to know for smooth MongoDB development in .NET, especially in a microservices or distributed environment. We need to tell the MongoDB .NET driver to ignore any extra elements it finds in the BSON document that don’t have a corresponding property in our C# class. We do this with the [BsonIgnoreExtraElements]
attribute:
[BsonIgnoreExtraElements] // Add this attribute
public class DeliveryDetail
{
public string DeliveryAddress { get; set; }
public DateTime DeliveryDate { get; set; }
public string PostCode { get; set; }
public bool Delivered { get; set; }
public DateTime LastUpdated { get; set; }
}
By adding [BsonIgnoreExtraElements]
to the DeliveryDetail
class, you instruct the MongoDB .NET driver to gracefully ignore any elements found in the BSON document that do not have a corresponding property in the C# class being deserialized. This means if Project A (Updater) has started saving documents with a LastUpdated
field, but Project B (Reader) is running an older version of the DeliveryDetail
class that doesn’t yet have a LastUpdated
property, Project B’s deserialization will no longer throw an error. The driver will simply skip mapping that ‘extra’ LastUpdated
element. This attribute ensures forward and backward compatibility of your C# models with your MongoDB documents as your schema evolves. (For reference: https://www.mongodb.com/docs/drivers/csharp/current/fundamentals/serialization/class-mapping/#ignore-extra-elements)
Why This Doesn’t Happen with Relational Databases in .NET
If you’re coming from a relational database background in .NET, you might be scratching your head because this scenario wouldn’t typically cause an error. The fundamental difference lies in how schema is handled.
In a relational database, the schema is strictly defined at the database level. If you add a new LastUpdated
column, all existing rows effectively get that column, albeit with a NULL
value by default. When your .NET application, potentially running an older version of the code that doesn’t yet know about the LastUpdated
property in its C# model, queries the database, it simply retrieves the columns it’s expecting. The database doesn’t send “extra” unrecognized columns that would cause a mapping error on the client side. Your ORM (like Entity Framework) only maps the columns it’s configured to, and if a column is missing from your C# model but present in the database, it’s typically ignored without causing a deserialization failure. There’s no “extra element” to worry about, as the database schema explicitly defines all columns, and the client only selects what it needs.
This highlights a fundamental difference in how schema is handled and how drivers interact with the data storage. MongoDB’s flexibility means the driver needs explicit instructions on how to handle deviations between your C# model and the actual BSON document. For more information about .Net class mapping in MongoDB driver, refer to https://www.mongodb.com/docs/drivers/csharp/current/fundamentals/serialization/class-mapping/
Wrapping Up
This first “gotcha” taught me a valuable lesson about the interplay between MongoDB’s schema flexibility and the class mapping behaviour of the .Net driver. Understanding how the driver maps BSON documents to C# objects, and how to explicitly handle scenarios like unrecognized elements, is crucial. While the [BsonIgnoreExtraElements]
solution is simple, comprehending why it’s needed – as a core aspect of driver configuration for schema evolution – is key to writing robust and resilient MongoDB applications, especially in scenarios with multiple consumers of the same data.
I’ve got another interesting gotcha to share regarding MongoDB and .NET. You can read about it now in my second post: Dot Notation’s Hidden Peril: When MongoDB Arrays Become Objects.
Leave a Reply