[CSHARP-1984] String array is not serialized when casted to object Created: 15/May/17  Updated: 20/Jan/23  Resolved: 07/Apr/21

Status: Closed
Project: C# Driver
Component/s: Serialization
Affects Version/s: 2.4.3
Fix Version/s: None

Type: Bug Priority: Major - P3
Reporter: Andrius Zalimas Assignee: Robert Stam
Resolution: Duplicate Votes: 5
Labels: None
Remaining Estimate: Not Specified
Time Spent: Not Specified
Original Estimate: Not Specified
Environment:

.NET 4.5.1. We are upgrading the driver from v1.8.x. Project infrastructure does not allow generics yet so cast to object is used as a workaround.


Issue Links:
Duplicate
duplicates CSHARP-2215 Update.Set can generate invalid $set ... Closed
Related
related to CSHARP-2215 Update.Set can generate invalid $set ... Closed
Epic Link: Improve Serialization
Case:

 Description   

Trying to build update command with $set operator using UpdateDefinitionBuilder. However it does not serialize string arrays when they are casted to object. Instead it gives "(Collection)".
This works fine in v2.4.2. But is broken in v2.4.3.
Looks like it works fine with other arrays too. Problem is just with string arrays.

Here is simple console app to show to issue:

class Program
    {
        static void Main(string[] args)
        {
            var updateBuilder = new UpdateDefinitionBuilder<Item>();
            var fooValue = new List<int> { 11, 22, 33 };
            var barValue = new List<string> { "Bar1", "Bar2", "Bar3" };
            
            Console.WriteLine(Render(updateBuilder.Set<List<int>>(x => x.Foo, fooValue)));
            Console.WriteLine(Render(updateBuilder.Set<List<string>>(x => x.Bar, barValue)));
            Console.WriteLine(Render(updateBuilder.Set<object>(x => x.Foo, fooValue)));
            Console.WriteLine(Render(updateBuilder.Set<object>(x => x.Bar, barValue)));
            Console.ReadKey();
        }
 
        public class Item
        {
            public Guid Id { get; set; }
            public List<int> Foo { get; set; }
            public List<string> Bar { get; set; }
        }
 
        private static string Render(UpdateDefinition<Item> definition)
        {
            var settings = new MongoCollectionSettings();
            var serializer = settings.SerializerRegistry.GetSerializer<Item>();
            return definition.Render(serializer, settings.SerializerRegistry).ToString();
        }
    }

Output with v2.4.2:

{ "$set" : { "Foo" : [11, 22, 33] } }
{ "$set" : { "Bar" : ["Bar1", "Bar2", "Bar3"] } }
{ "$set" : { "Foo" : { "_t" : "System.Collections.Generic.List`1[System.Int32]", "_v" : [11, 22, 33] } } }
{ "$set" : { "Bar" : { "_t" : "System.Collections.Generic.List`1[System.String]", "_v" : ["Bar1", "Bar2", "Bar3"] } } }

Output with v2.4.3:

{ "$set" : { "Foo" : [11, 22, 33] } }
{ "$set" : { "Bar" : ["Bar1", "Bar2", "Bar3"] } }
{ "$set" : { "Foo" : { "_t" : "System.Collections.Generic.List`1[System.Int32]", "_v" : [11, 22, 33] } } }
{ "$set" : { "Bar" : "(Collection)" } }



 Comments   
Comment by Robert Stam [ 07/Apr/21 ]

Fixed by CSHARP-2215.

Comment by Robert Stam [ 07/Apr/21 ]

It was fixed in CSHARP-2215. Closing this ticket as a duplicate.

Comment by Robert Stam [ 07/Apr/21 ]

Output using 2.7.3:

{ "$set" : { "Foo" : [11, 22, 33] } }
{ "$set" : { "Bar" : ["Bar1", "Bar2", "Bar3"] } }
{ "$set" : { "Foo" : { "_t" : "System.Collections.Generic.List`1[System.Int32]", "_v" : [11, 22, 33] } } }
{ "$set" : { "Bar" : "(Collection)" } }

Output using 2.8.0:

{ "$set" : { "Foo" : [11, 22, 33] } }
{ "$set" : { "Bar" : ["Bar1", "Bar2", "Bar3"] } }
{ "$set" : { "Foo" : [11, 22, 33] } }
{ "$set" : { "Bar" : ["Bar1", "Bar2", "Bar3"] } }

So the issue appears to be fixed in 2.8.0.

Will try and find what ticket fixed it.

 

Comment by Ashwin Lingannagari [ 10/Dec/18 ]

