Sometimes you have a document in MongoDB with a property that is an array of nested objects. You’d like to update one of those objects. What’s the best way to go about that in C#?
Let’s say you have a simple document representing a class with students:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
I’d like to update Bob’s grade to a 93 since he passed his last exam.
One simple way to do it is to load the whole Class document into memory, update the grade in C#, and then put the document back into the database.
That would look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
There is a problem with this approach though.
For one thing, its pretty inefficient, especially for a large document. It’s probably not much of a problem for this small sample. but imagine this was a seminar class with 200 students. And that the document also included individual paper grades for each student. This is a lot of data to bring across the wire, instantiate C# objects for, send back across the wire, then eventually garbage collect the whole document in .NET land. It can add up pretty quick, especially in a high volume environment.
A more insidious problem is the race condition involved in updating the whole document. There’s no such thing as a lock in MongoDB, so its possible that two threads could pull down the whole document, make changes and race to get their version saved last.
For example, imagine initially Alice’s score is 95 and Bob’s is 85. Two threads pull down that document.
Thread 1 updates Alice’s score to 92, and Thread 2 update’s Bob’s score to 89. Then they both write their version of the document back to the database. Will Alice have her 92? Will Bob have his 89?
Who knows! Since each thread is replacing the entire document, who ever saves last wins!
Undefined behavior is not something you want in your database layer.
Luckily, MongoDB offers a findAndUpdate operation that can atomically change a subset of the fields in a document. Since the entire document is the unit of atomicity in MongoDB, the two threads can each issue a findAndUpdate at the same time and it will work out OK, provided they are each editing separate students.
If both threads are attempting to modify the same student, than just like before, all bets are off.
Anyway, the magic incantation for
findAndModify looks like this:
1 2 3 4
Couple things going on here.
query description, we’re using an
$elemMatch projection which tells
MongoDB to find a document with an array called
Students with an element with
update description includes that curious string,
That dollar sign is called the Positional Operator.
It refers to the first matching element in the array.
We also use
NumberInt(89) so that Mongo writes an integer instead of a
floating point number, which is the default.
OK, this is all great, but how do we coerce the C# LINQ provider into executing
this query? C# doesn’t have a positional operator! And whatabout the
projection? There’s no LINQ method called
ElementMatch or anything…
It turns out the Mongo Driver LINQ provider translates the LINQ
$elemMatch. So the expression
cls => cls.Students.Any(s => s.Name == "Bob")
will give us that query.
That makes a bit of sense: it lines up with how you might query an in memory list of Classes using LINQ to Objects.
But what about the Positional Operator? Unfortunately, since there’s no C#
language support for a custom operator like this, the authors of the C# driver
have special-cased an index of
-1 to mean “use the positional operator”.
So the whole thing winds up looking like this:
1 2 3 4 5 6 7
I think the hard-coded
-1 index1 is confusing. You have to know that the
LINQ provider handles that in a different way than it would work in LINQ to
objects. You could use a constant, like
PositionalOperator to bring clarity,
but I’d rather see an extension method provided by Mongo called something like
FirstMatching(). Then it might look like:
1 2 3 4
Anyway, good luck.
Note that if you’ve modeled the
IEnumerable<Student>you won’t be able to apply the indexer (
) in your LINQ expression. In that case you can use
c.Students.ElementAt(-1)for the same effect.↩