The problem

Given the following two classes:

[BsonDiscriminator(RootClass = true)]
[BsonKnownTypes(typeof(MongoDbSomeModel))]
[BsonIgnoreExtraElements]
public class MongoDbBaseModel
{
    [BsonId]
    public string Id { get; set; }
    public string Name { get; set; }
}

public class MongoDbSomeModel : MongoDbBaseModel
{
    public string SomeProperty { get; set; }
}

When inserting a MongoDbSomeModel to the database, it should contain a _t field in the database with the following value:

_t: ["MongoDbBaseModel", "MongoDbSomeModel"]

Let's test this by running the following code:

var settings = MongoClientSettings.FromConnectionString($"mongodb://username:[email protected]");
settings.ConnectTimeout = TimeSpan.FromSeconds(5);
var client = new MongoClient(settings);
await client.DropDatabaseAsync("test");
var database = client.GetDatabase("test");
var collection = database.GetCollection<MongoDbSomeModel>("test");
var bulkOperations = new List<WriteModel<MongoDbSomeModel>>();

var items = new List<MongoDbSomeModel>
{
    new MongoDbSomeModel{Id = "4", Name = "Name 4 INSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
    new MongoDbSomeModel{Id = "5", Name = "Name 5 INSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
    new MongoDbSomeModel{Id = "6", Name = "Name 6 INSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")}
};

foreach (var record in items)
{
    var insertOperation = new InsertOneModel<MongoDbSomeModel>(record);
    bulkOperations.Add(insertOperation);
}

await collection.BulkWriteAsync(bulkOperations);

We create some items and save them to the database in a BulkWrite. If we look in the database, we can see that the _t field is present and populated with the correct values.

with-types

Now, imagine that I would like to do an upsert instead. If the document already exist, I only want to update the existing properties. The reason for using UpdateOneModel instead of ReplaceOneModel is that I write to the same document from multiple applications. If I use a ReplaceOneModel, all the data coming from the other applications would be removed. One could argue that this is not optimal (writing to the same document from multiple applications) but I have my reasons, period. :)

So, the code using UpdateOneModel looks like this:

var settings = MongoClientSettings.FromConnectionString($"mongodb://username:[email protected]");
settings.ConnectTimeout = TimeSpan.FromSeconds(5);
var client = new MongoClient(settings);
await client.DropDatabaseAsync("test");
var database = client.GetDatabase("test");
var collection = database.GetCollection<MongoDbSomeModel>("test");
var bulkOperations = new List<WriteModel<MongoDbSomeModel>>();

