6

I would like to update only a single field of a document. I mean to understand that I have to use UpdateOneAsync. When I try to do this, I always get MongoDB.Bson.BsonSerializationException : Element name 'Test' is not valid'..

The following code reproduces my problem (xUnit, .NET Core, MongoDb in Docker).

public class Fixture
{
    public class DummyDocument : IDocument
    {
        public string Test { get; set; }

        public Guid Id { get; set; }

        public int Version { get; set; }
    }

    [Fact]
    public async Task Repro()
    {
        var db = new MongoDbContext("mongodb://localhost:27017", "myDb");
        var document = new DummyDocument { Test = "abc", Id = Guid.Parse("69695d2c-90e7-4a4c-b478-6c8fb2a1dc5c") };
        await db.GetCollection<DummyDocument>().InsertOneAsync(document);
        FilterDefinition<DummyDocument> u = new ExpressionFilterDefinition<DummyDocument>(d => d.Id == document.Id);
        await db.GetCollection<DummyDocument>().UpdateOneAsync(u, new ObjectUpdateDefinition<DummyDocument>(new DummyDocument { Test = "bla" }));
    }
}
msallin
  • 862
  • 1
  • 8
  • 16

2 Answers2

10

Recently I have experimented with implementing UpdateOneAsync method into my own project, for learning purposes, and believe the insights I have acquired may be helpful to msallin and others. I also relate to the usage of UpdateManyAsync.

From msallin's post I get the impression that the desired functionality envolves around updating one or more of the fields of a given object class. The comment by crgolden (and the comments therein) confirm this, expressing a need of not replacing the entire document (within the database) but rather fields pertaining to object classes that are stored within. There is a also a need of learning about the usage of the ObjectUpdateDefinition, which I am unfamiliar with and dont work directly with. Therefore, I will display how the former functionality can be implemented.

To cover potential usecases for msallin I will show how fields on both parent and child classes can be updated. Every example shown has been run and found to be functional as of writing this post.

Connection info (using MongoDB.Driver v2.9.2), and parent and child class structure:

connectionString = "mongodb://localhost:27017";
client = new MongoClient(connectionString);
database = client.GetDatabase("authors_and_books");      // rename as appropriate
collection = database.GetCollection<Author>("authors");  // rename as appropriate

public class Author // Parent class
{
    public Guid Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string LifeExpectancy { get; set; }
    public IList<Book> Books { get; set; }
    = new List<Book>();
}

public class Book // Child class
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public Guid ParentId { get; set; }
}

Assumptions:

  • The database (here: authors_and_books) contains a collection named authors, which contains one or more Author class objects.
  • Each Author class object contains a list of Book class objects. The list may be empty, or filled with one or more Book class objects.
  • The collection authors is already filled with some Author class objects, for testing purposes
  • The ParentId property of any Book object is always the same as the Id of the enclosing parent class. This is maintained on the repository level, when inserting Author objects into the authors collection, or for instance when initializing the database with test data. The Guid identifier serves as a unique link between parent and child classes, which may come in handy.
  • The Id property of Author class objects is generated when a new author is added to the collection, such as by using Guid.NewGuid().
  • We assume a list of Book objects, IEnumerable<Book>, that we wish to add to the Books collection of some authors. We assume we want to append these to any books already present, if any, instead of replacing the ones already there. Other modifications will also be made.
  • The modifications made are meant to display method functionality and may not always be contextually sensible.
  • We run these I/O-bound operations asynchronously, using async and await, and Task. This prevents the execution thread from locking up when there are multiple calls to the same method, compared to running it synchronously. For instance, instead of AddBooksCollection(IEnumerable<Book> bookEntities) we use async Task AddBooksCollection(IEnumerable<Book> bookEntities) when declaring the class method.

Test data: Parent author classes, to initialize database
Insert them into our collection with ie. InsertMany or InsertManyAsync.

