/// <summary>
        /// Gets <see cref="PartitionKeyRange"/> instance which corresponds to <paramref name="rangeFromContinuationToken"/>
        /// </summary>
        /// <param name="providedPartitionKeyRanges"></param>
        /// <param name="routingMapProvider"></param>
        /// <param name="collectionRid"></param>
        /// <param name="rangeFromContinuationToken"></param>
        /// <param name="suppliedTokens"></param>
        /// <param name="direction"></param>
        /// <returns>null if collection with specified <paramref name="collectionRid"/> doesn't exist, which potentially means
        /// that collection was resolved to outdated Rid by name. Also null can be returned if <paramref name="rangeFromContinuationToken"/>
        /// is not found - this means it was split.
        /// </returns>
        public virtual async Task <ResolvedRangeInfo> TryGetTargetRangeFromContinuationTokenRangeAsync(
            IReadOnlyList <Range <string> > providedPartitionKeyRanges,
            IRoutingMapProvider routingMapProvider,
            string collectionRid,
            Range <string> rangeFromContinuationToken,
            List <CompositeContinuationToken> suppliedTokens,
            RntdbEnumerationDirection direction = RntdbEnumerationDirection.Forward)
        {
            // For queries such as "SELECT * FROM root WHERE false",
            // we will have empty ranges and just forward the request to the first partition
            if (providedPartitionKeyRanges.Count == 0)
            {
                return(new ResolvedRangeInfo(
                           await routingMapProvider.TryGetRangeByEffectivePartitionKeyAsync(
                               collectionRid,
                               PartitionKeyInternal.MinimumInclusiveEffectivePartitionKey),
                           suppliedTokens));
            }

            // Initially currentRange will be empty
            if (rangeFromContinuationToken.IsEmpty)
            {
                if (direction == RntdbEnumerationDirection.Reverse)
                {
                    PartitionKeyRange lastPartitionKeyRange = (await routingMapProvider.TryGetOverlappingRangesAsync(collectionRid, providedPartitionKeyRanges.Single())).Last();
                    return(new ResolvedRangeInfo(
                               lastPartitionKeyRange,
                               suppliedTokens));
                }

                Range <string> minimumRange = PartitionRoutingHelper.Min(
                    providedPartitionKeyRanges,
                    Range <string> .MinComparer.Instance);

                return(new ResolvedRangeInfo(
                           await routingMapProvider.TryGetRangeByEffectivePartitionKeyAsync(collectionRid, minimumRange.Min),
                           suppliedTokens));
            }

            PartitionKeyRange targetPartitionKeyRange = await routingMapProvider.TryGetRangeByEffectivePartitionKeyAsync(collectionRid, rangeFromContinuationToken.Min);

            if (targetPartitionKeyRange == null)
            {
                return(new ResolvedRangeInfo(null, suppliedTokens));
            }

            if (!rangeFromContinuationToken.Equals(targetPartitionKeyRange.ToRange()))
            {
                // Cannot find target range. Either collection was resolved incorrectly or the range was split
                List <PartitionKeyRange> replacedRanges = (await routingMapProvider.TryGetOverlappingRangesAsync(collectionRid, rangeFromContinuationToken, true)).ToList();

                if (replacedRanges == null || replacedRanges.Count < 1)
                {
                    return(new ResolvedRangeInfo(null, null));
                }
                else
                {
                    if (!(replacedRanges[0].MinInclusive.Equals(rangeFromContinuationToken.Min) && replacedRanges[replacedRanges.Count - 1].MaxExclusive.Equals(rangeFromContinuationToken.Max)))
                    {
                        return(new ResolvedRangeInfo(null, null));
                    }
                }

                if (direction == RntdbEnumerationDirection.Reverse)
                {
                    replacedRanges.Reverse();
                }

                List <CompositeContinuationToken> continuationTokensToBePersisted = null;

                if (suppliedTokens != null && suppliedTokens.Count > 0)
                {
                    continuationTokensToBePersisted = new List <CompositeContinuationToken>(replacedRanges.Count + suppliedTokens.Count - 1);

                    foreach (PartitionKeyRange partitionKeyRange in replacedRanges)
                    {
                        CompositeContinuationToken token = (CompositeContinuationToken)suppliedTokens[0].ShallowCopy();
                        token.Range = partitionKeyRange.ToRange();
                        continuationTokensToBePersisted.Add(token);
                    }

                    continuationTokensToBePersisted.AddRange(suppliedTokens.Skip(1));
                }

                return(new ResolvedRangeInfo(replacedRanges[0], continuationTokensToBePersisted));
            }

            return(new ResolvedRangeInfo(targetPartitionKeyRange, suppliedTokens));
        }
        public virtual async Task <bool> TryAddPartitionKeyRangeToContinuationTokenAsync(
            INameValueCollection backendResponseHeaders,
            IReadOnlyList <Range <string> > providedPartitionKeyRanges,
            IRoutingMapProvider routingMapProvider,
            string collectionRid,
            ResolvedRangeInfo resolvedRangeInfo,
            RntdbEnumerationDirection direction = RntdbEnumerationDirection.Forward)
        {
            Debug.Assert(resolvedRangeInfo.ResolvedRange != null, "ResolvedRange can't be null");

            PartitionKeyRange currentRange = resolvedRangeInfo.ResolvedRange;

            // IF : Split happened, or already had multiple target ranges in the continuation
            if (resolvedRangeInfo.ContinuationTokens != null && resolvedRangeInfo.ContinuationTokens.Count > 1)
            {
                if (!string.IsNullOrEmpty(backendResponseHeaders[HttpConstants.HttpHeaders.Continuation]))
                {
                    resolvedRangeInfo.ContinuationTokens[0].Token = backendResponseHeaders[HttpConstants.HttpHeaders.Continuation];
                }
                else
                {
                    resolvedRangeInfo.ContinuationTokens.RemoveAt(0);
                }

                backendResponseHeaders[HttpConstants.HttpHeaders.Continuation] = JsonConvert.SerializeObject(resolvedRangeInfo.ContinuationTokens);
            }
            else
            {
                //// ELSE: Single target Range was provided, and no split happened

                PartitionKeyRange rangeToUse = currentRange;

                // We only need to get the next range if we have to
                if (string.IsNullOrEmpty(backendResponseHeaders[HttpConstants.HttpHeaders.Continuation]))
                {
                    if (direction == RntdbEnumerationDirection.Reverse)
                    {
                        rangeToUse = PartitionRoutingHelper.MinBefore(
                            (await routingMapProvider.TryGetOverlappingRangesAsync(collectionRid, providedPartitionKeyRanges.Single())).ToList(),
                            currentRange);
                    }
                    else
                    {
                        Range <string> nextProvidedRange = PartitionRoutingHelper.MinAfter(
                            providedPartitionKeyRanges,
                            currentRange.ToRange(),
                            Range <string> .MaxComparer.Instance);

                        if (nextProvidedRange == null)
                        {
                            return(true);
                        }

                        string max = string.CompareOrdinal(nextProvidedRange.Min, currentRange.MaxExclusive) > 0
                             ? nextProvidedRange.Min
                             : currentRange.MaxExclusive;

                        if (string.CompareOrdinal(max, PartitionKeyInternal.MaximumExclusiveEffectivePartitionKey) == 0)
                        {
                            return(true);
                        }

                        PartitionKeyRange nextRange = await routingMapProvider.TryGetRangeByEffectivePartitionKeyAsync(collectionRid, max);

                        if (nextRange == null)
                        {
                            return(false);
                        }

                        rangeToUse = nextRange;
                    }
                }

                if (rangeToUse != null)
                {
                    backendResponseHeaders[HttpConstants.HttpHeaders.Continuation] = PartitionRoutingHelper.AddPartitionKeyRangeToContinuationToken(
                        backendResponseHeaders[HttpConstants.HttpHeaders.Continuation],
                        rangeToUse);
                }
            }

            return(true);
        }
        public override async Task <ResponseMessage> SendAsync(
            RequestMessage request,
            CancellationToken cancellationToken)
        {
            using (ITrace childTrace = request.Trace.StartChild(this.FullHandlerName, TraceComponent.RequestHandler, Tracing.TraceLevel.Info))
            {
                request.Trace = childTrace;

                ResponseMessage response             = null;
                string          originalContinuation = request.Headers.ContinuationToken;
                try
                {
                    RntdbEnumerationDirection rntdbEnumerationDirection = RntdbEnumerationDirection.Forward;
                    if (request.Properties.TryGetValue(HttpConstants.HttpHeaders.EnumerationDirection, out object direction))
                    {
                        rntdbEnumerationDirection = (byte)direction == (byte)RntdbEnumerationDirection.Reverse ? RntdbEnumerationDirection.Reverse : RntdbEnumerationDirection.Forward;
                    }

                    request.Headers.Remove(HttpConstants.HttpHeaders.IsContinuationExpected);
                    request.Headers.Add(HttpConstants.HttpHeaders.IsContinuationExpected, bool.TrueString);

                    if (!request.Properties.TryGetValue(HandlerConstants.StartEpkString, out object startEpk))
                    {
                        startEpk = PartitionKeyInternal.MinimumInclusiveEffectivePartitionKey;
                    }

                    if (!request.Properties.TryGetValue(HandlerConstants.EndEpkString, out object endEpk))
                    {
                        endEpk = PartitionKeyInternal.MaximumExclusiveEffectivePartitionKey;
                    }

                    startEpk ??= PartitionKeyInternal.MinimumInclusiveEffectivePartitionKey;
                    endEpk ??= PartitionKeyInternal.MaximumExclusiveEffectivePartitionKey;

                    List <Range <string> > providedRanges = new List <Range <string> >
                    {
                        new Range <string>(
                            (string)startEpk,
                            (string)endEpk,
                            isMinInclusive: true,
                            isMaxInclusive: false)
                    };

                    DocumentServiceRequest serviceRequest = request.ToDocumentServiceRequest();

                    PartitionKeyRangeCache routingMapProvider = await this.client.DocumentClient.GetPartitionKeyRangeCacheAsync();

                    CollectionCache collectionCache = await this.client.DocumentClient.GetCollectionCacheAsync(NoOpTrace.Singleton);

                    ContainerProperties collectionFromCache =
                        await collectionCache.ResolveCollectionAsync(serviceRequest, CancellationToken.None);

                    //direction is not expected to change  between continuations.
                    Range <string> rangeFromContinuationToken =
                        this.partitionRoutingHelper.ExtractPartitionKeyRangeFromContinuationToken(serviceRequest.Headers, out List <CompositeContinuationToken> suppliedTokens);

                    ResolvedRangeInfo resolvedRangeInfo =
                        await this.partitionRoutingHelper.TryGetTargetRangeFromContinuationTokenRangeAsync(
                            providedPartitionKeyRanges : providedRanges,
                            routingMapProvider : routingMapProvider,
                            collectionRid : collectionFromCache.ResourceId,
                            rangeFromContinuationToken : rangeFromContinuationToken,
                            suppliedTokens : suppliedTokens,
                            direction : rntdbEnumerationDirection);

                    if (serviceRequest.IsNameBased && resolvedRangeInfo.ResolvedRange == null && resolvedRangeInfo.ContinuationTokens == null)
                    {
                        serviceRequest.ForceNameCacheRefresh = true;
                        collectionFromCache = await collectionCache.ResolveCollectionAsync(serviceRequest, CancellationToken.None);

                        resolvedRangeInfo = await this.partitionRoutingHelper.TryGetTargetRangeFromContinuationTokenRangeAsync(
                            providedPartitionKeyRanges : providedRanges,
                            routingMapProvider : routingMapProvider,
                            collectionRid : collectionFromCache.ResourceId,
                            rangeFromContinuationToken : rangeFromContinuationToken,
                            suppliedTokens : suppliedTokens,
                            direction : rntdbEnumerationDirection);
                    }

                    if (resolvedRangeInfo.ResolvedRange == null && resolvedRangeInfo.ContinuationTokens == null)
                    {
                        return(((DocumentClientException) new NotFoundException(
                                    $"{DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)}: Was not able to get queryRoutingInfo even after resolve collection async with force name cache refresh to the following collectionRid: {collectionFromCache.ResourceId} with the supplied tokens: {JsonConvert.SerializeObject(suppliedTokens)}")
                                ).ToCosmosResponseMessage(request));
                    }

                    serviceRequest.RouteTo(new PartitionKeyRangeIdentity(collectionFromCache.ResourceId, resolvedRangeInfo.ResolvedRange.Id));

                    response = await base.SendAsync(request, cancellationToken);

                    if (!response.IsSuccessStatusCode)
                    {
                        this.SetOriginalContinuationToken(request, response, originalContinuation);
                    }
                    else
                    {
                        if (!await this.partitionRoutingHelper.TryAddPartitionKeyRangeToContinuationTokenAsync(
                                response.Headers.CosmosMessageHeaders,
                                providedPartitionKeyRanges: providedRanges,
                                routingMapProvider: routingMapProvider,
                                collectionRid: collectionFromCache.ResourceId,
                                resolvedRangeInfo: resolvedRangeInfo,
                                direction: rntdbEnumerationDirection))
                        {
                            return(((DocumentClientException) new NotFoundException(
                                        $"{DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture)}: Call to TryAddPartitionKeyRangeToContinuationTokenAsync failed to the following collectionRid: {collectionFromCache.ResourceId} with the supplied tokens: {JsonConvert.SerializeObject(suppliedTokens)}")
                                    ).ToCosmosResponseMessage(request));
                        }
                    }

                    return(response);
                }
                catch (DocumentClientException ex)
                {
                    ResponseMessage errorResponse = ex.ToCosmosResponseMessage(request);
                    this.SetOriginalContinuationToken(request, errorResponse, originalContinuation);
                    return(errorResponse);
                }
                catch (CosmosException ex)
                {
                    ResponseMessage errorResponse = ex.ToCosmosResponseMessage(request);
                    this.SetOriginalContinuationToken(request, errorResponse, originalContinuation);
                    return(errorResponse);
                }
                catch (AggregateException ex)
                {
                    this.SetOriginalContinuationToken(request, response, originalContinuation);

                    // TODO: because the SDK underneath this path uses ContinueWith or task.Result we need to catch AggregateExceptions here
                    // in order to ensure that underlying DocumentClientExceptions get propagated up correctly. Once all ContinueWith and .Result
                    // is removed this catch can be safely removed.
                    AggregateException innerExceptions    = ex.Flatten();
                    Exception          docClientException = innerExceptions.InnerExceptions.FirstOrDefault(innerEx => innerEx is DocumentClientException);
                    if (docClientException != null)
                    {
                        return(((DocumentClientException)docClientException).ToCosmosResponseMessage(request));
                    }

                    throw;
                }
            }
        }