3 / 3
Mar 17

Hi!
I recently came across some odd behaviour during an upsert while using FindOneAndUpdateAsync.

For some reason, the BsonType of a string property with null value is being converted from BsonNull to BsonString and, consequently, the property is being saved with the value of BsonNull, instead of null!

The core logic is similar to the below snippet

async Task FindOneAndUpdateAsync<T>(T document, FilterDefinition<T> filter = null) { var bsonDocument = document.ToBsonDocument(); if (filter == null) { var bsonValue = bsonDocument["_id"]; filter = Builders<T>.Filter.Eq("_id", bsonValue); } var propertiesToIgnore = new List<string> { "_t" }; if (bsonDocument.GetValue("_id", null) == null) { propertiesToIgnore.Add("_id"); } var updates = bsonDocument .Where(a => !propertiesToIgnore.Contains(a.Name)) .Select(a => Builders<T>.Update.Set(a.Name, a.Value)); await GetCollection<T>().FindOneAndUpdateAsync( filter, Builders<T>.Update.Combine(updates), new FindOneAndUpdateOptions<T> { IsUpsert = true }); }

Is anyone able to shed some light into why this is happening?

Here is a link to the working example.

Thank you.

14 days later

Hi @Jose_Mira1

The builders we provide for building filter definitions or update operations have both a type-safe interface such as Builders<T>.Update.Set(t => t.Url, "sample.org")) and string-based interface such as Builders<T>.Update.Set("Url", "sample.org")). The type-safe interface provides compile-time safety while the string-based interface doesn’t which allows for weird things to happen at runtime if the user didn’t make sure their field type and value type match.

In the snippet you provided, you converted the document to a BsonDocument which will convert the null value of the Url field to a BsonNull as that is how nulls are represented in a BsonDocument.

var updates = bsonDocument .Where(a => !propertiesToIgnore.Contains(a.Name)) .Select(a => Builders<T>.Update.Set(a.Name, a.Value));

In this section above, you are using the string-based interface for the update definition where for the case of the Url field, it’ll be Builders<T>.Update.Set("Url", BsonNull)). The type of Url for T is string but we are trying to assign a BsonNull to it. Now here is where the driver is doing something wrong. It should ideally throw an exception as there is no conversion from BsonNull to string but it instead uses the string representation of BsonNull.

We’ll probably make a ticket to have the driver throw in such situations. In your snippet I don’t know why you are converting your document to a BsonDocument, but you should avoid converting it and use the type-safe interface with the builders. In general, I recommend always using the type-safe interface when working with POCOs (Plain Old CLR Objects). The string-based interface should really only be used when working with BsonDocuments.

Hi @Adelin_Mbida_Owona

Thank you for the reply and clarification.
We have since moved away from the above implementation, and we are now using typed interfaces.

In regards to your comments, just one question. You say

It should ideally throw an exception as there is no conversion from BsonNull to string

In C# null is a valid value for a property/field of type string. As such, instead of throwing should it not allow null?