var test_data = new List<DbAuthor>()
{
    new Author()
    {
        Id = new Guid("6f51ea64-6d46-4eb2-b5d5-c92cdaf3260c"),
        FirstName = "Owen",
        LastName = "King",
        LifeExpectancy = "30 years",
        Version = 1
    },
    new Author()
    {
        Id = new Guid("25320c5e-f58a-4b1f-b63a-8ee07a840bdf"),
        FirstName = "Stephen",
        LastName = "Fry",
        LifeExpectancy = "50 years",
        Version = 1,
        Books = new List<Book>()
        {
            new Book()
            {
                Id = new Guid("c7ba6add-09c4-45f8-8dd0-eaca221e5d93"),
                Title = "The Shining Doorstep",
                Description = "The amenable kitchen doorsteps kills again..",
                ParentId = new Guid("25320c5e-f58a-4b1f-b63a-8ee07a840bdf")
            }
        }
    },
    new Author()
    {
        Id = new Guid("76053df4-6687-4353-8937-b45556748abe"),
        FirstName = "Johnny",
        LastName = "King",
        LifeExpectancy = "13 years",
        Version = 1,
        Books = new List<Book>()
        {
            new Book()
            {
                Id = new Guid("447eb762-95e9-4c31-95e1-b20053fbe215"),
                Title = "A Game of Machines",
                Description = "The book just 'works'..",
                ParentId = new Guid("76053df4-6687-4353-8937-b45556748abe")
            },
            new Book()
            {
                Id = new Guid("bc4c35c3-3857-4250-9449-155fcf5109ec"),
                Title = "The Winds of the Sea",
                Description = "Forthcoming 6th novel in..",
                ParentId = new Guid("76053df4-6687-4353-8937-b45556748abe")
            }
        }
    },
    new Author()
    {
        Id = new Guid("412c3012-d891-4f5e-9613-ff7aa63e6bb3"),
        FirstName = "Nan",
        LastName = "Brahman",
        LifeExpectancy = "30 years",
        Version = 1,
        Books = new List<Book>()
        {
            new Book()
            {
                Id = new Guid("9edf91ee-ab77-4521-a402-5f188bc0c577"),
                Title = "Unakien Godlings",
                Description = "If only crusty bread were 'this' good..",
                ParentId = new Guid("412c3012-d891-4f5e-9613-ff7aa63e6bb3")
            }
        }
    }
};

Test data: Child book classes, to add to existing authors
Some of the books have the same ParentId as others. This is intentional.

var bookEntities = new List<Book>()
{
    new Book()
    {
        Id = new Guid("2ee90507-8952-481e-8866-4968cdd87e74"),
        Title = "My First Book",
        Description = "A book that makes you fall asleep and..",
        ParentId = new Guid("6f51ea64-6d46-4eb2-b5d5-c92cdaf3260c")
    },
    new Book()
    {
        Id = new Guid("da89edbd-e8cd-4fde-ab73-4ef648041697"),
        Title = "Book about trees",
        Description = "Forthcoming 100th novel in Some Thing.",
        ParentId = new Guid("412c3012-d891-4f5e-9613-ff7aa63e6bb3")
    },
    new Book()
    {
        Id = new Guid("db76a03d-6503-4750-84d0-9efd64d9a60b"),
        Title = "Book about tree saplings",
        Description = "Impressive 101th novel in Some Thing",
        ParentId = new Guid("412c3012-d891-4f5e-9613-ff7aa63e6bb3")
    },
    new Book()
    {
        Id = new Guid("ee2d2593-5b22-453b-af2f-bcd377dd75b2"),
        Title = "The Winds of the Wind",
        Description = "Try not to wind along..",
        ParentId = new Guid("25320c5e-f58a-4b1f-b63a-8ee07a840bdf")
    }
};

Iterating over book entities directly
We iterate over the Book objects available for insertion and call UpdateOneAsync(filter, update) on each book. The filter ensures that the update only happens for authors with matching identifier value. The AddToSet method enables us to append a single book at a time to the books collection of an author.

await bookEntities.ToAsyncEnumerable().ForEachAsync(async book => await collection.UpdateOneAsync(
    Builders<Author>.Filter.Eq(p => p.Id, book.ParentId),
    Builders<Author>.Update.AddToSet(z => z.Books, book)
    ));

Iterating over grouped-up book entities
We now use c, rather than book, to represent the book in the ForEachAsync iterations. Grouping books into anonymous objects by common ParentId could be useful to reduce overhead, by preventing repeated UpdateOneAsync calls for the same parent object. The AddToSetEach method enables us to append one or more books to the books collection of an author, so long as these are input as an IEnumerable<Book>.

var bookGroups = bookEntities.GroupBy(c => c.ParentId, c => c,
    (key, books) => new { key, books });
await bookGroups.ToAsyncEnumerable().ForEachAsync(async c => await collection.UpdateOneAsync(
    Builders<Author>.Filter.Eq(p => p.Id, c.key),
    Builders<Author>.Update.AddToSetEach(z => z.Books, c.books.ToList())
    ));

Attaching book entities to parent class objects, and iterating over these
This can be done without anonymous objects, by mapping directly to Author class objects. The resulting IEnumerable<Author> is iterated over to append books to book collections of the different authors.

var authors = bookEntities.GroupBy(c => c.ParentId, c => c,
    (key, books) => new Author() { Id = key, Books = books.ToList() });
await authors.ToAsyncEnumerable().ForEachAsync(async c => await collection.UpdateOneAsync(
    Builders<Author>.Filter.Eq(p => p.Id, c.Id),
    Builders<Author>.Update.AddToSetEach(z => z.Books, c.Books)
    ));

