The Day Our MongoDB Reads Exploded: A Tale of Two Microservices and a New Property

By

/

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 the Order 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.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

By leaving comment, you agree to our Privacy Policy.