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 ParentId
s). 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.