7

How do I modify in Mongo (C# driver) a single element in a nested property (array) without retrieving the whole document?

public class Element
{
    public int Value {get; set;}

    public string Name {get; set;}
}

public class Document
{

     public Element [] Elements {get; set;}
}

In example I want to find the element with name "Car" and sets its value to 4 in a single query.

2 Answers 2

10

You need $ positional operator where you can specify document-level condition and array-level condition to find single nested item in an array of particular document. In C# $ sign is represented by -1 passed as an index of your model array. Try:

var col = mydb.GetCollection<Document>("collectionName");
var id = new ObjectId("5babaaf5509f6d342da5abaa");
var elementName = "Car";
var newValue = 2;

var filterBuilder = Builders<Document>.Filter;
var filter = filterBuilder.Eq(x => x.Id, id) &
    filterBuilder.ElemMatch(doc => doc.Elements, el => el.Name == elementName);

var updateBuilder = Builders<Document>.Update;
var update = updateBuilder.Set(doc => doc.Elements[-1].Value, newValue);

Col.UpdateOne(filter, update);

Update: -1 is not supported in MongoDB.Driver 2.19 in .NET 6, you'll get Expression not supported: x.Elements.get_Item(-1) because negative indexes are not valid. To use the positional operator $ use FirstMatchingElement instead of an index value of -1. Just include that using MongoDB.Driver.Linq statement and replace the line above.

using MongoDB.Driver.Linq;

var update = updateBuilder.Set(doc => doc.Elements.FirstMatchingElement().Value, newValue);
Sign up to request clarification or add additional context in comments.

5 Comments

I need to update all elements where matches the filter. It will only update one element?
@HammadSajid yes, this will update only one, for multiple you can try: stackoverflow.com/questions/60219952/…
That's very sweet of you, I am searching for hours' but not able to found it
"In C# $ sign is represented by -1 passed as an index of your model array" Learned this here, thank you!
So grateful for the update above, which solved my problem after hours of searching around the web. The official documentation really needs some love. FirstMatchingElement is exactly what I needed, and nice and clean too.
1

For those looking for a generic method, this works as of driver version 2.23.1. T is the parent doc and C is the sub/nested doc.

public async Task UpdateArrayItemAsync<C>(
    Expression<Func<T, bool>> filter, // eg. (parent => parent.Id == "1")
    Expression<Func<C, bool>> arrayFilter, // eg. (sub => sub.Id == "1a")
    string arrayField, // Name of the array field on the parent doc
    IDictionary<string, object> updates) // Fields and values to set on array object
{
    var updateFilter = Builders<T>.Filter.And(
        Builders<T>.Filter.Where(filter),
        Builders<T>.Filter.ElemMatch<C>(arrayField, arrayFilter));
    
    // '.$' refers to the first matched element in Builders<T>.Filter.ElemMatch<C>()
    var updateDefs = updates.Keys
        .Select(field => Builders<T>.Update.Set(
            $"{arrayField}.$.{field}", updates[field]));

    await _mongoCollection.UpdateOneAsync(updateFilter,
        Builders<T>.Update.Combine(updateDefs));
}

Comments

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.