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:password");
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.
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:password");
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:
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:password");
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:
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.
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);
}
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.
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.