Simplification with C# LINQ
Note that we can simplify some of the statements above with C# LINQ, specifically regarding the filter used in the UpdateOneAsync(filter, update) method. Example below.

var authors = bookEntities.GroupBy(c => c.ParentId, c => c,
    (key, books) => new Author() { Id = key, Books = books.ToList() });
await authors.ToAsyncEnumerable().ForEachAsync(async c => await collection.UpdateOneAsync(
    p => p.Id == c.Id,
    Builders<Author>.Update.AddToSetEach(z => z.Books, c.Books)
    ));

Performing multiple updates on the same filtered context
Multiple updates are possible by chaining them into Builders<Author>.Update.Combine(update01, update02, etc..). The first update is the same as before, with the second update incrementing author versioning to 1.01, thereby differentiating them from authors who had no new books at this time. In order to perform more complex queries we could expand on the Author objects created in the GroupBy call, to carry additional data. This is utilized in the third update, where author life expectancy changes depending on the author's last name. Note the if-else statement, as (Do you have bananas?) ? if yes : if no

var authors = bookEntities.GroupBy(c => c.ParentId, c => c,
    (key, books) => new DbAuthor()
    {
        Id = key,
        Books = books.ToList(),
        LastName = collection.Find(c => c.Id == key).FirstOrDefault().LastName,
        LifeExpectancy = collection.Find(c => c.Id == key).FirstOrDefault().LifeExpectancy
    });

await authors.ToAsyncEnumerable().ForEachAsync(async c => await collection.UpdateOneAsync(
    p => p.Id == c.Id,
    Builders<DbAuthor>.Update.Combine(
        Builders<Author>.Update.AddToSetEach(z => z.Books, c.Books),
        Builders<Author>.Update.Inc(z => z.Version, 0.01),
        Builders<Author>.Update.Set(z => z.LifeExpectancy,
            (c.LastName == "King") ? "Will live forever" : c.LifeExpectancy)
        )));

Using UpdateManyAsync
Since the fate of any given author is known before running the ForEachAsync iterations, we can query specifically for that and only alter those matches (instead of having to call Set on the ones we dont want to change). This set operation can then be excluded from the above iteration, if desired, and used separately using UpdateManyAsync. We use the same authors variable as declared above. Due to larger differences between not using and using C# LINQ I here show both ways of doing it (pick one). We use multiple filters at once ie. as Builders<Author>.Filter.And(filter01, filter02, etc).

var authorIds = authors.Select(c => c.Id).ToList();

// Using builders:
await collection.UpdateManyAsync(
    Builders<Author>.Filter.And(
        Builders<Author>.Filter.In(c => c.Id, authorIds),
        Builders<Author>.Filter.Eq(p => p.LastName, "King")
        ),
    Builders<Author>.Update.Set(p => p.LifeExpectancy, "Will live forever"));

// Using C# LINQ:    
await collection.UpdateManyAsync<Author>(c => authorIds.Contains(c.Id) && c.LastName == "King",
    Builders<Author>.Update.Set(p => p.LifeExpectancy, "Will live forever"));

Other usecases for UpdateManyAsync
Here I show some other perhaps relevant ways to use UpdateManyAsync with our data. To keep things compact I have decided to exclusively use C# LINQ in this section.

// Shouldn't all authors with lastname "King" live eternally?
await collection.UpdateManyAsync<Author>(c => c.LastName == "King",
    Builders<Author>.Update.Set(p => p.LifeExpectancy, "Will live forever"));

// Given a known author id, how do I update the related author?
var authorId = new Guid("412c3012-d891-4f5e-9613-ff7aa63e6bb3");
await collection.UpdateManyAsync<Author>(c => c.Id == authorId), 
    Builders<Author>.Update.Set(p => p.LifeExpectancy, "Will not live forever"));

// Given a known book id, how do I update any related authors?
var bookId = new Guid("c7ba6add-09c4-45f8-8dd0-eaca221e5d93");
await collection.UpdateManyAsync<Author>(c => c.Books.Any(p => p.Id == bookId),
    Builders<Author>.Update.Set(p => p.LifeExpectancy, "Death by doorsteps in due time"));

Nested documents: Updating individual child book objects
To update on sub document fields directly, we use the Mongo DB Positional Operator. In C# it is declared by typing [-1], which is the equivalent of $ in the Mongo DB Shell. As of writing this, it appears that the driver only supports using [-1] with the IList type. There were some initial struggle with using this operator, due to the error Cannot apply indexing with [] to an expression of type ICollection<Book>. The same error arose when trying to use an IEnumerable<Book> instead. Hence, make sure to use IList<Book> for now.

