Tim Pohlmann
Tim Pohlmann

Reputation: 4430

How to insert a value in a List nested in a Dictionary (Array of Documents) in MongoDb with the C# driver?

I have the following classes:

class Parent
{
    public ObjectId Id { get; set; }

    [BsonDictionaryOptions(Representation = DictionaryRepresentation.ArrayOfDocuments)]
    public IDictionary<string, List<Child>> Children { get; set; }
}

class Child { }

I know want to add a new Child in one of the entries of the Dictionary:

using MongoDB.Driver;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var collection = new MongoClient("mongodb://localhost:27017")
                .GetDatabase("Test").GetCollection<Parent>("Parents");
            var parent = new Parent();
            collection.InsertOne(parent);

            collection.UpdateOne(p => p.Id == parent.Id, 
                Builders<Parent>.Update.Push(p => p.Children["string1"], new Child()));
        }
    }
}

I get a System.InvalidOperationException:

Unable to determine the serialization information for p => p.Children.get_Item("string1").

I managed to get it to work like this:

collection.UpdateOne(p => p.Id == parent.Id && p.Children.Any(a => a.Key == "string1"),
    Builders<Parent>.Update.Push($"{nameof(Parent.Children)}.$.v", new Child()));

But this is super ugly and only works if the entry "string1" already exists.

This might be related: Unable to determine the serialization information for the expression error

Upvotes: 1

Views: 482

Answers (1)

JoshuaCS
JoshuaCS

Reputation: 2624

I could finally got something working. Here my code (explanation below):

class Parent
{
    public ObjectId Id;

    //Remove the attribute. This field needs to be initialized
    public IDictionary<string, List<Child>> Children = new Dictionary<string, List<Child>>();
}

class Child 
{ 
    public string ChildName;  // This isn't needed
}

class Program
{
    static void Main(string[] args)
    {
        var collection = new MongoClient("mongodb://localhost:27017")
            .GetDatabase("Test").GetCollection<Parent>("Parents");

        var parent = new Parent();

        collection.InsertOne(parent);

        collection.UpdateOne(p => p.Id == parent.Id, 
            Builders<Parent>.Update
                .Push(p => p.Children["string1"], new Child {ChildName = "I am the new guy!"}));
    }
}

There are two things going on here. First, I removed the attribute. Second, that field needs to be initialized with an empty dictionary, otherwise you will get an error. If the key you are setting doesn't exist yet, the associated list will be automatically created. I also added a field to the Child class but that is not needed, it can remain empty.

You can even create multiple keys in the same $push:

collection.UpdateOne(p => p.Id == parent.Id, 
    Builders<Parent>.Update
        .Push(p => p.Children["string1"], new Child {ChildName = "I am the new guy!"})
        .Push(p => p.Children["string2"], new Child {ChildName = "I am a child in string2!"}));

But, and this is an important but, if you try to push to the same key, just the last push will take effect. This won't work:

collection.UpdateOne(p => p.Id == parent.Id, 
    Builders<Parent>.Update
        .Push(p => p.Children["string1"], new Child {ChildName = "I wont be pushed to string1"})
        .Push(p => p.Children["string1"], new Child {ChildName = "I will overwrite the previous push"}));

In those cases, you must use PushEach and pass an array of items. You can combine PushEach with Push:

collection.UpdateOne(p => p.Id == parent.Id, 
    Builders<Parent>.Update
        .PushEach(p => p.Children["string1"], new [] {new Child {ChildName = "I am the first child!"}, new Child {ChildName = "I am the second child!"}})
        .Push(p => p.Children["string2"], new Child {ChildName = "I am a child in string2!"}));

Let me know if this worked for you!

Upvotes: 1

Related Questions