13

I'm trying to patch a child object of a Cosmos Db document using the new Partial Document Update feature. However, I can't find a way to do it.

If the document looks like below, how would you update child (in the Items array) with ID 2 ProductCode property to "A-111" using the new Patch method?


{
    "id": "SalesOrder2",
    "ponumber": "PO15428132599",
    "OrderDate": "2005-07-01T00:00:00",
    "DueDate": "2005-07-13T00:00:00",
    "ShippedDate": "2005-07-08T00:00:00",
    "AccountNumber": "Account2",
    "SubTotal": 6107.082,
    "TaxAmt": 586.1203,
    "Freight": 183.1626,
    "TotalDue": 4893.3929,
    "DiscountAmt": 1982.872,
    "Items": [
        {
            "Id": 1,
            "OrderQty": 3,
            "ProductCode": "A-123",
            "ProductName": "Product 1",
            "CurrencySymbol": "$",
            "CurrencyCode": "USD",
            "UnitPrice": 17.1,
            "LineTotal": 5.7
        },
        {
            "Id": 2,
            "OrderQty": 2,
            "ProductCode": "A-456",
            "ProductName": "Product 2",
            "CurrencySymbol": "$",
            "CurrencyCode": "USD",
            "UnitPrice": 10,
            "LineTotal": 20
        }
    ],
    "_rid": "BsMkAMc43s4CAAAAAAAAAA==",
    "_self": "dbs/BsMkAA==/colls/BsMkAMc43s4=/docs/BsMkAMc43s4CAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-e136-0dbec04601d7\"",
    "_attachments": "attachments/",
    "_ts": 1637760030
}

Pietv
  • 215
  • 2
  • 8

2 Answers2

11

You need to use the Replace Operation by passing the specific index of the object that you want to update,

in this case, it would be something like,

 ItemResponse<SalesOrder> response = await container.PatchItemAsync<SalesOrder>(
 id: "SalesOrder2",
 partitionKey: new PartitionKey("/SalesOrder2"),
 patchOperations: new[] { PatchOperation.Replace("/Items/1/ProductCode","A-111") });
