Search code examples
c#azureazure-cosmosdb

How to obtain ETag for individual document when using FeedResponse from Microsoft.Azure.Cosmos


I'm looking at samples from https://github.com/Azure/azure-cosmos-dotnet-v3/blob/dc3468bd5ce828e504ddef92ef792c35370de055/Microsoft.Azure.Cosmos.Samples/Usage/ItemManagement/Program.cs#L590

Here is the example of using ETag when using container.ReadItemAsync:

            ItemResponse<SalesOrder> itemResponse = await container.ReadItemAsync<SalesOrder>(
                partitionKey: new PartitionKey("Account1"),
                id: "SalesOrder1");

            Console.WriteLine("ETag of read item - {0}", itemResponse.ETag);

            SalesOrder item = itemResponse;
            //Update the total due
            itemResponse.Resource.TotalDue = 1000000;

            //persist the change back to the server
            ItemResponse<SalesOrder> updatedDoc = await container.ReplaceItemAsync<SalesOrder>(
                partitionKey: new PartitionKey(item.AccountNumber),
                id: item.Id,
                item: item);

            Console.WriteLine("ETag of item now that is has been updated - {0}", updatedDoc.ETag);

            //now, using the originally retrieved item do another update 
            //but set the AccessCondition class with the ETag of the originally read item and also set the AccessConditionType
            //this tells the service to only do this operation if ETag on the request matches the current ETag on the item
            //in our case it won't, because we updated the item and therefore gave it a new ETag
            try
            {
                itemResponse.Resource.TotalDue = 9999999;
                updatedDoc = await container.ReplaceItemAsync<SalesOrder>(itemResponse, item.Id, new PartitionKey(item.AccountNumber), new ItemRequestOptions { IfMatchEtag = itemResponse.ETag });
            }
            catch (CosmosException cre)
            {
                //   now notice the failure when attempting the update 
                //   this is because the ETag on the server no longer matches the ETag of doc (b/c it was changed in step 2)
                if (cre.StatusCode == HttpStatusCode.PreconditionFailed)
                {
                    Console.WriteLine("As expected, we have a pre-condition failure exception\n");
                }
            }

In my scenario I have a query and I need to obtain ETag for later use in Update. In this sample we get FeedResponse<SalesOrder> which has ETag, but this is the tag associated with last transaction, not document. So my question is how to get ETag of an individual document?

        private static async Task QueryItems()
        {
            //******************************************************************************************************************
            // 1.4 - Query for items by a property other than Id
            //
            // NOTE: Operations like AsEnumerable(), ToList(), ToArray() will make as many trips to the database
            //       as required to fetch the entire result-set. Even if you set MaxItemCount to a smaller number. 
            //       MaxItemCount just controls how many results to fetch each trip. 
            //******************************************************************************************************************
            Console.WriteLine("\n1.4 - Querying for a item using its AccountNumber property");

            QueryDefinition query = new QueryDefinition(
                "select * from sales s where s.AccountNumber = @AccountInput ")
                .WithParameter("@AccountInput", "Account1");

            FeedIterator<SalesOrder> resultSet = container.GetItemQueryIterator<SalesOrder>(
                query,
                requestOptions: new QueryRequestOptions()
                {
                    PartitionKey = new PartitionKey("Account1"),
                    MaxItemCount = 1
                });

            List<SalesOrder> allSalesForAccount1 = new List<SalesOrder>();
            while (resultSet.HasMoreResults)
            {
                FeedResponse<SalesOrder> response = await resultSet.ReadNextAsync();
                SalesOrder sale = response.First();
                Console.WriteLine($"\n1.4.1 Account Number: {sale.AccountNumber}; Id: {sale.Id};");
                if(response.Diagnostics != null)
                {
                    Console.WriteLine($" Diagnostics {response.Diagnostics.ToString()}");
                }

                allSalesForAccount1.Add(sale);
            }

Solution

  • I have found a solution for now. Not perfect as I call JObject.ToObject<T> twice for:

    • SalesOrder - business object
    • ETagCosmosDocument - object with ETag
    public class ETagCosmosDocument
    {
        public string _etag {get; set;}
    }
    
    public class CosmosDocument<T>
    {
        public T Document {get; private set;}
        public string ETag {get; private set;}
    
        public CosmosDocument(T document, string etag)
        {
            Document = document;
            ETag = etag;
        }
    
        public static implicit operator CosmosDocument<T> (JObject o)
        {
            T document = o.ToObject<T>();
            ETagCosmosDocument etagDoc = o.ToObject<ETagCosmosDocument>();
    
            return new CosmosDocument<T>(document, etagDoc._etag);
         }
    }
    

    then using FeedIterator<dynamic> instead of FeedIterator<SalesOrder>

    QueryDefinition query = new QueryDefinition(...)
    
    FeedIterator<dynamic> resultSet = container.GetItemQueryIterator<dynamic>(
                    query,
                    requestOptions: new QueryRequestOptions()
                    {
                        PartitionKey = new PartitionKey("Account1"),
                        MaxItemCount = 1
                    });
    
    while (resultSet.HasMoreResults)
    {   
        FeedResponse<dynamic> response = await resultSet.ReadNextAsync();
    
        JObject r = response.First();
        CosmosDocument<SalesOrder> cd = r;
        ...
    }