var items = new List<MongoDbSomeModel>
{
    new MongoDbSomeModel{Id = "1", Name = "Name 1 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
    new MongoDbSomeModel{Id = "2", Name = "Name 2 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
    new MongoDbSomeModel{Id = "3", Name = "Name 3 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")}
};

foreach (var record in items)
{
    var updateDefinition = new UpdateDefinitionBuilder<MongoDbSomeModel>()
        .Set(x => x.Name, record.Name)
        .Set(x => x.SomeProperty, record.SomeProperty);
    var updateOperation = new UpdateOneModel<MongoDbSomeModel>(
        Builders<MongoDbSomeModel>.Filter.Eq(x => x.Id, record.Id),
        updateDefinition)
    {
        IsUpsert = true
    };

    bulkOperations.Add(updateOperation);
}

await collection.BulkWriteAsync(bulkOperations);

In the above code I basically say: If the document already exists, update the Name and SomeProperty, if it doesn't exists, create the document (upsert = true).

So, let's have a look in the database:
no-types

As you can see, the _t field is missing. I will show you why this is a problem (and also how I noticed that I had this problem).

Let's run the following code:

static async Task Main(string[] args)
{
    var settings = MongoClientSettings.FromConnectionString($"mongodb://username:[email protected]");
    settings.ConnectTimeout = TimeSpan.FromSeconds(5);
    var client = new MongoClient(settings);

    await client.DropDatabaseAsync("test");
    var database = client.GetDatabase("test");
    var collection = database.GetCollection<MongoDbSomeModel>("test");

    var bulkOperations = new List<WriteModel<MongoDbSomeModel>>();
    AddUpdateOneModels(bulkOperations);
    AddInsertOneModels(bulkOperations);

    var bulkResult = await collection.BulkWriteAsync(bulkOperations);
}

private static void AddUpdateOneModels(List<WriteModel<MongoDbSomeModel>> bulkOperations)
{
    var items = new List<MongoDbSomeModel>
    {
        new MongoDbSomeModel{Id = "1", Name = "Name 1 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
        new MongoDbSomeModel{Id = "2", Name = "Name 2 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
        new MongoDbSomeModel{Id = "3", Name = "Name 3 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")}
     };
    foreach (var record in items)
    {
        var updateDefinition = new UpdateDefinitionBuilder<MongoDbSomeModel>()
            .Set(x => x.Name, record.Name)
            .Set(x => x.SomeProperty, record.SomeProperty);
        var updateOperation = new UpdateOneModel<MongoDbSomeModel>(
            Builders<MongoDbSomeModel>.Filter.Eq(x => x.Id, record.Id),
            updateDefinition)
        {
            IsUpsert = true
        };

        bulkOperations.Add(updateOperation);
    }
}

private static void AddInsertOneModels(List<WriteModel<MongoDbSomeModel>> bulkOperations)
{
    var items = new List<MongoDbSomeModel>
    {
        new MongoDbSomeModel{Id = "4", Name = "Name 4 INSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
        new MongoDbSomeModel{Id = "5", Name = "Name 5 INSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
        new MongoDbSomeModel{Id = "6", Name = "Name 6 INSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")}
    };

    foreach (var record in items)
    {
        var insertOperation = new InsertOneModel<MongoDbSomeModel>(record);
        bulkOperations.Add(insertOperation);
    }
}

In the above code I insert 3 items using UpdateOneModel upsert and 3 items using InsertOneModel.
It produces the following result:
both-runs
The first three items is missing the _t field.

Why is this a problem?

Well imagine that you have the following code:

var baseCollection = database.GetCollection<MongoDbBaseModel>("test");
var items = await baseCollection.Find(x => true).ToListAsync();

foreach (var item in items)
{
    switch (item)
    {
        case MongoDbSomeModel someModel:
            DoSomethingWithSomeModel(someModel)
            break;
        default:
            throw new Exception($"We don't support '{item.GetType().Name}'");
    }
}

I'm using pattern matching here to do something when item == MongoDbSomeModel. Note that I expect all the items to be of the MongoDbSomeModel type since that's the type I've inserted to the database.

But this code will throw an exception since the _t field is missing for the first 3 items.
exception

Solution (workarounds)

Now, I'm not sure if this behaviour is a bug or if it's working as intended, either way, I need to use UpdateOneModel and have the _t field written to the document.

ReplaceOneModel

If you don't write to the same document from multiple places/applications, you could use ReplaceOneModel instead of UpdateOneModel.

var items = new List<MongoDbSomeModel>
{
    new MongoDbSomeModel{Id = "1", Name = "Name 1 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
    new MongoDbSomeModel{Id = "2", Name = "Name 2 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
    new MongoDbSomeModel{Id = "3", Name = "Name 3 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")}
};

foreach (var record in items)
{
    var updateOperation = new ReplaceOneModel<MongoDbSomeModel>(
        Builders<MongoDbSomeModel>.Filter.Eq(x => x.Id, record.Id),
    record)
    {
        IsUpsert = true
    };

    bulkOperations.Add(updateOperation);
}

replaceonemodel
As you can see, when using ReplaceOneModel, the _t field is added to the document.

Append it yourself!

Since I couldn't use ReplaceOneModel, I went for this approach. I generate the array myself and append it in the UpdateDefinition.

var items = new List<MongoDbSomeModel>
{
    new MongoDbSomeModel{Id = "1", Name = "Name 1 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
    new MongoDbSomeModel{Id = "2", Name = "Name 2 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")},
    new MongoDbSomeModel{Id = "3", Name = "Name 3 UPSERT", SomeProperty = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss")}
};

foreach (var record in items)
{
    var updateDefinition = new UpdateDefinitionBuilder<MongoDbSomeModel>()
        .Set("_t", StandardDiscriminatorConvention.Hierarchical.GetDiscriminator(typeof(MongoDbBaseModel), typeof(MongoDbSomeModel)))
        .Set(x => x.Name, record.Name)
        .Set(x => x.SomeProperty, record.SomeProperty);
        var updateOperation = new UpdateOneModel<MongoDbSomeModel>(
            Builders<MongoDbSomeModel>.Filter.Eq(x => x.Id, record.Id),
            updateDefinition)
        {
            IsUpsert = true
        };

    bulkOperations.Add(updateOperation);
}

I use the StandardDiscriminatorConvention to generate the array, I need to send in the base type and the actual type.

After running the code again, we can see that the _t field is now present in all documents.
discriminator-present-in-all-documents

Now, as I said before, I'm not sure if this is a bug or if it's working as intended, but to me it feels like a bug, especially since ReplaceOneModel correctly appends the _t field.

A small repro can be found here.