28

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;
}
Kristian Barrett
  • 3,574
  • 2
  • 26
  • 40
  • possible duplicate of [MongoDB - Update an object in nested Array](http://stackoverflow.com/questions/10522347/mongodb-update-an-object-in-nested-array) – Blakes Seven Jul 16 '15 at 12:08
  • 3
    @BlakesSeven that is not a c# question? – Kristian Barrett Jul 16 '15 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. – Blakes Seven Jul 16 '15 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. – Kristian Barrett Jul 16 '15 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. – Blakes Seven Jul 16 '15 at 12:13
  • @BlakesSeven alright that sounds promising. Can you point me in the right direction; since I am failing to find this on my own. Hence the question above. – Kristian Barrett Jul 16 '15 at 12:16
  • And heres the Documentation: [Update an Embedded Field](https://docs.mongodb.org/getting-started/csharp/update/#update-an-embedded-field) – Blakes Seven Jul 16 '15 at 12:16

5 Answers5

70

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" } })
Søren Kruse
  • 1,160
  • 10
  • 12
  • Sorry for being so slow at accepting your answer. Works very well. Unfortunately they don't support more than one operational selectors: https://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. – Kristian Barrett Sep 21 '16 at 14:29
  • 1
    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 or ICollection and forces you to use the more cumbersome IList or a simple array – Buvy Nov 01 '16 at 23:31
  • @Buvy You should be able to use the LINQ extension method `ElementAt(-1)` as well. – Søren Kruse Nov 02 '16 at 10:02
  • 1
    @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 which is what I was after. – Buvy Nov 02 '16 at 18:14
  • 2
    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. – Suleman Aug 27 '19 at 23:50
  • @Suleman have you found any solution for every index matching update? – Hammad Sajid Apr 11 '20 at 09:03
  • It's quite possible that doing `.Result` here will cause locking issues – Liam Feb 25 '21 at 16:38
  • use `x.Items.FirstMatchingElement().Title` instead of `[-1]` – Reza Taba Jul 19 '23 at 16:08
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);
davoc bradley
  • 220
  • 2
  • 16
  • 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. – EgoPingvina Dec 12 '19 at 08:40
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]"
HakonIngvaldsen
  • 354
  • 2
  • 12
1

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);
}
Reza Taba
  • 1,151
  • 11
  • 15
-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);
Patrick
  • 1,717
  • 7
  • 21
  • 28
  • 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)`. – Colin Jul 14 '21 at 17:54
  • Please do not spam an answer. We can all **clearly** see it has already been answered. – Ergis Feb 21 '23 at 10:06