Sajeetharan
  • 216,225
  • 63
  • 350
  • 396
  • 5
    Is there a way to do this without specifying the index position (in this case, 1) of the element? For example, the OP said, "with ID 2". Could this be done using the ID instead pf the array position? – DeadZone May 09 '22 at 20:15
  • Sajeetharan gets us most of the way. One important note is that you need to know the index of the child item array you want to update. So the only way I could think of doing that is querying the document for the child array you want to update, use C# to find the matching index and then use it in his patch code example. – The Muffin Man Jun 12 '22 at 04:48
  • @TheMuffinMan - I'm new in noSQL, so want to ask - is there any other noSQL database service that I can use instead of CosmosDB to avoid mentioned issue with array index? I'm interested in both cases: on-cloud and on-premise solutions. Thank you. – Bohdan Aug 05 '22 at 20:01
  • I don't think so. You either need to know the index up front, query the entire array to get the index or model your data differently for your use case. Remember that denormalization is normal in noSQL. You have to make decisions based on your use case such as heavy read or writes. Write additional logic to keep your data sets in sync – The Muffin Man Aug 06 '22 at 21:13
  • @DeadZone maybe a filter predicate? https://learn.microsoft.com/en-au/azure/cosmos-db/partial-document-update-getting-started?tabs=dotnet – sharpc Oct 04 '22 at 13:10
  • @sharpc see [this SO question](https://stackoverflow.com/questions/72186654/azure-cosmosdb-partial-document-update-patch-on-complex-structures) for my attempts to use the filter predicate to resolve this without an index. (tldr: the filter predicate seems to be an all or nothing. If the filter condition passes, then the operation continues. If not, then it cancels.) – DeadZone Jan 03 '23 at 14:10
  • Please upvote https://feedback.azure.com/d365community/idea/29a17839-f8fc-ed11-a81c-000d3a0d3715 if you would like the ability to perform partial document updates on array elements without specifying indexes explicitly. – BenjiFB May 29 '23 at 13:02
1

Unfortunately, there isn't a way to use some sql like syntax to update a specific child array item using a condition. You have to know the index to it. That means you need to pull down your document (you can select only the fields you need to do this work).

This is verbose, but it's really not doing a whole lot once you read through it.

Document:

{
"id": "15bab994-6ea2-436c-badd-a31f44d2e85d",
"conversationId": "5663d3ff-1347-4413-a584-9c634425c7ab",
"subject": "Office credit card",
"dateSent": "2022-06-08",
"dateCreated": "2022-06-06",
"isRetracted": false,
"parentId": "",
"isDeletedForSender": true,
"sender": {
    "emailAddress": "nick@gmail.com",
    "isUnread": true
},
"recipients": [
    {
        "id": 2,
        "emailAddress": "john@gmail.com",
        "type": 2,
        "isUnread": true,
        "labelIds": [
            2
        ]
    },
    {
        "id": 1,
        "emailAddress": "nick@gmail.com",
        "type": 1,
        "isUnread": true,
        "labelIds": [
            1
        ]
    }
],
"attachments": [
    {
        "name": "card.pdf",
        "sizeInBytes": 129323
    }
],
"_rid": "zl9jANa86aABAAAAAAAAAA==",
"_self": "dbs/zl9jAA==/colls/zl9jANa86aA=/docs/zl9jANa86aABAAAAAAAAAA==/",
"_etag": "\"0d0093b8-0000-0100-0000-62a57c470000\"",
"_attachments": "attachments/",
"recipients[0].isUnread": true,
"_ts": 1655012423
}

Conditional patch code to update isUnread for sender or recipients that match a specific email.

var container = _cosmosClient.GetContainer("my-database", "Message");

    // Get all of the messages that match the list of conversations to update
    var queryResultSetIterator = container.GetItemLinqQueryable<Message2>()
        .Select(x => new PatchMessageContainer
        {
            Id = x.Id,
            ConversationId = x.ConversationId,
            Recipients = x.Recipients,
            Sender = x.Sender
        })
        .Where(x => payload.ConversationIds.Contains(x.ConversationId)).ToFeedIterator();
    var conversations = new List<PatchMessageContainer>();
    while (queryResultSetIterator.HasMoreResults)
        conversations.AddRange(await queryResultSetIterator.ReadNextAsync().ConfigureAwait(false));

    // Set isUnread for the authenticated user in the recipients list
    var updateTasks = new List<Task>();
    foreach (var conversation in conversations)
    {
        // The following Patch operations are smart enough to skip if the target value is already set to the desired value
        var patchOpts = new List<PatchOperation>();
        // Update sender isUnread if sender matches auth email
        if (conversation.Sender.EmailAddress == userClaims.Email && conversation.Sender.IsUnread != payload.SetIsUnread)
            patchOpts.Add(PatchOperation.Set($"/sender/isUnread", payload.SetIsUnread));

        // Update recipient isUnread where recipient matches auth email
        var idx = conversation.Recipients.FindIndex(x => x.EmailAddress == userClaims.Email);
        if (idx > -1 && conversation.Recipients[idx].IsUnread != payload.SetIsUnread)
            patchOpts.Add(PatchOperation.Set($"/recipients/{idx}/isUnread", payload.SetIsUnread));

        // Send the actual patch request
        var task = container.PatchItemAsync<Message2>(conversation.Id.ToString(),
               new PartitionKey(conversation.ConversationId.ToString()), patchOpts);
        updateTasks.Add(task);
    }

    await Task.WhenAll(updateTasks);

conversations variable here is really just the Message document in the first code block, but I've only selected a subset of fields needed to accomplish the update.

Hope this helps.

The Muffin Man
  • 19,585
  • 30
  • 119
  • 191
  • 1
    I sure how Azure updates this soon, not being able to specify a more precise filter query to child arrays to specify what to patch. If I load an object with an array of 10 items and issue a PatchItemAsync() for the item at index 2, someone else could have inserted a new array item at the start and my patch would update the wrong item. No exception gets raised... just a hard to track down runtime bug. – Benjamin Brandt Mar 24 '23 at 17:14
  • 1
    @BenjaminBrandt I share the same frustrations but I think right approach here is verify the etag as means of checking "versioning" of the document you are patching. Cosmos SDK has built in support for that and you'd then handle those cases in your code either refusing update or implementing some sort of retry mechanism. – Street0 Apr 13 '23 at 17:10
  • To me, it's a bit of a bummer to need to write your own code to validate the etag for this case. MongoDB includes https://www.mongodb.com/docs/manual/reference/operator/update/positional/ to allow for this. See https://feedback.azure.com/d365community/idea/29a17839-f8fc-ed11-a81c-000d3a0d3715 if you want similar functionality added to the SQL api. – BenjiFB May 29 '23 at 13:05