/// <summary>
        /// Creates a Firestore document that stores the cache value and its
        /// expiration info, but does not store it in Firestore.
        /// </summary>
        /// <param name="value">The value to be stored in the cache.</param>
        /// <param name="options">Expiration info.</param>
        /// <returns></returns>
        private CacheDoc MakeCacheDoc(byte[] value, DistributedCacheEntryOptions options)
        {
            CacheDoc doc = new CacheDoc()
            {
                LastRefresh = _clock.GetCurrentDateTimeUtc(),
                Value       = value,
            };

            if (options.SlidingExpiration.HasValue)
            {
                doc.SlidingExpirationSeconds = options.SlidingExpiration.Value.TotalSeconds;
            }
            else
            {
                doc.SlidingExpirationSeconds = _forever.TotalSeconds;
            }
            if (options.AbsoluteExpiration.HasValue)
            {
                doc.AbsoluteExpiration = options.AbsoluteExpiration.Value.UtcDateTime;
            }
            if (options.AbsoluteExpirationRelativeToNow.HasValue)
            {
                doc.AbsoluteExpiration = _clock.GetCurrentDateTimeUtc() +
                                         options.AbsoluteExpirationRelativeToNow.Value;
            }
            return(doc);
        }
        /// <summary>
        /// Extracts the cache value from a Firestore document snapshot.
        /// Checks all the expiration fields so never returns a stale value.
        /// </summary>
        /// <param name="snapshot">The snapshot to pull the cache value from.</param>
        /// <returns>The cache value or null.</returns>
        private byte[] ValueFromSnapshot(DocumentSnapshot snapshot)
        {
            if (!snapshot.Exists)
            {
                return(null);
            }
            CacheDoc doc = snapshot.ConvertTo <CacheDoc>();
            var      now = _clock.GetCurrentDateTimeUtc();

            if (doc.AbsoluteExpiration < now)
            {
                return(null);
            }
            var slidingExpiration = doc.LastRefresh.GetValueOrDefault()
                                    + TimeSpan.FromSeconds(doc.SlidingExpirationSeconds.GetValueOrDefault());

            if (slidingExpiration < now)
            {
                return(null);
            }
            return(doc.Value);
        }
        /// <summary>
        /// Scans the Firestore collection for expired cache entries and
        /// deletes them.
        /// </summary>
        /// <param name="token">A cancellation token.</param>
        public async Task CollectGarbageAsync(CancellationToken token)
        {
            _logger.LogTrace("Begin garbage collection.");
            // Purge entries whose AbsoluteExpiration has passed.
            const int pageSize = 40;
            int       batchSize; // Batches of cache entries to be deleted.
            var       now = _clock.GetCurrentDateTimeUtc();

            do
            {
                QuerySnapshot querySnapshot = await
                                              _cacheEntries.OrderByDescending("AbsoluteExpiration")
                                              .StartAfter(now)
                                              .Limit(pageSize)
                                              .GetSnapshotAsync(token)
                                              .ConfigureAwait(false);

                batchSize = 0;
                WriteBatch writeBatch = _cacheEntries.Database.StartBatch();
                foreach (DocumentSnapshot docSnapshot in querySnapshot.Documents)
                {
                    if (docSnapshot.ConvertTo <CacheDoc>().AbsoluteExpiration.HasValue)
                    {
                        writeBatch.Delete(docSnapshot.Reference, Precondition.LastUpdated(
                                              docSnapshot.UpdateTime.GetValueOrDefault()));
                        batchSize += 1;
                    }
                }
                if (batchSize > 0)
                {
                    _logger.LogDebug("Collecting {0} cache entries.", batchSize);
                    await writeBatch.CommitAsync(token).ConfigureAwait(false);
                }
                token.ThrowIfCancellationRequested();
            } while (batchSize == pageSize);

            // Purge entries whose SlidingExpiration has passed.
            do
            {
                QuerySnapshot querySnapshot = await
                                              _cacheEntries.OrderBy("LastRefresh")
                                              .Limit(pageSize)
                                              .GetSnapshotAsync(token)
                                              .ConfigureAwait(false);

                batchSize = 0;
                WriteBatch writeBatch = _cacheEntries.Database.StartBatch();
                foreach (DocumentSnapshot docSnapshot in querySnapshot.Documents)
                {
                    CacheDoc doc = docSnapshot.ConvertTo <CacheDoc>();
                    if (doc.SlidingExpirationSeconds.HasValue)
                    {
                        var slidingExpiration =
                            doc.LastRefresh.GetValueOrDefault()
                            + TimeSpan.FromSeconds(doc.SlidingExpirationSeconds.Value);
                        if (slidingExpiration < now)
                        {
                            writeBatch.Delete(docSnapshot.Reference, Precondition.LastUpdated(
                                                  docSnapshot.UpdateTime.GetValueOrDefault()));
                            batchSize += 1;
                        }
                    }
                }
                if (batchSize > 0)
                {
                    _logger.LogDebug("Collecting {batchSize} cache entries.", batchSize);
                    await writeBatch.CommitAsync(token).ConfigureAwait(false);
                }
                token.ThrowIfCancellationRequested();
            } while (batchSize > 0);
            _logger.LogTrace("End garbage collection.");
        }