// BaseKey<delimiter>H<delimiter>HeaderName=HeaderValue<delimiter>Q<delimiter>QueryName=QueryValue1<subdelimiter>QueryValue2
        public string CreateStorageVaryByKey(ResponseCachingContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            var varyByRules = context.CachedVaryByRules;

            if (varyByRules == null)
            {
                throw new InvalidOperationException($"{nameof(CachedVaryByRules)} must not be null on the {nameof(ResponseCachingContext)}");
            }

            if (StringValues.IsNullOrEmpty(varyByRules.Headers) && StringValues.IsNullOrEmpty(varyByRules.QueryKeys))
            {
                return(varyByRules.VaryByKeyPrefix);
            }

            var request = context.HttpContext.Request;
            var builder = _builderPool.Get();

            try
            {
                // Prepend with the Guid of the CachedVaryByRules
                builder.Append(varyByRules.VaryByKeyPrefix);

                // Vary by headers
                var headersCount = varyByRules?.Headers.Count ?? 0;
                if (headersCount > 0)
                {
                    // Append a group separator for the header segment of the cache key
                    builder.Append(KeyDelimiter)
                    .Append('H');

                    var requestHeaders = context.HttpContext.Request.Headers;
                    for (var i = 0; i < headersCount; i++)
                    {
                        var header       = varyByRules !.Headers[i] !;
                        var headerValues = requestHeaders[header];
                        builder.Append(KeyDelimiter)
                        .Append(header)
                        .Append('=');

                        var headerValuesArray = headerValues.ToArray();
                        Array.Sort(headerValuesArray, StringComparer.Ordinal);

                        for (var j = 0; j < headerValuesArray.Length; j++)
                        {
                            builder.Append(headerValuesArray[j]);
                        }
                    }
                }

                // Vary by query keys
                if (varyByRules?.QueryKeys.Count > 0)
                {
                    // Append a group separator for the query key segment of the cache key
                    builder.Append(KeyDelimiter)
                    .Append('Q');

                    if (varyByRules.QueryKeys.Count == 1 && string.Equals(varyByRules.QueryKeys[0], "*", StringComparison.Ordinal))
                    {
                        // Vary by all available query keys
                        var queryArray = context.HttpContext.Request.Query.ToArray();
                        // Query keys are aggregated case-insensitively whereas the query values are compared ordinally.
                        Array.Sort(queryArray, QueryKeyComparer.OrdinalIgnoreCase);

                        for (var i = 0; i < queryArray.Length; i++)
                        {
                            builder.Append(KeyDelimiter)
                            .AppendUpperInvariant(queryArray[i].Key)
                            .Append('=');

                            var queryValueArray = queryArray[i].Value.ToArray();
                            Array.Sort(queryValueArray, StringComparer.Ordinal);

                            for (var j = 0; j < queryValueArray.Length; j++)
                            {
                                if (j > 0)
                                {
                                    builder.Append(KeySubDelimiter);
                                }

                                builder.Append(queryValueArray[j]);
                            }
                        }
                    }
                    else
                    {
                        for (var i = 0; i < varyByRules.QueryKeys.Count; i++)
                        {
                            var queryKey       = varyByRules.QueryKeys[i] !;
                            var queryKeyValues = context.HttpContext.Request.Query[queryKey];
                            builder.Append(KeyDelimiter)
                            .Append(queryKey)
                            .Append('=');

                            var queryValueArray = queryKeyValues.ToArray();
                            Array.Sort(queryValueArray, StringComparer.Ordinal);

                            for (var j = 0; j < queryValueArray.Length; j++)
                            {
                                if (j > 0)
                                {
                                    builder.Append(KeySubDelimiter);
                                }

                                builder.Append(queryValueArray[j]);
                            }
                        }
                    }
                }

                return(builder.ToString());
            }
            finally
            {
                _builderPool.Return(builder);
            }
        }
        /// <summary>
        /// Finalize cache headers.
        /// </summary>
        /// <param name="context"></param>
        /// <returns><c>true</c> if a vary by entry needs to be stored in the cache; otherwise <c>false</c>.</returns>
        private bool OnFinalizeCacheHeaders(ResponseCachingContext context)
        {
            if (_policyProvider.IsResponseCacheable(context))
            {
                var storeVaryByEntry = false;
                context.ShouldCacheResponse = true;

                // Create the cache entry now
                var response      = context.HttpContext.Response;
                var varyHeaders   = new StringValues(response.Headers.GetCommaSeparatedValues(HeaderNames.Vary));
                var varyQueryKeys = new StringValues(context.HttpContext.Features.Get <IResponseCachingFeature>()?.VaryByQueryKeys);
                context.CachedResponseValidFor = context.ResponseSharedMaxAge ??
                                                 context.ResponseMaxAge ??
                                                 (context.ResponseExpires - context.ResponseTime.Value) ??
                                                 DefaultExpirationTimeSpan;

                // Generate a base key if none exist
                if (string.IsNullOrEmpty(context.BaseKey))
                {
                    context.BaseKey = _keyProvider.CreateBaseKey(context);
                }

                // Check if any vary rules exist
                if (!StringValues.IsNullOrEmpty(varyHeaders) || !StringValues.IsNullOrEmpty(varyQueryKeys))
                {
                    // Normalize order and casing of vary by rules
                    var normalizedVaryHeaders   = GetOrderCasingNormalizedStringValues(varyHeaders);
                    var normalizedVaryQueryKeys = GetOrderCasingNormalizedStringValues(varyQueryKeys);

                    // Update vary rules if they are different
                    if (context.CachedVaryByRules == null ||
                        !StringValues.Equals(context.CachedVaryByRules.QueryKeys, normalizedVaryQueryKeys) ||
                        !StringValues.Equals(context.CachedVaryByRules.Headers, normalizedVaryHeaders))
                    {
                        context.CachedVaryByRules = new CachedVaryByRules
                        {
                            VaryByKeyPrefix = FastGuid.NewGuid().IdString,
                            Headers         = normalizedVaryHeaders,
                            QueryKeys       = normalizedVaryQueryKeys
                        };
                    }

                    // Always overwrite the CachedVaryByRules to update the expiry information
                    _logger.LogVaryByRulesUpdated(normalizedVaryHeaders, normalizedVaryQueryKeys);
                    storeVaryByEntry = true;

                    context.StorageVaryKey = _keyProvider.CreateStorageVaryByKey(context);
                }

                // Ensure date header is set
                if (!context.ResponseDate.HasValue)
                {
                    context.ResponseDate = context.ResponseTime.Value;
                    // Setting the date on the raw response headers.
                    context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(context.ResponseDate.Value);
                }

                // Store the response on the state
                context.CachedResponse = new CachedResponse
                {
                    Created    = context.ResponseDate.Value,
                    StatusCode = context.HttpContext.Response.StatusCode,
                    Headers    = new HeaderDictionary()
                };

                foreach (var header in context.HttpContext.Response.Headers)
                {
                    if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase))
                    {
                        context.CachedResponse.Headers[header.Key] = header.Value;
                    }
                }

                return(storeVaryByEntry);
            }

            context.ResponseCachingStream.DisableBuffering();
            return(false);
        }
 public IEnumerable <string> CreateLookupVaryByKeys(ResponseCachingContext context)
 {
     return(new string[] { CreateStorageVaryByKey(context) });
 }
        internal async Task <bool> TryServeCachedResponseAsync(ResponseCachingContext context, IResponseCacheEntry?cacheEntry)
        {
            if (!(cacheEntry is CachedResponse cachedResponse))
            {
                return(false);
            }

            context.CachedResponse        = cachedResponse;
            context.CachedResponseHeaders = cachedResponse.Headers;
            context.ResponseTime          = _options.SystemClock.UtcNow;
            var cachedEntryAge = context.ResponseTime.Value - context.CachedResponse.Created;

            context.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero;

            if (_policyProvider.IsCachedEntryFresh(context))
            {
                // Check conditional request rules
                if (ContentIsNotModified(context))
                {
                    _logger.NotModifiedServed();
                    context.HttpContext.Response.StatusCode = StatusCodes.Status304NotModified;

                    if (context.CachedResponseHeaders != null)
                    {
                        foreach (var key in HeadersToIncludeIn304)
                        {
                            if (context.CachedResponseHeaders.TryGetValue(key, out var values))
                            {
                                context.HttpContext.Response.Headers[key] = values;
                            }
                        }
                    }
                }
                else
                {
                    var response = context.HttpContext.Response;
                    // Copy the cached status code and response headers
                    response.StatusCode = context.CachedResponse.StatusCode;
                    foreach (var header in context.CachedResponse.Headers)
                    {
                        response.Headers[header.Key] = header.Value;
                    }

                    // Note: int64 division truncates result and errors may be up to 1 second. This reduction in
                    // accuracy of age calculation is considered appropriate since it is small compared to clock
                    // skews and the "Age" header is an estimate of the real age of cached content.
                    response.Headers.Age = HeaderUtilities.FormatNonNegativeInt64(context.CachedEntryAge.Value.Ticks / TimeSpan.TicksPerSecond);

                    // Copy the cached response body
                    var body = context.CachedResponse.Body;
                    if (body.Length > 0)
                    {
                        try
                        {
                            await body.CopyToAsync(response.BodyWriter, context.HttpContext.RequestAborted);
                        }
                        catch (OperationCanceledException)
                        {
                            context.HttpContext.Abort();
                        }
                    }
                    _logger.CachedResponseServed();
                }
                return(true);
            }

            return(false);
        }
        public virtual bool IsResponseCacheable(ResponseCachingContext context)
        {
            var responseCacheControlHeader = context.HttpContext.Response.Headers[HeaderNames.CacheControl];

            // Only cache pages explicitly marked with public
            if (!HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.PublicString))
            {
                context.Logger.ResponseWithoutPublicNotCacheable();
                return(false);
            }

            // Check response no-store
            if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoStoreString))
            {
                context.Logger.ResponseWithNoStoreNotCacheable();
                return(false);
            }

            // Check no-cache
            if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoCacheString))
            {
                context.Logger.ResponseWithNoCacheNotCacheable();
                return(false);
            }

            var response = context.HttpContext.Response;

            // Do not cache responses with Set-Cookie headers
            if (!StringValues.IsNullOrEmpty(response.Headers[HeaderNames.SetCookie]))
            {
                context.Logger.ResponseWithSetCookieNotCacheable();
                return(false);
            }

            // Do not cache responses varying by *
            var varyHeader = response.Headers[HeaderNames.Vary];

            if (varyHeader.Count == 1 && string.Equals(varyHeader, "*", StringComparison.OrdinalIgnoreCase))
            {
                context.Logger.ResponseWithVaryStarNotCacheable();
                return(false);
            }

            // Check private
            if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.PrivateString))
            {
                context.Logger.ResponseWithPrivateNotCacheable();
                return(false);
            }

            // Check response code
            if (response.StatusCode != StatusCodes.Status200OK)
            {
                context.Logger.ResponseWithUnsuccessfulStatusCodeNotCacheable(response.StatusCode);
                return(false);
            }

            // Check response freshness
            if (!context.ResponseDate.HasValue)
            {
                if (!context.ResponseSharedMaxAge.HasValue &&
                    !context.ResponseMaxAge.HasValue &&
                    context.ResponseTime.Value >= context.ResponseExpires)
                {
                    context.Logger.ExpirationExpiresExceeded(context.ResponseTime.Value, context.ResponseExpires.Value);
                    return(false);
                }
            }
            else
            {
                var age = context.ResponseTime.Value - context.ResponseDate.Value;

                // Validate shared max age
                if (age >= context.ResponseSharedMaxAge)
                {
                    context.Logger.ExpirationSharedMaxAgeExceeded(age, context.ResponseSharedMaxAge.Value);
                    return(false);
                }
                else if (!context.ResponseSharedMaxAge.HasValue)
                {
                    // Validate max age
                    if (age >= context.ResponseMaxAge)
                    {
                        context.Logger.ExpirationMaxAgeExceeded(age, context.ResponseMaxAge.Value);
                        return(false);
                    }
                    else if (!context.ResponseMaxAge.HasValue)
                    {
                        // Validate expiration
                        if (context.ResponseTime.Value >= context.ResponseExpires)
                        {
                            context.Logger.ExpirationExpiresExceeded(context.ResponseTime.Value, context.ResponseExpires.Value);
                            return(false);
                        }
                    }
                }
            }

            return(true);
        }
 public virtual bool AllowCacheStorage(ResponseCachingContext context)
 {
     // Check request no-store
     return(!HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.NoStoreString));
 }
        public virtual bool IsCachedEntryFresh(ResponseCachingContext context)
        {
            var age = context.CachedEntryAge.Value;
            var cachedCacheControlHeaders  = context.CachedResponseHeaders[HeaderNames.CacheControl];
            var requestCacheControlHeaders = context.HttpContext.Request.Headers[HeaderNames.CacheControl];

            // Add min-fresh requirements
            TimeSpan?minFresh;

            if (HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MinFreshString, out minFresh))
            {
                age += minFresh.Value;
                context.Logger.ExpirationMinFreshAdded(minFresh.Value);
            }

            // Validate shared max age, this overrides any max age settings for shared caches
            TimeSpan?cachedSharedMaxAge;

            HeaderUtilities.TryParseSeconds(cachedCacheControlHeaders, CacheControlHeaderValue.SharedMaxAgeString, out cachedSharedMaxAge);

            if (age >= cachedSharedMaxAge)
            {
                // shared max age implies must revalidate
                context.Logger.ExpirationSharedMaxAgeExceeded(age, cachedSharedMaxAge.Value);
                return(false);
            }
            else if (!cachedSharedMaxAge.HasValue)
            {
                TimeSpan?requestMaxAge;
                HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxAgeString, out requestMaxAge);

                TimeSpan?cachedMaxAge;
                HeaderUtilities.TryParseSeconds(cachedCacheControlHeaders, CacheControlHeaderValue.MaxAgeString, out cachedMaxAge);

                var lowestMaxAge = cachedMaxAge < requestMaxAge ? cachedMaxAge : requestMaxAge ?? cachedMaxAge;
                // Validate max age
                if (age >= lowestMaxAge)
                {
                    // Must revalidate or proxy revalidate
                    if (HeaderUtilities.ContainsCacheDirective(cachedCacheControlHeaders, CacheControlHeaderValue.MustRevalidateString) ||
                        HeaderUtilities.ContainsCacheDirective(cachedCacheControlHeaders, CacheControlHeaderValue.ProxyRevalidateString))
                    {
                        context.Logger.ExpirationMustRevalidate(age, lowestMaxAge.Value);
                        return(false);
                    }

                    TimeSpan?requestMaxStale;
                    var      maxStaleExist = HeaderUtilities.ContainsCacheDirective(requestCacheControlHeaders, CacheControlHeaderValue.MaxStaleString);
                    HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxStaleString, out requestMaxStale);

                    // Request allows stale values with no age limit
                    if (maxStaleExist && !requestMaxStale.HasValue)
                    {
                        context.Logger.ExpirationInfiniteMaxStaleSatisfied(age, lowestMaxAge.Value);
                        return(true);
                    }

                    // Request allows stale values with age limit
                    if (requestMaxStale.HasValue && age - lowestMaxAge < requestMaxStale)
                    {
                        context.Logger.ExpirationMaxStaleSatisfied(age, lowestMaxAge.Value, requestMaxStale.Value);
                        return(true);
                    }

                    context.Logger.ExpirationMaxAgeExceeded(age, lowestMaxAge.Value);
                    return(false);
                }
                else if (!cachedMaxAge.HasValue && !requestMaxAge.HasValue)
                {
                    // Validate expiration
                    DateTimeOffset expires;
                    if (HeaderUtilities.TryParseDate(context.CachedResponseHeaders[HeaderNames.Expires].ToString(), out expires) &&
                        context.ResponseTime.Value >= expires)
                    {
                        context.Logger.ExpirationExpiresExceeded(context.ResponseTime.Value, expires);
                        return(false);
                    }
                }
            }

            return(true);
        }