Managing polymorphism in MongoDB with .NET
In a .NET Core–based project, I needed to read and write MongoDB documents containing polymorphic objects using the official MongoDB .NET driver.
The challenge arises when you need to store and reconstruct objects of different types, all derived from the same base class, while ensuring consistent behavior within the database.
In this article, I’ll show how the MongoDB .NET driver makes it easy to work with polymorphic objects, allowing you to read and write documents while preserving the flexibility that polymorphism provides.
What is a polymorphic object?
A polymorphic object is an instance of a derived class that can be treated as an object of its base class.
This principle, fundamental in object-oriented programming, allows methods or properties with the same name to be invoked on different types, producing behavior specific to each type through inheritance and overriding.
Polymorphism makes it possible to write more flexible and reusable code: the same piece of logic can work uniformly with instances of different classes.
In our case, the MongoDB document parameters will be polymorphic: BooleanParameter, NumberParameter, and StringParameter all derive from the base class Parameter.
public enum ParameterType
{
Boolean = 1,
Number = 2,
String = 3
}
public class Parameter
{
public string Id { get; init; }
public ParameterType Type { get; init; }
public string Name { get; init; }
public bool Required { get; init; }
}
public class BooleanParameter : Parameter
{
public bool? DefaultValue { get; init; }
}
public class NumberParameter : Parameter
{
public double? DefaultValue { get; init; }
public int? MinValue { get; init; }
public int? MaxValue { get; init; }
}
public class StringParameter : Parameter
{
public string? DefaultValue { get; init; }
public int? MinLength { get; init; }
public int? MaxLength { get; init; }
}
Advantages of polymorphism
Polymorphism reduces code duplication and improves maintainability.
It makes it possible to work with different types in a uniform way (for example, within mixed collections) and to add new derived types without modifying the existing code that works with the base class.
When polymorphism is needed in MongoDB
MongoDB, as a schemaless database, allows you to store documents with different fields even within the same collection. However, when a .NET application uses derived classes — for example, to represent different types of parameters or configurations — the database must also persist the actual type of each object.
Without this information, the driver would not know which subclass to instantiate during deserialization, and all objects would be reconstructed as instances of the base class.
For this reason, it is essential to define your entities correctly so that the MongoDB driver can distinguish between different types within the same model.
Create an entity on MongoDB
To read and write objects on MongoDB using the .NET driver, we define the classes that represent the structure of the documents.
In this example, the polymorphic parameters are encapsulated inside a main class (MyDocumentEntity), but they could also be stored as independent documents.
There are two primary approaches for defining and implementing these classes:
- using class-level attributes;
- using the
BsonClassMapclass.
For simplicity, we will use the attribute-based approach, although the logic is the same when using BsonClassMap.
The most common attributes we will use are:
[BsonIgnoreExtraElements]: ignores unmapped elements during deserialization;[BsonNoId]: indicates that the class does not contain an_idfield;[BsonId]: identifies the_idfield;[BsonRequired]: marks the field as required;[BsonElement("name")]: assigns a specific name to the property inside the document;[BsonRepresentation(BsonType.String)]: stores an enum as a string instead of a numeric value;[BsonIgnoreIfNull]: omits the field when the value isnull.
The following code defines an entity:
[BsonNoId]
[BsonIgnoreExtraElements]
public class ParameterEntity
{
[BsonRequired]
[BsonElement("id")]
public string Id { get; init; } = Guid.NewGuid().ToString();
[BsonRequired]
[BsonRepresentation(BsonType.String)]
[BsonElement("type")]
public ParameterType Type { get; init; }
[BsonRequired]
[BsonElement("name")]
public string Name { get; init; }
[BsonRequired]
[BsonElement("required")]
public bool Required { get; init; }
}
[BsonIgnoreExtraElements]
public class BooleanParameterEntity : ParameterEntity
{
[BsonIgnoreIfNull]
[BsonElement("defaultValue")]
public bool? DefaultValue { get; init; }
}
[BsonIgnoreExtraElements]
public class NumberParameterEntity : ParameterEntity
{
[BsonIgnoreIfNull]
[BsonElement("defaultValue")]
public double? DefaultValue { get; init; }
[BsonIgnoreIfNull]
[BsonElement("minValue")]
public int? MinValue { get; init; }
[BsonIgnoreIfNull]
[BsonElement("maxValue")]
public int? MaxValue { get; init; }
}
[BsonIgnoreExtraElements]
public class StringParameterEntity : ParameterEntity
{
[BsonIgnoreIfNull]
[BsonElement("defaultValue")]
public string? DefaultValue { get; init; }
[BsonIgnoreIfNull]
[BsonElement("minLength")]
public int? MinLength { get; init; }
[BsonIgnoreIfNull]
[BsonElement("maxLength")]
public int? MaxLength { get; init; }
}
public class MyDocumentEntity
{
[BsonId]
public string Id { get; init; }
[BsonRequired]
[BsonElement("name")]
public string Name { get; init; }
[BsonRequired]
[BsonElement("parameters")]
public ParameterEntity[] Parameters { get; init; } = [];
}
How MongoDB discriminators work
The official MongoDB driver for .NET provides built-in support for polymorphism through the discriminator mechanism.
Whenever a derived object is saved, the driver automatically adds a special field (named _t by default) that identifies the concrete type of the instance.
During reading, this field allows the driver to automatically reconstruct the object using the correct type, without requiring any additional logic from the developer.
It is also possible to customize the discriminator behavior — for example by changing the field name or defining specific values for each subclass — through attributes such as [BsonDiscriminator] and [BsonKnownTypes].
This capability makes discriminators a powerful and flexible tool for handling polymorphism natively, while keeping documents consistent and easily extensible.
In other words, polymorphism support in MongoDB with .NET is an integrated, fully supported feature of the driver — not a workaround or custom solution.
Implementation on the base class
The BsonKnownTypes attribute is used to list all derived classes that the driver must recognize during deserialization.
[BsonKnownTypes(
typeof(BooleanParameterEntity),
typeof(NumberParameterEntity),
typeof(StringParameterEntity)
)]
public class ParameterEntity
{
...
}
Customising the discriminator in derived classes
The BsonDiscriminator attribute allows you to define the value of the `_t` field that identifies each type.
If the attribute is not specified, the driver uses the class name.
[BsonDiscriminator("boolean-parameter")]
public class BooleanParameterEntity : ParameterEntity
{
...
}
[BsonDiscriminator("number-parameter")]
public class NumberParameterEntity : ParameterEntity
{
...
}
[BsonDiscriminator("string-parameter")]
public class StringParameterEntity : ParameterEntity
{
...
}
Example of writing with discriminators
Let’s now look at a complete example of a document with three different type parameters, which will be inserted into the collection.
var document = new MyDocumentEntity
{
Id = Guid.NewGuid().ToString(),
Name = "Example Document",
Parameters = new ParameterEntity[]
{
new BooleanParameterEntity
{
Id = "Boolean parameter",
Type = ParameterType.Boolean,
Name = "Enable Feature",
Required = true,
DefaultValue = true
},
new NumberParameterEntity
{
Id = "Number parameter",
Type = ParameterType.Number,
Name = "MaxValue Retries",
Required = false,
DefaultValue = 5,
MinValue = 1,
MaxValue = 10
},
new StringParameterEntity
{
Id = "String parameter",
Type = ParameterType.String,
Name = "Username",
Required = true,
DefaultValue = "user123",
MinLength = 3,
MaxLength = 15
}
}
};
await MyDocumentsCollection.InsertOneAsync(document);
The document saved in MongoDB will have the following structure (JSON), with the discriminator field _t identifying the type of each parameter.
{
"id": "",
"name": "Example Document",
"parameters": [
{
"_t": "boolean-parameter",
"id": "Boolean parameter",
"type": "Boolean",
"name": "Enable Feature",
"required": true,
"defaultValue": true
},
{
"_t": "number-parameter",
"id": "Number parameter",
"type": "Number",
"name": "MaxValue Retries",
"required": false,
"defaultValue": 5,
"minValue": 1,
"maxValue": 10
},
{
"_t": "string-parameter",
"id": "String parameter",
"type": "String",
"name": "Username",
"required": true,
"defaultValue": "user123",
"minLength": 3,
"maxLength": 15
}
]
}
Example of reading with discriminators
When reading the document from the collection, the driver uses the _t field to correctly instantiate polymorphic objects.
var retrievedDocument = await MyDocumentsCollection
.Find(d => d.Id == document.Id)
.FirstOrDefaultAsync();
The driver automatically recognises the _t field and instantiates the correct subclass based on its value.
As shown in the image below, the document is deserialised correctly, with each parameter reconstructed in its specific type.

Global discriminator management
You can also configure the discriminator field globally, which is useful if you want to change the default name _t.
In general, however, it is advisable to keep the default value to ensure document compatibility.
BsonSerializer.RegisterDiscriminatorConvention(
typeof(ParameterEntity),
new ScalarDiscriminatorConvention("typeTag")
);
This way, instead of using _t, MongoDB will store the type in the typeTag field.
Remember that changing the discriminator name may render previous documents incompatible or prevent correct deserialisation if the same convention is not applied throughout the project.
References
For further reading, I invite you to consult the official MongoDB documentation for .NET on discriminators.
The version of the MongoDB .NET driver used in this article is 3.4.3.
Conclusions
Discriminators are a simple but essential tool for managing polymorphism in MongoDB with .NET.
By configuring them correctly, you can keep your data structure consistent and easily extensible, ensuring compatibility with the driver’s automatic deserialisation.