5

I have the following code to filter some data in a Cosmos DB:

Container container = await service.GetContainer(containerName, partitionKeyA);
using (FeedIterator<T> resultSet = container.GetItemQueryIterator<T>(
    queryDefinition: GetComplexSQLQueryDefinition(),
    paginationInfo.Token,
    requestOptions: new QueryRequestOptions()
    {
      PartitionKey = new PartitionKey(partitionKey),
      MaxItemCount = 10
    }
    ))
{ 
  FeedResponse<T> response = await resultSet.ReadNextAsync();
  //get total count
  int totalCount = -1;
}

The query can yield many records, hence I need the pagination.

Unfortunately I need the total count of the items - and it can vary according to the filtering.

According to this answer, I have only 2 options:

  1. I create a second SQL query that uses an SQL select command to count the records - then query the database again to select the actual records:

    var query = new QueryDefinition("SELECT value count(1) FROM c WHERE c.tenantId = @type");
    query.WithParameter("@type", '5d484526d76e9653e6226aa2');
    var container = client.GetContainer("DatabaseName", "CollectionName");
    var iterator = container.GetItemQueryIterator<int>(query);
    var count = 0;
    while (iterator.HasMoreResults)
    {
        var currentResultSet = await iterator.ReadNextAsync();
        foreach (var res in currentResultSet)
        {
            count += res;
        }
    }
    Console.WriterLine($"The first count is: {count}");
    
  2. I translate my complex SQL query to LINQ and use its Count method:

    //should be formatted as code

    var count = container.GetItemLinqQueryable(true) .Count(item => item.tenantId.Equals('5d484526d76e9653e6226aa2'));

Both seems quite cumbersome for such a simple task so I wonder if there is any better or more effective approach.

What else could I try?

Nestor
  • 8,194
  • 7
  • 77
  • 156

1 Answers1

5

Here's roughly the code I've used to perform count operations with .NET SDK v3 based on an arbitrary QueryDefinition. There are a few aspects not shown like the full logic of deserialization with System.Text.Json, but hopefully the gist comes across. The idea is to provide a query with base:

SELECT VALUE COUNT(1) FROM c

To get a single document as result which contains the result count;

public async Task<int?> CountAsync(QueryDefinition query, string partitionKey)
{
    var options = new QueryRequestOptions() { PartitionKey = new(partitionKey), MaxItemCount = 1 };
    int? count = null;
    using var resultSet = cosmosService.DataContainer.GetItemQueryStreamIterator(query, requestOptions: options);
    while (resultSet.HasMoreResults)
    {
        using var response = await resultSet.ReadNextAsync();
        if (response.IsSuccessStatusCode)
        {
            // Deserialize response into CosmosResponse<int>
            var deserializeResult = await FromJsonStream<CosmosResponse<int>>(response.Content);
            if (!deserializeResult.HasSuccessValue(out CosmosResponse<int>? responseContent))
            {
                return null; // Or some failure
            }

            if (responseContent.Documents.Any())
            {
                count = responseContent.Documents[0];
                break;
            }
            else
            {
                return null;// Or some failure
            }
        }
        else // Unexpected status. Abort processing.
        {
            return null;// Or some failure
        }
    }

    return count;
}

Helper class to deserialize the response:

public class CosmosResponse<T>
{
    [JsonPropertyName("Documents")]
    public IReadOnlyList<T> Documents { get; set; } = Array.Empty<T>();

    [JsonPropertyName("_count")]
    public int Count { get; set; }
}
Noah Stahl
  • 6,905
  • 5
  • 25
  • 36
  • 1
    Thanks, I took the query and used it ***twice***: first I read the records and then I call the same query with `COUNT(1)` to get the total count. Seems like a waste of call but I hope the execution plan is cached so it remains quick - and I hope it won't cost _too much RUs_. – Nestor May 29 '21 at 09:41
  • I've spent the past day looking at approaches to implement this, and this still seems to be the best way (unfortunately). Thank you for your response, I made a small amendment however. Rather than building a ComsosResponse object, I created a separate method to do the count, similar to you. However, my method instead calls GetItemQueryIterator(countQuery)) on the container object, rather than your custom deserialization. Just wanted to throw it out there for anybody else looking at similar approaches but don't want to create a dedicated class to just return a count int – Josh Wright Aug 03 '22 at 12:15