Same issue here. This is still open for so long. We replaced Update with ReplaceOne. Unfortunately, replace replaces one document at a time but we have the abstracted API to update fields of multiple documents at the same time. So, the number of transactions to update multiple documents significantly increased. I wish this issue gets fixed soon.

Mongo DB driver - 2.7

Comment by Roman Zhyliov [ 20/Sep/18 ]

Surprised to see this still open for a year. Having exactly same issue with 2.7 which blocks our production completely from having and abstracted updates for dynamic schemas functionality. The whole .Update.Set() logic in my mind is broken.

Developer should be able to set a Merge like updates. I.e. -> Update/Merge only the fields my code/schema knows about and don't replace the rest

And using reflections like that (GetType().GetProperty) makes our code less performant.

Currently due to this bug we are forced to use ReplaceOneAsync and cannot afford to have different versions of schemas in remote without some wacky hacks and workarounds.

Most likely will be switching to some alternative drivers with better native js driver support (cuz you can clearly do any merge updates with the native syntax)

Comment by John Mills [ 10/Aug/18 ]

I'm having a similar issue as well with string fields! Whenever the field is null, the string content "BsonNull" is being stored forcing the field type to become string instead of null. This is causing all sorts of issues with lookup and retrieval since we are treating that as a normal string value and cannot tell tha tit is supposed to be null!

Please fix ASAP!

 

MongoDB Driver v2.7 / mongoDB v3.6 & 4.0

 

// fragment using LINQPad
 
public class Foo
{
    public ObjectId Id { get; set; }
    public int A { get; private set; }
    public string B { get; set; }
    public Bar C { get; set; }
}
 
public class Bar
{
    public double? D { get; set; }    
}
 
void Main()
{
    var firstFoo = new Foo { C = new Bar() };
    
    var bsonDoc = firstFoo.ToBsonDocument();
  
    bsonDoc.ToString().Dump("BSON Doc");
    
    bsonDoc.ToJson().Dump("JSON doc");
 
    var fooBack = BsonSerializer.Deserialize<Foo>(bsonDoc)
        .ToJson()
        .Dump("Foo back");
 
 
    UpdateDefinitionBuilder<Foo> updateBuilder = Builders<Foo>.Update;
 
    IList<UpdateDefinition<Foo>> fieldAndValueList = bsonDoc.Select(item =>
            {
                return updateBuilder.Set(item.Name, item.Value);
            })
        .ToList();
 
    var updater = updateBuilder.Combine(fieldAndValueList);
 
    var renderedUpdater = updater.Render(
        BsonSerializer.SerializerRegistry.GetSerializer<Foo>(), 
        BsonSerializer.SerializerRegistry);
    
    renderedUpdater.ToString().Dump("Rendered Updater");
    
 
// store in mongodb
    var client = new MongoClient("mongodb://localhost:27017/ContentTests");
 
    var database = client.GetDatabase("ContentTests");
 
    var collection = database.GetCollection<Foo>("properties");
 
    collection.UpdateOne(
            Builders<Foo>.Filter.Eq(f => f.A, 4), 
            updater,
            new UpdateOptions
            {
                IsUpsert = true
            });
    
// retrieve it    
    collection.Find(f => f.A == 0)
        .FirstOrDefault()
        .ToJson()
        .Dump("found in mongodb");
}

 Output:

 Notice that the rendered updater and retrieved object both have the text "BsonNull" for the string field named 'B'. Robo3T also shows the field type as 'String' instead of 'Null'.

BSON Doc
 
{ "_id" : ObjectId("000000000000000000000000"), "A" : 0, "B" : null, "C" : { "D" : null } } 
 
 
JSON doc
 
{ "_id" : ObjectId("000000000000000000000000"), "A" : 0, "B" : null, "C" : { "D" : null } } 
 
 
Foo back
 
{ "_id" : ObjectId("000000000000000000000000"), "A" : 0, "B" : null, "C" : { "D" : null } } 
 
 
Rendered Updater
 
{ "$set" : { "_id" : ObjectId("000000000000000000000000"), "A" : 0, "B" : "BsonNull", "C" : { "D" : null } } } 
 
 
found in mongodb
 
{ "_id" : ObjectId("000000000000000000000000"), "A" : 0, "B" : "BsonNull", "C" : { "D" : null } } 

 

Comment by Corvino Fabio [ 19/Jun/18 ]

Same issue. 

MongoDB version 3.6

Comment by Maksim Kislyakov [ 28/Feb/18 ]

