Explore Developer Center's New Chatbot! MongoDB AI Chatbot can be accessed at the top of your navigation to answer all your MongoDB questions.

Join us at AWS re:Invent 2024! Learn how to use MongoDB for AI use cases.
MongoDB Developer
C#
plus
Sign in to follow topics
MongoDB Developer Centerchevron-right
Developer Topicschevron-right
Languageschevron-right
C#chevron-right

Using Polymorphism with MongoDB and C#

Markus Wildgruber6 min read • Published Apr 30, 2024 • Updated Apr 30, 2024
C#
Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
In comparison to relational database management systems (RDBMS), MongoDB's flexible schema is a huge step forward when handling object-oriented data. These structures often make use of polymorphism where common base classes contain the shared fields that are available for all classes in the hierarchy; derived classes add the fields that are relevant only to the specific objects. An example might be to have several types of vehicles, like cars and motorcycles, that have some fields in common, but each type also adds some fields that make only sense if used for a type:
An entity class diagram showing a vehicle type and then car and motorcycle classes that extend it
For RDBMS, storing an object hierarchy is a challenge. One way is to store the data in a table that contains all fields of all classes, though for each row, only a subset of fields is needed. Another approach is to create a table for the base class that contains the shared fields and add a table for each derived class that stores the columns for the specific type and references the base table. Neither of these approaches is optimal in terms of storage and when it comes to querying the data.
However, with MongoDB's flexible schema, one can easily store documents in the same collection that do only share some but not all fields. This article shows how the MongoDB C# driver makes it easy to use this for storing class hierarchies in a very natural way.
Example use cases include storing metadata for various types of documents, e.g., offers, invoices, or other documents related to business partners in a collection. Common fields could be a document title, a summary, the date, a vector embedding, and the reference to the business partner, whereas an invoice would add fields for the line items and totals but would not add the fields for a project report.
Another possible use case is to serve both an overview and a detail view from the same collection. We will have a closer look at how to implement this in the summary of this article.

Basics

When accessing a collection from C#, we use an object that implements IMongoCollection<T> interface. This object can be created like this:
1var vehiclesColl = db.CreateCollection<Vehicle>("vehicles");
When serializing or deserializing documents, the type parameter T and the actual type of the object provide the MongoDB C# driver with a hint on how to map the BSON representation to a C# class and vice versa. If only documents of the same type reside in the collection, the driver uses the class map of the type.
However, to be able to handle class hierarchies correctly, the driver needs more information. This is where the type discriminator comes in. When storing a document of a derived type in the collection, the driver adds a field named _t to the document that contains the name of the class, e.g.:
1await vehiclesColl.InsertOneAsync(new Car());
leads to the following document structure:
1{
2 "_id": ObjectId("660d7d43e042f8f6f2726f6a"),
3 "_t": "Car",
4 // ... fields for vehicle
5 // ... fields specific to car
6}
When deserializing the document, the value of the _t field is used to identify the type of the object that is to be created.
Though this works out of the box without specific configuration, it is advised to support the driver by specifying the class hierarchy explicitly by using the BsonKnownTypes attribute, if you are using declarative mapping:
1[BsonKnownTypes(typeof(Car), typeof(Motorcycle))]
2public abstract class Vehicle
3{
4 // ...
5}
If you configure the class maps imperatively, just add a class map for each type in the hierarchy to reach the same effect.
By default, only the name of the class is used as value for the type discriminator. Especially if the hierarchy spans several levels and you want to query for any level in the hierarchy, you should store the hierarchy as an array in the type discriminator by using the BsonDiscriminator attribute:
1[BsonDiscriminator(RootClass = true)]
2[BsonKnownTypes(typeof(Car), typeof(Motorcycle))]
3public abstract class Vehicle
4{
5 // ...
6}
This applies a different discriminator convention to the documents and stores the hierarchy as an array:
1{
2 "_id": ObjectId("660d81e5825f1c064024a591"),
3 "_t": [
4 "Vehicle",
5 "Car"
6 ],
7 // ...
8}
For additional details on how to configure the class maps for polymorphic objects, see the documentation of the driver.

Querying collections with polymorphic documents

When reading objects from a collection, the MongoDB C# driver uses the type discriminator to identify the matching type and creates a C# object of the corresponding class. The following query might yield both Car and Motorcycle objects:
1var vehiclesColl = db.GetCollection<Vehicle>("vehicles");
2var vehicles = (await vehiclesColl.FindAsync(FilterDefinition<Vehicle>.Empty))
3 .ToEnumerable();
If you are only interested in documents of a specific type, you can create another instance of IMongoCollection<T> that returns only these:
1var carsColl = vehiclesColl.OfType<Car>();
2var cars = (await carsColl.FindAsync(FilterDefinition<Car>.Empty))
3 .ToEnumerable();
This new collection instance respects the corresponding type discriminator whenever an operation is performed. The following statement removes only Car documents from the collection but keeps the Motorcycle documents as they are:
1await carsColl.DeleteManyAsync(FilterDefinition<Car>.Empty);
If you are using the LINQ provider brought by the MongoDB C# driver, you can also use the LINQ OfType<T> extension method to only retrieve the Car objects:
1var cars = vehiclesColl.AsQueryable().OfType<Car>();

