4

I'm working through migrating an application from Cosmos C# SDK v2 to v3. I hit a roadblock with pagination and can't seem to figure it out.

I have a request that will search a particular container and paginate through requests. My code is setup to handle the page size (maxItemCount) and continuation token. Additionally, the first few requests work great. But when I get to like the fourth page, the continuation token I get in the response from Cosmos is invalid. It looks like this:

[{\"range\":{\"min\":\"05C1DFFFFFFFFC\",\"max\":\"FF\"}}]

And, when I pass that to the next request to grab the following page, I get the following error:

Response status code does not indicate success: BadRequest (400); Substatus: 0; ActivityId: ; Reason: (Response status code does not indicate success: BadRequest (400); Substatus: 0; ActivityId: ; Reason: (Response status code does not indicate success: BadRequest (400); Substatus: 0; ActivityId: ; Reason: (Response status code does not indicate success: BadRequest (400); Substatus: 0; ActivityId: ; Reason: (Response status code does not indicate success: BadRequest (400); Substatus: 0; ActivityId: ; Reason: (Response status code does not indicate success: BadRequest (400); Substatus: 0; ActivityId: ; Reason: (Response status code does not indicate success: BadRequest (400); Substatus: 0; ActivityId: ; Reason: (Response status code does not indicate success: BadRequest (400); Substatus: 0; ActivityId: ; Reason: (CompositeContinuationToken is missing field: 'token': {"range":{"min":"05C1DFFFFFFFFC","max":"FF"}}););););););););

Now, I have found that if I modify the continuation token before sending it, I can get the request to succeed. This requires me adding a property to the JSON string of, you guessed it, token.

Here is the code in question that processes this request:

            IQueryable<LeadEntity> query;
            var queryResults = new List<LeadEntity>();

            var requestOptions = new QueryRequestOptions
            {
                MaxItemCount = input.Page?.Size ?? Core.Shared.Constants.PageSize.Default
            };

            if (input.Page?.ContinuationToken == "" || input.Page?.ContinuationToken == null) 
            {
                query = Container.GetItemLinqQueryable<LeadEntity>(false, null, requestOptions);
            }
            else 
            {
                query = Container.GetItemLinqQueryable<LeadEntity>(false, input.Page.ContinuationToken, requestOptions);
            }

            if (input.Criteria?.CallKeys.IsActive ?? false)
            {
                var callKeys = (input.Criteria.CallKeys?.Value ?? Enumerable.Empty<Guid>())
                    .Select(a => a.ToString());

                query = query.Where(entity => callKeys.Contains(entity.CallKey));
            }

            if (input.Criteria?.AdvisorOids.IsActive ?? false)
            {
                var advisorOids = (input.Criteria.AdvisorOids?.Value ?? Enumerable.Empty<Guid>())
                    .Select(a => a.ToString());

                query = query.Where(entity => advisorOids.Contains(entity.AdvisorOid));
            }

            if (input.Criteria?.CreatedOn.IsActive ?? false)
            {
                var start = input.Criteria.CreatedOn.Value.Start;
                var duration = input.Criteria.CreatedOn.Value.Duration;
                var end = input.Criteria.CreatedOn.Value.End;

                if (start.HasValue && end.HasValue)
                {
                    query = query.Where(entity => start.Value <= entity.Created.On && entity.Created.On <= end.Value);
                }
                else if (start.HasValue)
                {
                    query = query.Where(entity => start.Value <= entity.Created.On && entity.Created.On <= start.Value.Add(+duration));
                }
                else if (end.HasValue)
                {
                    query = query.Where(entity => end.Value >= entity.Created.On && entity.Created.On >= end.Value.Add(-duration));
                }
                else
                {
                    query = query.Where(entity => false);
                }
            }

            if (input.Criteria?.HasPortalKey.IsActive ?? false)
            {
                query = query.Where(entity => input.Criteria.HasPortalKey.Value
                        && entity.PortalKey != null
                        && entity.PortalKey != default(Guid).ToString()
                );
            }

            query = query.Where(a => a.IsRemoved.IsDefined() ? !a.IsRemoved : true);

            var totalCount = await query.CountAsync(ct);

            query = input.Orderings?.Any() ?? false
                ? query.OrderBy(string.Join(",", input.Orderings))
                : query;

            query = input.Limit.HasValue
                ? query.Take(input.Limit.Value)
                : query;

            var feedIterator = query.ToFeedIterator();

            FeedResponse<LeadEntity> feedResults = await feedIterator.ReadNextAsync(ct);
            
            queryResults.AddRange(feedResults);

            Console.WriteLine($"[LeadSearchAsync] total operation cost: {feedResults.RequestCharge} RUs");

            var output = new PageOutput<LeadOutput>
            {
                Items = Mapper.Map<IEnumerable<LeadOutput>>(queryResults),
                ContinuationToken = feedResults.ContinuationToken,
                TotalCount = totalCount
            };

            return output;

I have been googling my butt off and have only found a few instances of this happening and it's normally on the v2 sdk, not the v3. Part of me wants to think I'm doing pagination wrong, but it appears to be working for the most part.

Any help resolving this would be greatly appreciated. I haven't found a whole lot of documentation on the v3 SDK regarding pagination and its clearly different than v2.

Nate
  • 345
  • 3
  • 12

2 Answers2

3

OK. After some more googling today, I discovered this Github issue.

Basically I had this block of code in my startup that set the default JSONSerializerSettings to ignore null values:

var JsonSerializerSettings = new JsonSerializerSettings 
{
    DateTimeZoneHandling = DateTimeZoneHandling.RoundtripKind,
    DateParseHandling = DateParseHandling.DateTimeOffset,             
    NullValueHandling = NullValueHandling.Ignore,             
    ReferenceLoopHandling = ReferenceLoopHandling.Ignore
};

JsonSerializerSettings.Converters.Add(new StringEnumConverter());
JsonConvert.DefaultSettings = () => JsonSerializerSettings;

Which caused my continuation token string to remove the field token because it was null. Apparently its perfectly normal to have a null token field in your continuation token.

If I'm incorrect on the above assumption, please let me know. Because halfway through paginating I get a token that looks like this:

[{\"token\":null,\"range\":{\"min\":\"05C1DFFFFFFFFC\",\"max\":\"FF\"}}]

Anyways, I'm marking this as self-resolved. Hopefully it helps someone else out there with the same issue.

Nate
  • 345
  • 3
  • 12
0
 IQueryable<returnVModel> query;
 var requestOptions = new QueryRequestOptions
 {
     MaxItemCount = 20
 };
 if (Token == "" || Token == null)
 {
        query = Container.GetItemLinqQueryable<returnVModel>(false, null, requestOptions).Where(x => x.id == id);
 }
 else
 {
        query = Container.GetItemLinqQueryable<returnVModel>(false, Token, requestOptions).Where(x => x.id == id);
 }
 var ct = new CancellationTokenSource();
 var totalCount = await query.CountAsync(ct.Token); //Total Count
 var feedIterator = query.ToFeedIterator();
 var queryResults = new List<returnVModel>();
 FeedResponse<returnVModel> feedResults = await feedIterator.ReadNextAsync(ct.Token);
 queryResults.AddRange(feedResults); // Output
 var PaginationToken = feedResults.ContinuationToken //Token

@Nate I was refered your code and implemented pagination, It was working fine in my development. Whether i can get any issue after migrate into production environment.

Prince Antony G
  • 932
  • 4
  • 18
  • 39