3

How to modify a child element in a child array of a document in MongoDB? I'm using the latest C# Driver for MongoDB (C# Driver Version: 2.5, Server Version: 3.2.0) and I've tried solutions from multiple answers on this from SO (duplicate questions also) and from various other sources. Most of the solutions proved to be obsolete.

Below is my sample document:

[{ 
"_id" : "69c4f77d-05ef-431d-ae17-c076c6173e04",     
"title" : "Root Page", 
"createdAt" : ISODate("2017-12-21T12:28:00.680+0000"), 
"modifiedAt" : ISODate("2017-12-26T07:18:11.165+0000"), 
"pages" : [
    {
        "_id" : "f449dc0b-3d1b-4a59-b622-6a42ce10b147", 
        "title" : "Test page 1", 
        "slug" : "test-page-1", 
        "createdAt" : ISODate("2017-12-21T12:28:00.680+0000"), 
        "modifiedAt" : ISODate("2017-12-21T12:28:00.680+0000")
    }, 
    {
        "_id" : "3d1497d7-f74c-4d88-b15c-bf2f9c736374", 
        "title" : "Test page 2", 
        "slug" : "test-page-2", 
        "createdAt" : ISODate("2017-12-25T11:27:55.006+0000"), 
        "modifiedAt" : ISODate("2017-12-25T11:27:55.006+0000")
    }, 
    {
        "_id" : "6e827e2a-5a25-4343-b646-885816bb8cc4", 
        "title" : "Test page 3", 
        "slug" : "test-page-3", 
        "createdAt" : ISODate("2017-12-25T11:31:16.516+0000"), 
        "modifiedAt" : ISODate("2017-12-25T11:31:16.516+0000")
    }]
},
...,
...
]

Document Structure: The Root Document contains an array of sub documents, which has it's metadata and a list of pages (array)

Inside a Sub Document - I'm trying to update the title and slug of a child page object inside pages array, but have no luck so far with the latest driver.

I'm using the following filter and update query:

        var filter = Builders<SubDocument>.Filter.And(Builders<SubDocument>.Filter.Eq("_id", "xxx"), Builders<SubDocument>.Filter.ElemMatch(x => x.Pages, p => p.Id == "xxx"));

        var update = Builders<SubDocument>.Update
                        .Set("pages.$.title", "changed title")
                        .Set("pages.$.slug", "changed-title")
                        .Set("pages.$.modifiedAt", DateTime.UtcNow);

        _mongoDb.Documents.UpdateOne(filter, update);

Now - this query returns me an error:

Invalid BSON field name pages.$.title

The references I came along for last few hours are saying to use "$" operator but it throws error in latest driver.

[Edit 1] Here's my models:

public class SubDocument
{
    public SubDocument()
    {
        Id = Guid.NewGuid().ToString();
        CreatedAt = DateTime.UtcNow;
        ModifiedAt = DateTime.UtcNow;
        Pages = new List<Page>();
    }

    [BsonId]
    public string Id { get; set; }        

    [BsonElement("title")]
    public string Title { get; set; }        

    [BsonElement("createdAt")]
    public DateTime CreatedAt { get; set; }

    [BsonElement("modifiedAt")]
    public DateTime ModifiedAt { get; set; }

    [BsonElement("pages")]
    public List<Page> Pages { get; set; }


}

public class Page
{
    [BsonId]
    public string Id { get; set; }

    [BsonElement("title")]
    public string Title { get; set; }

    [BsonElement("slug")]
    public string Slug { get; set; }

    [BsonElement("createdAt")]
    public DateTime CreatedAt { get; set; }

    [BsonElement("modifiedAt")]
    public DateTime ModifiedAt { get; set; }

}

Anyone? Thanks in advance!

12
  • Which element you need to update and to which value? Or all pages? Do you have any filter? Commented Dec 26, 2017 at 10:30
  • Hi @Evk, I've updated the question, PS, thanks! Commented Dec 26, 2017 at 10:40
  • Can you also include model for your SubDocument? Just so that one can copy paste it Commented Dec 26, 2017 at 10:42
  • 1
    Anyway, try doing this: .Set(c => c.Pages[-1].Title, "changed title") (and the same for other fields). Commented Dec 26, 2017 at 11:03
  • 1
    The same code you posted (as well as my suggestion) works for me with driver version 2.5 and mongo version 3.4 Commented Dec 26, 2017 at 12:05

1 Answer 1

4

Your code actually does work with mongod v3.4 and driver v2.5.

As suggested by Evk within comments, here is the strongly-typed version of your code, which is refactoring friendly.

var filter = Builders<SubDocument>.Filter
    .And(
        Builders<SubDocument>.Filter.Eq(d => d.Id, "69c4f77d-05ef-431d-ae17-c076c6173e04"), 
        Builders<SubDocument>.Filter.ElemMatch(x => x.Pages, p => p.Id == "3d1497d7-f74c-4d88-b15c-bf2f9c736374"));

var update = Builders<SubDocument>.Update
                .Set(c => c.Pages[-1].Title, "changed title")
                .Set(c => c.Pages[-1].Slug, "changed slug")
                .Set(c => c.Pages[-1].ModifiedAt, DateTime.UtcNow);

subDocumentsCollection.UpdateOne(filter, update);

A different approach might consider your subdocument entity as a whole (document oriented databases encourage this approach), thus always updating the entity altogether and avoiding fine-grained updates (that are more likely in case of R-DBMSs). In this case, you might write something like this.

var document = subDocumentsCollection
    .Find(d => d.Id == "69c4f77d-05ef-431d-ae17-c076c6173e04")
    .Single();

var page = document.Pages.Single(p => p.Id == "3d1497d7-f74c-4d88-b15c-bf2f9c736374");

page.Title = "changed title";
page.Slug = "changed slug";
page.ModifiedAt = DateTime.UtcNow;

subDocumentsCollection.ReplaceOne(d => d.Id == document.Id, document);

This second approach has the following PROs and CONs.

PROs

  • the update happens in the domain-layer (possibly through well suited methods), thus allowing enforcement of all domain rules and preventing dirty data to go into the database; thus it is more object-oriented.
  • Since code is domain-oriented, it is much simpler and does not depend on database internals.

CONs

  • this approach requires two hits on the database (one for the read and one for the update);
  • in case your entity is large, network has to transport more data than strictly needed.

Considering that premature optimization is the root of all evil (or at least most of it) in programming (here), in general I would go for the second approach, falling back to the first one only in case of very strict performance requirements or low network bandwidth.

Sign up to request clarification or add additional context in comments.

5 Comments

@Marcello- Excellent answer
@Marcello, you're example appears to update the parent document - which would have potential for conflict. Am I misunderstanding?
@Chazt3n Yes. Anyway, the first approach might lead to update conflicts, too (e.g. two users simultaneously updating page.Title in a sub-document). In the second approach the potential conflict perimeter is wider. In case of such concurrency problems, a locking strategy should be used (e.g. these).
I need to update All Childs Which matches the criteria?
the second approach can be wrapped within transaction sadly, however, mongodb performance seems to drop significantly when it comes to transaction for cross multiple collections

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.