// Given a known book id, how do I update only that book? (non-async, also works with UpdateOne)
var bookId = new Guid("447eb762-95e9-4c31-95e1-b20053fbe215");
collection.FindOneAndUpdate(
    Builders<Author>.Filter.ElemMatch(c => c.Books, p => p.Id == bookId),
    Builders<Author>.Update.Set(z => z.Books[-1].Title, "amazing new title")
    );

// Given a known book id, how do I update only that book? (async)
var bookId = new Guid("447eb762-95e9-4c31-95e1-b20053fbe215");
await collection.UpdateOneAsync(
    Builders<Author>.Filter.ElemMatch(c => c.Books, p => p.Id == bookId),
    Builders<Author>.Update.Set(z => z.Books[-1].Title, "here we go again")
    );

// Given several known book ids, how do we update each of the books?
var bookIds = new List<Guid>()
{
    new Guid("c7ba6add-09c4-45f8-8dd0-eaca221e5d93"),
    new Guid("bc4c35c3-3857-4250-9449-155fcf5109ec"),
    new Guid("447eb762-95e9-4c31-95e1-b20053fbe215")
};       
await bookIds.ToAsyncEnumerable().ForEachAsync(async c => await collection.UpdateOneAsync(
    p => p.Books.Any(z => z.Id == c),
    Builders<Author>.Update.Set(z => z.Books[-1].Title, "new title yup yup")
    ));

It may be tempting to call UpdateManyAsync one time instead of calling UpdateOneAsync three times, as above. Implementation is shown below. While this works (the call does not error out), it does not update all three books. Since it accesses each chosen author one at a time, it can only perform updates to two of the three books (since we deliberately listed books with Guid ids that had equal Guid ParentIds). While, above, all three updates can be completed because we iterate over the same author one extra time.

await collection.UpdateManyAsync(
    c => c.Books.Any(p => bookIds.Contains(p.Id)),
    Builders<Author>.Update.Set(z => z.Books[-1].Title, "new title once per author")
    );

Concluding remarks
How did this aid you? Do you have any further inquiries with your needs? I started looking at these async methods half a week ago, concerning my personal project, so I hope what I have presented is of relevance to you.

You might also be interested in bulk writes, to bundle up changes that you want to make to the database and having Mongo Db optimize the calls to the database, for performance optimization. In that regard, I would recommend this extensive example on bulk operations for performance. This example might also be relevant.

Fhyarnir
  • 453
  • 1
  • 4
  • 12
  • 17
    The question is how to use UpdateOneAsync, the answer should be as short as possible not a blog post – Sam Mar 15 '20 at 17:50
  • 1
    Thanks for the feedback and down vote! Given that the question (in my opinion) was too vague, I chose to broaden the scope (and be somewhat relevant) rather than being too specific (and not relevant). – Fhyarnir Mar 17 '20 at 16:42
  • 2
    While this is a really DETAILED explanation - it goes far beyond the question that was asked and makes it very difficult to find the answer to the actual question. – Anthony Nichols Apr 30 '21 at 20:55
  • This was waaay off topic and answers a completely different question – Rhys Bevilaqua Jun 03 '22 at 07:25
5

Have you tried doing it the way it's shown in the docs? I think for you it might be something like:

[Fact]
public async Task Repro()
{
    var db = new MongoDbContext("mongodb://localhost:27017", "myDb");
    var document = new DummyDocument { Test = "abc", Id = Guid.Parse("69695d2c-90e7-4a4c-b478-6c8fb2a1dc5c") };
    await db.GetCollection<DummyDocument>().InsertOneAsync(document);
    var filter = Builders<DummyDocument>.Filter.Eq("Id", document.Id);
    var update = Builders<DummyDocument>.Update.Set("Test", "bla");
    await db.GetCollection<DummyDocument>().UpdateOneAsync(filter, update);
}

UPDATE:

Based on OP comment, to send an entire object instead of updating specific properties, try ReplaceOneAsync:

⋮
var filter = Builders<DummyDocument>.Filter.Eq("Id", document.Id);
await db.GetCollection<DummyDocument>().ReplaceOneAsync(filter, new DummyDocument { Test = "bla" }, new UpdateOptions { IsUpsert = true });
Ben Nieting
  • 1,360
  • 2
  • 14
  • 23
crgolden
  • 4,332
  • 1
  • 22
  • 40
  • Yes, I tried it and it works. But its not what I want to to. I want to pass an object. I could write reflection to make it work but the I guess, I just use "ObjectUpdateDefinition" the wrong way. – msallin Oct 02 '19 at 05:21
  • @msallin I made an update - maybe that is what you're looking for. – crgolden Oct 02 '19 at 12:40
  • Thanks for the update. I don't want to replace the whole document but just a bunch of fields. I now ended up using reflection, to do what I assumed that the ObjectUpdateDefinition will do. Still, I'm wondering how to use ObjectUpdateDefinition. – msallin Oct 02 '19 at 13:33