2

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!

Sunny Sharma
  • 4,688
  • 5
  • 35
  • 73
  • Which element you need to update and to which value? Or all pages? Do you have any filter? – Evk Dec 26 '17 at 10:30
  • Hi @Evk, I've updated the question, PS, thanks! – Sunny Sharma Dec 26 '17 at 10:40
  • Can you also include model for your SubDocument? Just so that one can copy paste it – Evk Dec 26 '17 at 10:42
  • sure @Evk, done! – Sunny Sharma Dec 26 '17 at 10:48
  • You are saying that you use driver version 4.0, but there is no such version, latest if 2.5 as far as I know. – Evk Dec 26 '17 at 11:00
  • 1
    Anyway, try doing this: `.Set(c => c.Pages[-1].Title, "changed title")` (and the same for other fields). – Evk Dec 26 '17 at 11:03
  • I checked the version in Visual Studio and it says 4.0.0.0... it should be 2.5 then I've freshly downloaded the latest one only. I've tried the syntax you're asking for, but it throws up the same $ error "Invalid BSON field name pages.$.title" – Sunny Sharma Dec 26 '17 at 11:12
  • I cannot actually reproduce this, with both syntaxes it works for me. And what is version of mongo server? – Evk Dec 26 '17 at 11:13
  • that's wierd! Mongo version is : 3.2.0 – Sunny Sharma Dec 26 '17 at 11:29
  • can you please share what's working for you? I've tried to get this thing working with C# Driver and mongo shell both... with the similar syntax but no luck yet. – Sunny Sharma Dec 26 '17 at 11:32
  • 1
    The same code you posted (as well as my suggestion) works for me with driver version 2.5 and mongo version 3.4 – Evk Dec 26 '17 at 12:05
  • What if i need to update multiple objects ? every child element which matches the filter? – Hammad Sajid Apr 10 '20 at 20:01

1 Answers1

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.

Marcello
  • 879
  • 6
  • 20
  • @Marcello- Excellent answer – Shakeer Hussain Dec 27 '18 at 21:09
  • 1
    @Marcello, you're example appears to update the parent document - which would have potential for conflict. Am I misunderstanding? – Chazt3n May 07 '19 at 20:40
  • 1
    @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](https://stackoverflow.com/a/129397/1045789)). – Marcello Jan 01 '20 at 15:03
  • I need to update All Childs Which matches the criteria? – Hammad Sajid Apr 10 '20 at 20:14