29

EDIT: Not looking for the javascript way of doing this. I am looking for the MongoDB C# 2.0 driver way of doing this (I know it might not be possible; but I hope somebody knows a solution).

I am trying to update the value of an item embedded in an array on the primary document in my mongodb.

I am looking for a strongly typed way to do this. I am using the Mongodb c# 2.0 driver

I can do it by popping the element, updating the value, then reinserting. This just doesn't feel right; since I am overwriting what might have been written in the meantime.

Here is what I have tried so far but with no luck:

private readonly IMongoCollection<TempAgenda> _collection;

void Main()
{
    var collectionName = "Agenda";
    var client = new MongoClient("mongodb://localhost:27017");
    var db = client.GetDatabase("Test");
    _collection = db.GetCollection<TempAgenda>(collectionName);
    UpdateItemTitle(1, 1, "hello");
}

public void UpdateItemTitle(string agendaId, string itemId, string title){
    var filter = Builders<TempAgenda>.Filter.Eq(x => x.AgendaId, agendaId);
    var update = Builders<TempAgenda>.Update.Set(x => x.Items.Single(p => p.Id.Equals(itemId)).Title, title);
    var result = _collection.UpdateOneAsync(filter, update).Result;
}
7
  • possible duplicate of MongoDB - Update an object in nested Array Commented Jul 16, 2015 at 12:08
  • 3
    @BlakesSeven that is not a c# question? Commented Jul 16, 2015 at 12:08
  • It's still the same principle. Match the array element in the query portion and use the positional $ operator in the update portion. I didn't just randomly pull that out of a bag. Commented Jul 16, 2015 at 12:09
  • 7
    @BlakesSeven I can read the mongo documentation and do it with pure javascript, but, as I stated in my question, I am looking for the strongly typed way to do this and as far as I know embedding javascript strings in my C# code is not strongly typed. Commented Jul 16, 2015 at 12:11
  • There still is a positional operator for C#. It's not "javascript strings" it's "dot notation" and valid across all languages. Commented Jul 16, 2015 at 12:13

5 Answers 5

71

Took me a while to figure this out as it doesn't appear to be mentioned in any of the official documentation (or anywhere else). I did however find this on their issue tracker, which explains how to use the positional operator $ with the C# 2.0 driver.

This should do what you want:

public void UpdateItemTitle(string agendaId, string itemId, string title){
    var filter = Builders<TempAgenda>.Filter.Where(x => x.AgendaId == agendaId && x.Items.Any(i => i.Id == itemId));
    var update = Builders<TempAgenda>.Update.Set(x => x.Items[-1].Title, title);
    var result = _collection.UpdateOneAsync(filter, update).Result;
}

Notice that your Item.Single() clause has been changed to Item.Any() and moved to the filter definition.

[-1] or .ElementAt(-1) is apparently treated specially (actually everything < 0) and will be replaced with the positional operator $.

The above will be translated to this query:

db.Agenda.update({ AgendaId: 1, Items.Id: 1 }, { $set: { Items.$.Title: "hello" } })
Sign up to request clarification or add additional context in comments.

8 Comments

Sorry for being so slow at accepting your answer. Works very well. Unfortunately they don't support more than one operational selectors: jira.mongodb.org/browse/SERVER-831 Seems weird to me, since this can be used to make a better isolation of the update needed to be done.
Thanks Søren. I must say I am not a fan of the syntax they have chosen though because it means you cannot use IEnumerable<T> or ICollection<T> and forces you to use the more cumbersome IList<T> or a simple array
@SørenKruse you are indeed correct while that syntax I just verified that works like a charm. it is a little cumbersome compared to array indexer but at least I don't have to make all my domain classes use IList<T> which is what I was after.
I tried this solution and the code actually compiles without any error but what happens in runtime when I query the document, the [-1] in that array starts a reverse indexed search instead of using a positional operator. So -1 results the last element of array, -2 gives second last. This isn't exactly what we want, indexing should be dynamic through positional operator.
use x.Items.FirstMatchingElement().Title instead of [-1]
|
4

Here's the combined working solution of the above answers with .NET 7 and MongoDB.Driver 2.20.0