Serving multiple views from a single collection

As promised before, we now take a closer look at a use case for polymorphism: Let's suppose we are building a system that supports monitoring sensors that are distributed over several sites. The system should provide an overview that lists all sites with their name and the last value that was reported for the site along with a timestamp. When selecting a site, the system shows detailed information for the site that consists of all the data on the overview and also lists the sensors that are located at the specific site with their last value and its timestamp.
This can be depicted by creating a base class for the documents that contains the id of the site, a name to identify the document, and the last measurement, if available. A derived class for the site overview adds the site address; another one for the sensor detail contains the location of the sensor:
1using MongoDB.Bson;
2using MongoDB.Bson.Serialization.Attributes;
3
4public abstract class BaseDocument
5{
6 [BsonRepresentation(BsonType.ObjectId)]
7 public string Id { get; set; } = ObjectId.GenerateNewId().ToString();
8
9 [BsonRepresentation(BsonType.ObjectId)]
10 public string SiteId { get; set; } = ObjectId.GenerateNewId().ToString();
11
12 public string Name { get; set; } = string.Empty;
13
14 public Measurement? Last { get; set; }
15}
16
17public class Measurement
18{
19 public int Value { get; set; }
20
21 public DateTime Timestamp { get; set; }
22}
23
24public class Address
25{
26 // ...
27}
28
29public class SiteOverview : BaseDocument
30{
31 public Address Address { get; set; } = new();
32}
33
34public class SensorDetail : BaseDocument
35{
36 public string Location { get; set; } = string.Empty;
37}
When ingesting new measurements, both the site overview and the sensor detail are updated (for simplicity, we do not use a multi-document transaction):
1async Task IngestMeasurementAsync(
2 IMongoCollection<BaseDocument> overviewsColl,
3 string sensorId,
4 int value)
5{
6 var measurement = new Measurement()
7 {
8 Value = value,
9 Timestamp = DateTime.UtcNow
10 };
11 var sensorUpdate = Builders<SensorDetail>
12 .Update
13 .Set(x => x.Last, measurement);
14 var sensorDetail = await overviewsColl
15 .OfType<SensorDetail>()
16 .FindOneAndUpdateAsync(
17 x => x.Id == sensorId,
18 sensorUpdate,
19 new() { ReturnDocument = ReturnDocument.After });
20 if (sensorDetail != null)
21 {
22 var siteUpdate = Builders<SiteOverview>
23 .Update
24 .Set(x => x.Last, measurement);
25 var siteId = sensorDetail.SiteId;
26 await overviewsColl
27 .OfType<SiteOverview>()
28 .UpdateOneAsync(x => x.SiteId == siteId, siteUpdate);
29 }
30}
Above sample uses FindAndUpdateAsync to both update the sensor detail document and also retrieve the resulting document so that the site id can be determined. If the site id is known beforehand, a simple update can also be used.
When retrieving the documents for the site overview, the following code returns all the relevant documents:
1var siteOverviews = (await overviewsColl
2 .OfType<SiteOverview>()
3 .FindAsync(FilterDefinition<SiteOverview>.Empty))
4 .ToEnumerable();
When displaying detailed data for a specific site, the following query retrieves all documents for the site by its id in a single request:
1var siteDetails = await (await overviewsColl
2 .FindAsync(x => x.SiteId == siteId))
3 .ToListAsync();
The result of the query can contain objects of different types; you can use the LINQ OfType<T> extension method on the list to discern between the types, e.g., when building a view model.
This approach allows for efficient querying from different perspectives so that central views of the application can be served with minimum load on the server.

Summary

Polymorphism is an important feature of object-oriented languages and there is a wide range of use cases for it. As you can see, the MongoDB C# driver provides a solid bridge between object orientation and the MongoDB flexible document schema. If you want to dig deeper into the subject from a data modeling perspective, be sure to check out the polymorphic pattern part of the excellent series "Building With Patterns" on the MongoDB Developer Center.
Top Comments in Forums
There are no comments on this article yet.
Start the Conversation

Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Related
Article

MongoDB Provider for EF Core: The Latest Updates


Aug 29, 2024 | 6 min read
Tutorial

Query Your Data With ASP.NET Core, OData, and the MongoDB Entity Framework Core Provider


Jul 08, 2024 | 7 min read
Tutorial

Saving Data in Unity3D Using PlayerPrefs


Sep 09, 2024 | 11 min read
Tutorial

Working With MongoDB Transactions With C# and the .NET Framework


Sep 11, 2024 | 3 min read
Table of Contents