Hello everyone. I got the same bug too. I use MongoDB version 2.5.0. I have the following extension method and when i try to update array of strings driver writes to db just string "String[]", but not actual array. Is there any news about fixing this bug? Or is it not a bug, but a normal situation?

public static UpdateDefinition<T> SetAll<T>(this UpdateDefinitionBuilder<T> builder, T model, params Expression<Func<T, object>>[] fields)
        {
            if (fields.Length == 0)
                return null;
 
            UpdateDefinition<T> result = null;
 
            foreach(var field in fields)
            {
                Func<T, object> func = field.Compile();
                object value = func(model);
 
                if (result == null)
                    result = builder.Set(field, value);
                else
                    result = result.Set(field, value);
            }
 
            return result;
        }

Comment by John Bencina [ 28/Jan/18 ]

I'm running into this issue as well. In the C# driver, I use reflection to only update non-null fields. However, my object has List<string> property that's being saved as "(Collection)" in MongoDB. My hacky workaround is to recreate the list from scratch.

        public override MyObj Update(MyObj model)
        {
            var builder = Builders<MyObj>.Update;
            var builder_def = builder.Set(x => x.Id, model.Id);
 
            foreach (PropertyInfo prop in model.GetType().GetProperties())
            {
                var value = model.GetType().GetProperty(prop.Name).GetValue(model, null);
 
                if (value != null)
                {
                    builder_def = builder_def.Set(prop.Name, value); // Not setting lists correctly
                }
            }
 
            var filter = Builders<MyObj>.Filter;
            var filter_def = filter.Eq(x => x.Id, model.Id);
 
            Connection.Update(filter_def, builder_def);
 
            return model;
        }

Comment by Andrius Zalimas [ 18/May/17 ]

I agree that behavior in v2.4.2 is not a bug. Problem is only in v2.4.3. But it would be nice feature if driver recognized actual derived type and ignored generic.

Reason for using <object> is because we have an old project and want to upgrade mongo server. To do this we need to upgrade driver first. But our data access layer has features that are not compatible with generic types. We only have object values and some complex conversion to old Bson document (of v1.8.1).
We considered using reflection to build generic definitions or even continue building BsonDocument manually. But better option was to rewrite old DAL to support generics what we done eventually because we hit another driver issue with discriminators (explained below). So at this point no longer a problem for us.

Another issue (should I create another ticket?) :
When using $set operator with array cast to object it creates this structure in database:

{ _t: "Namespace.Type", _v: [array] }

Then it is no longer possible to use $push operations to this array.
I get this error: The field 'Foo' must be an array but is of type object in document {_id: BinData(3, EF7E2ABE43D7244399EBD971B099F270)}'
Not sure if this behavior is actually a bug. It could be my limited knowledge of discriminators.

Here is small console app to reproduce this. (MongoDB v3.4.4, MongoDriver v2.4.x):

    class Program
    {
        static void Main(string[] args)
        {
            var client = new MongoClient("mongodb://localhost");
            client.DropDatabase("MongoSandbox");
 
            var database = client.GetDatabase("MongoSandbox");
            database.CreateCollection("Item");
            var collection = database.GetCollection<Item>("Item");
 
            var updateBuilder = new UpdateDefinitionBuilder<Item>();
            var fooValue = new List<int> {11, 22, 33};
            var id = Guid.NewGuid();
 
            var definition = updateBuilder.Set<object>(x => x.Foo, fooValue);
            collection.UpdateOne(x => x.Id == id, definition, new UpdateOptions { IsUpsert = true });
 
            definition = updateBuilder.Push(x => x.Foo, 99);
            collection.UpdateOne(x => x.Id == id, definition, new UpdateOptions { IsUpsert = true }); // error here
 
            Console.ReadKey();
        }
 
        public class Item
        {
            public Guid Id { get; set; }
            public List<int> Foo { get; set; }
        }
 
    }

Comment by Robert Stam [ 16/May/17 ]

I can reproduce this.

But I would argue that the v2.4.2 behavior is also a bug. Seems like the desired behavior is that the value would be serialized the same in both cases.

Why are you using Set<object>?

The confusion is arising because the Set statement is stating that the type of the field is object when the actual type of the field is List<int> or List<string>.

Comment by Andrius Zalimas [ 15/May/17 ]

It would also be nice if driver used actual type (not generic cast) when making a decision to add or not the discriminator.

Generated at Wed Feb 07 21:41:16 UTC 2024 using Jira 9.7.1#970001-sha1:2222b88b221c4928ef0de3161136cc90c8356a66.