Use FirstMatchingElement() or AllElements() instead of [-1]

  • AgnendaId is the parent's
  • Items is the NestedArray

Update the FirstMatchingElement:

public void UpdateItemTitle(string agendaId, string itemId, string title)
{
    var filter = Builders<TempAgenda>.Filter.Where(x => x.AgendaId == agendaId && x.Items.Any(i => i.Id == itemId));
    var update = Builders<TempAgenda>.Update.Set(x => x.Items.FirstMatchingElement().Title, title);

    _collection.UpdateOneAsync(filter, update);
}

Update AllElements:

public void UpdateItemTitle(string agendaId, string itemId, string title)
{
    var filter = Builders<TempAgenda>.Filter.Where(x => x.AgendaId == agendaId && x.Items.Any(i => i.Id == itemId));
    var update = Builders<TempAgenda>.Update.Set(x => x.Items.AllElements().Title, title);

    _collection.UpdateOneAsync(filter, update);
}

Comments

2

Thanks, this was helpful. I have an addition though, I've used the above for arrays, pushing to a nested array and pulling from one. The issue I have found is that if I had an int array (So not an object, just a simple int array) that the PullFilter didn't actually work - "Unable to determine the serialization information" which is strange as it's only an array of ints. What I ended up doing was making it an array of objects with only one int parameter, and it all started to work. Possibly a bug, or perhaps my lack of understanding. Anyway, as I've struggled to find information about pulling and pushing to nested object arrays with the C# 2.0 driver, I thought I should post my findings here, as they use the above syntax.

var filter = Builders<MessageDto>.Filter.Where(x => x._id == entity.ParentID && x.NestedArray.Any(i => i._id == entity._id));
var update = Builders<MessageDto>.Update.PullFilter(x => x.NestedArray.ElementAt(-1).User, Builders<User>.Filter.Eq(f => f.UserID, userID));
Collection<MessageDto>(currentUser).UpdateOneAsync(filter, update);

And also:

var filter = Builders<MessageDto>.Filter.Where(x => x._id == entity.ParentID && x.NestedArray.Any(i => i._id == entity._id));
var update = Builders<MessageDto>.Update.Push(x => x.NestedArray.ElementAt(-1).Users, new User { UserID = userID });
Collection<MessageDto>(currentUser).UpdateOneAsync(filter, update);

1 Comment

you are talking about an array of int, but the example is still with an array storing complex structures. Show an example of removing ONE element from an array of int.
2

In newer drivers ElementAt(-1) might no longer be supported. I have had code with (-1) that stopped working when going to .NET6 and MongoDB Driver 2.19.0

They have introduced ExtensionMethods instead:

x.A.FirstMatchingElement() => "A.$"
x.A.AllElements() => "A.$[]"
x.A.AllMatchingElements("identifier") => "A.$[identifier]"

1 Comment

For those interested, this was documented in jira.mongodb.org/browse/CSHARP-4079
-1

The correct way to update a Document or sub array is as follows:

var filter = Builders<Declaracion>.Filter.Where(x => x.Id == di && x.RemuneracionMensualActual.RemuneracionActIndustrial.Any(s => s.Id == oid));

        var update = Builders<Declaracion>.Update.Set(x => x.RemuneracionMensualActual.RemuneracionActIndustrial.ElementAt(-1).Ingreso, datos.ActividadIndustrial.Ingreso)
            .Set(x => x.RemuneracionMensualActual.RemuneracionActIndustrial.ElementAt(-1).RazonSocial, datos.ActividadIndustrial.RazonSocial)
            .Set(x => x.RemuneracionMensualActual.RemuneracionActIndustrial.ElementAt(-1).TipoNegocio, datos.ActividadIndustrial.TipoNegocio);

2 Comments

I think you got downvoted because somebody got upset by translation, "The correct way to..." reads as "this it the one true way" which somebody got offended with, and didn't even have the courtesy to leave a comment. I will say that your answer does duplicate 2 of the answers given above, where they use [-1] or .ElementAt(-1).
Please do not spam an answer. We can all clearly see it has already been answered.

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.