From ccfa090e6e7d359e6fcbddb7b6a98a619b69dfcf Mon Sep 17 00:00:00 2001 From: John Luo Date: Mon, 12 Sep 2016 17:36:47 -0700 Subject: [PATCH] API review renames and updates --- .../CacheEntry/CacheEntrySerializer.cs | 10 +- .../CacheEntry/CachedResponse.cs | 14 +- .../CacheEntry/CachedResponseBody.cs | 6 +- .../CacheEntry/CachedVaryRules.cs | 16 +- .../{KeyProvider.cs => CacheKeyProvider.cs} | 71 +- .../CacheabilityValidator.cs | 61 +- .../ResponseCachingHttpContextExtensions.cs | 6 - ...ponseCachingServiceCollectionExtensions.cs | 2 +- .../Interfaces/ICacheKeyProvider.cs | 38 + .../Interfaces/ICacheabilityValidator.cs | 16 +- .../Interfaces/IKeyProvider.cs | 41 - ...ns.cs => InternalHttpContextExtensions.cs} | 21 +- .../Internal/ResponseCachingState.cs | 91 --- .../ResponseCachingContext.cs | 364 ++------- .../ResponseCachingMiddleware.cs | 378 +++++++-- .../VaryRules.cs | 13 - .../CacheEntrySerializerTests.cs | 29 +- .../CacheabilityValidatorTests.cs | 331 ++++---- .../HttpContextInternalExtensionTests.cs | 12 - .../KeyProviderTests.cs | 182 +++-- .../ResponseCachingContextTests.cs | 742 ------------------ .../ResponseCachingMiddlewareTests.cs | 578 ++++++++++++++ .../ResponseCachingTests.cs | 138 +--- .../TestUtils.cs | 211 +++++ 24 files changed, 1602 insertions(+), 1769 deletions(-) rename src/Microsoft.AspNetCore.ResponseCaching/{KeyProvider.cs => CacheKeyProvider.cs} (63%) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheKeyProvider.cs delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs rename src/Microsoft.AspNetCore.ResponseCaching/Internal/{HttpContextInternalExtensions.cs => InternalHttpContextExtensions.cs} (51%) delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/VaryRules.cs delete mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs create mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs create mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs index 48d28e319f..caa4061773 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs @@ -146,7 +146,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal param[index] = reader.ReadString(); } - return new CachedVaryRules { VaryKeyPrefix = varyKeyPrefix, VaryRules = new VaryRules() { Headers = headers, Params = param } }; + return new CachedVaryRules { VaryKeyPrefix = varyKeyPrefix, Headers = headers, Params = param }; } // See serialization format above @@ -222,14 +222,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { writer.Write(varyRules.VaryKeyPrefix); - writer.Write(varyRules.VaryRules.Headers.Count); - foreach (var header in varyRules.VaryRules.Headers) + writer.Write(varyRules.Headers.Count); + foreach (var header in varyRules.Headers) { writer.Write(header); } - writer.Write(varyRules.VaryRules.Params.Count); - foreach (var param in varyRules.VaryRules.Params) + writer.Write(varyRules.Params.Count); + foreach (var param in varyRules.Params) { writer.Write(param); } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs index d4413bf4d1..552b29d96a 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs @@ -4,18 +4,18 @@ using System; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.ResponseCaching.Internal +namespace Microsoft.AspNetCore.ResponseCaching { - internal class CachedResponse + public class CachedResponse { - internal string BodyKeyPrefix { get; set; } + public string BodyKeyPrefix { get; internal set; } - internal DateTimeOffset Created { get; set; } + public DateTimeOffset Created { get; internal set; } - internal int StatusCode { get; set; } + public int StatusCode { get; internal set; } - internal IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); + public IHeaderDictionary Headers { get; internal set; } = new HeaderDictionary(); - internal byte[] Body { get; set; } + public byte[] Body { get; internal set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs index 643fac449c..d714e3f131 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs @@ -1,10 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace Microsoft.AspNetCore.ResponseCaching.Internal +namespace Microsoft.AspNetCore.ResponseCaching { - internal class CachedResponseBody + public class CachedResponseBody { - internal byte[] Body { get; set; } + public byte[] Body { get; internal set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryRules.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryRules.cs index de3355e761..f3a8a9a75a 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryRules.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryRules.cs @@ -1,12 +1,16 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -namespace Microsoft.AspNetCore.ResponseCaching.Internal -{ - internal class CachedVaryRules - { - internal string VaryKeyPrefix { get; set; } +using Microsoft.Extensions.Primitives; - internal VaryRules VaryRules { get; set; } +namespace Microsoft.AspNetCore.ResponseCaching +{ + public class CachedVaryRules + { + public string VaryKeyPrefix { get; internal set; } + + public StringValues Headers { get; internal set; } + + public StringValues Params { get; internal set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheKeyProvider.cs similarity index 63% rename from src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs rename to src/Microsoft.AspNetCore.ResponseCaching/CacheKeyProvider.cs index 4de7a40b36..4e42f92083 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheKeyProvider.cs @@ -6,14 +6,13 @@ using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.ResponseCaching { - public class KeyProvider : IKeyProvider + public class CacheKeyProvider : ICacheKeyProvider { // Use the record separator for delimiting components of the cache key to avoid possible collisions private static readonly char KeyDelimiter = '\x1e'; @@ -21,7 +20,7 @@ namespace Microsoft.AspNetCore.ResponseCaching private readonly ObjectPool _builderPool; private readonly ResponseCachingOptions _options; - public KeyProvider(ObjectPoolProvider poolProvider, IOptions options) + public CacheKeyProvider(ObjectPoolProvider poolProvider, IOptions options) { if (poolProvider == null) { @@ -36,26 +35,25 @@ namespace Microsoft.AspNetCore.ResponseCaching _options = options.Value; } - public virtual IEnumerable CreateLookupBaseKey(HttpContext httpContext) + public virtual IEnumerable CreateLookupBaseKeys(ResponseCachingContext context) { - return new string[] { CreateStorageBaseKey(httpContext) }; + return new string[] { CreateStorageBaseKey(context) }; } - public virtual IEnumerable CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules) + public virtual IEnumerable CreateLookupVaryKeys(ResponseCachingContext context) { - return new string[] { CreateStorageVaryKey(httpContext, varyRules) }; + return new string[] { CreateStorageVaryKey(context) }; } // GET/PATH - // TODO: Method invariant retrieval? E.g. HEAD after GET to the same resource. - public virtual string CreateStorageBaseKey(HttpContext httpContext) + public virtual string CreateStorageBaseKey(ResponseCachingContext context) { - if (httpContext == null) + if (context == null) { - throw new ArgumentNullException(nameof(httpContext)); + throw new ArgumentNullException(nameof(context)); } - var request = httpContext.Request; + var request = context.HttpContext.Request; var builder = _builderPool.Get(); try @@ -74,24 +72,31 @@ namespace Microsoft.AspNetCore.ResponseCaching } // BaseKeyHHeaderName=HeaderValueQQueryName=QueryValue - public virtual string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules) + public virtual string CreateStorageVaryKey(ResponseCachingContext context) { - if (httpContext == null) + if (context == null) { - throw new ArgumentNullException(nameof(httpContext)); - } - if (varyRules == null || (StringValues.IsNullOrEmpty(varyRules.Headers) && StringValues.IsNullOrEmpty(varyRules.Params))) - { - return httpContext.GetResponseCachingState().CachedVaryRules.VaryKeyPrefix; + throw new ArgumentNullException(nameof(context)); } - var request = httpContext.Request; + var varyRules = context.CachedVaryRules; + if (varyRules == null) + { + throw new InvalidOperationException($"{nameof(CachedVaryRules)} must not be null on the {nameof(ResponseCachingContext)}"); + } + + if ((StringValues.IsNullOrEmpty(varyRules.Headers) && StringValues.IsNullOrEmpty(varyRules.Params))) + { + return varyRules.VaryKeyPrefix; + } + + var request = context.HttpContext.Request; var builder = _builderPool.Get(); try { // Prepend with the Guid of the CachedVaryRules - builder.Append(httpContext.GetResponseCachingState().CachedVaryRules.VaryKeyPrefix); + builder.Append(varyRules.VaryKeyPrefix); // Vary by headers if (varyRules?.Headers.Count > 0) @@ -102,18 +107,11 @@ namespace Microsoft.AspNetCore.ResponseCaching foreach (var header in varyRules.Headers) { - var value = httpContext.Request.Headers[header]; - - // TODO: How to handle null/empty string? - if (StringValues.IsNullOrEmpty(value)) - { - value = "null"; - } - builder.Append(KeyDelimiter) .Append(header) .Append("=") - .Append(value); + // TODO: Perf - iterate the string values instead? + .Append(context.HttpContext.Request.Headers[header]); } } @@ -127,7 +125,7 @@ namespace Microsoft.AspNetCore.ResponseCaching if (varyRules.Params.Count == 1 && string.Equals(varyRules.Params[0], "*", StringComparison.Ordinal)) { // Vary by all available query params - foreach (var query in httpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase)) + foreach (var query in context.HttpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase)) { builder.Append(KeyDelimiter) .Append(query.Key.ToUpperInvariant()) @@ -139,18 +137,11 @@ namespace Microsoft.AspNetCore.ResponseCaching { foreach (var param in varyRules.Params) { - var value = httpContext.Request.Query[param]; - - // TODO: How to handle null/empty string? - if (StringValues.IsNullOrEmpty(value)) - { - value = "null"; - } - builder.Append(KeyDelimiter) .Append(param) .Append("=") - .Append(value); + // TODO: Perf - iterate the string values instead? + .Append(context.HttpContext.Request.Query[param]); } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheabilityValidator.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheabilityValidator.cs index 2669c32041..fa86e766e7 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheabilityValidator.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheabilityValidator.cs @@ -13,13 +13,11 @@ namespace Microsoft.AspNetCore.ResponseCaching { private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); - public virtual bool RequestIsCacheable(HttpContext httpContext) + public virtual bool IsRequestCacheable(ResponseCachingContext context) { - var state = httpContext.GetResponseCachingState(); - // Verify the method // TODO: RFC lists POST as a cacheable method when explicit freshness information is provided, but this is not widely implemented. Will revisit. - var request = httpContext.Request; + var request = context.HttpContext.Request; if (!string.Equals("GET", request.Method, StringComparison.OrdinalIgnoreCase) && !string.Equals("HEAD", request.Method, StringComparison.OrdinalIgnoreCase)) { @@ -37,7 +35,7 @@ namespace Microsoft.AspNetCore.ResponseCaching // TODO: no-cache requests can be retrieved upon validation with origin if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl])) { - if (state.RequestCacheControl.NoCache) + if (context.RequestCacheControlHeaderValue.NoCache) { return false; } @@ -59,31 +57,29 @@ namespace Microsoft.AspNetCore.ResponseCaching return true; } - public virtual bool ResponseIsCacheable(HttpContext httpContext) + public virtual bool IsResponseCacheable(ResponseCachingContext context) { - var state = httpContext.GetResponseCachingState(); - // Only cache pages explicitly marked with public // TODO: Consider caching responses that are not marked as public but otherwise cacheable? - if (!state.ResponseCacheControl.Public) + if (!context.ResponseCacheControlHeaderValue.Public) { return false; } // Check no-store - if (state.RequestCacheControl.NoStore || state.ResponseCacheControl.NoStore) + if (context.RequestCacheControlHeaderValue.NoStore || context.ResponseCacheControlHeaderValue.NoStore) { return false; } // Check no-cache // TODO: Handle no-cache with headers - if (state.ResponseCacheControl.NoCache) + if (context.ResponseCacheControlHeaderValue.NoCache) { return false; } - var response = httpContext.Response; + var response = context.HttpContext.Response; // Do not cache responses with Set-Cookie headers if (!StringValues.IsNullOrEmpty(response.Headers[HeaderNames.SetCookie])) @@ -101,7 +97,7 @@ namespace Microsoft.AspNetCore.ResponseCaching // TODO: public MAY override the cacheability checks for private and status codes // Check private - if (state.ResponseCacheControl.Private) + if (context.ResponseCacheControlHeaderValue.Private) { return false; } @@ -115,35 +111,35 @@ namespace Microsoft.AspNetCore.ResponseCaching // Check response freshness // TODO: apparent age vs corrected age value - if (state.ResponseHeaders.Date == null) + if (context.TypedResponseHeaders.Date == null) { - if (state.ResponseCacheControl.SharedMaxAge == null && - state.ResponseCacheControl.MaxAge == null && - state.ResponseTime > state.ResponseHeaders.Expires) + if (context.ResponseCacheControlHeaderValue.SharedMaxAge == null && + context.ResponseCacheControlHeaderValue.MaxAge == null && + context.ResponseTime > context.TypedResponseHeaders.Expires) { return false; } } else { - var age = state.ResponseTime - state.ResponseHeaders.Date.Value; + var age = context.ResponseTime - context.TypedResponseHeaders.Date.Value; // Validate shared max age - if (age > state.ResponseCacheControl.SharedMaxAge) + if (age > context.ResponseCacheControlHeaderValue.SharedMaxAge) { return false; } - else if (state.ResponseCacheControl.SharedMaxAge == null) + else if (context.ResponseCacheControlHeaderValue.SharedMaxAge == null) { // Validate max age - if (age > state.ResponseCacheControl.MaxAge) + if (age > context.ResponseCacheControlHeaderValue.MaxAge) { return false; } - else if (state.ResponseCacheControl.MaxAge == null) + else if (context.ResponseCacheControlHeaderValue.MaxAge == null) { // Validate expiration - if (state.ResponseTime > state.ResponseHeaders.Expires) + if (context.ResponseTime > context.TypedResponseHeaders.Expires) { return false; } @@ -154,16 +150,15 @@ namespace Microsoft.AspNetCore.ResponseCaching return true; } - public virtual bool CachedEntryIsFresh(HttpContext httpContext, ResponseHeaders cachedResponseHeaders) + public virtual bool IsCachedEntryFresh(ResponseCachingContext context) { - var state = httpContext.GetResponseCachingState(); - var age = state.CachedEntryAge; - var cachedControlHeaders = cachedResponseHeaders.CacheControl ?? EmptyCacheControl; + var age = context.CachedEntryAge; + var cachedControlHeaders = context.CachedResponseHeaders.CacheControl ?? EmptyCacheControl; // Add min-fresh requirements - if (state.RequestCacheControl.MinFresh != null) + if (context.RequestCacheControlHeaderValue.MinFresh != null) { - age += state.RequestCacheControl.MinFresh.Value; + age += context.RequestCacheControlHeaderValue.MinFresh.Value; } // Validate shared max age, this overrides any max age settings for shared caches @@ -175,7 +170,7 @@ namespace Microsoft.AspNetCore.ResponseCaching else if (cachedControlHeaders.SharedMaxAge == null) { // Validate max age - if (age > cachedControlHeaders.MaxAge || age > state.RequestCacheControl.MaxAge) + if (age > cachedControlHeaders.MaxAge || age > context.RequestCacheControlHeaderValue.MaxAge) { // Must revalidate if (cachedControlHeaders.MustRevalidate) @@ -184,7 +179,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } // Request allows stale values - if (age < state.RequestCacheControl.MaxStaleLimit) + if (age < context.RequestCacheControlHeaderValue.MaxStaleLimit) { // TODO: Add warning header indicating the response is stale return true; @@ -192,10 +187,10 @@ namespace Microsoft.AspNetCore.ResponseCaching return false; } - else if (cachedControlHeaders.MaxAge == null && state.RequestCacheControl.MaxAge == null) + else if (cachedControlHeaders.MaxAge == null && context.RequestCacheControlHeaderValue.MaxAge == null) { // Validate expiration - if (state.ResponseTime > cachedResponseHeaders.Expires) + if (context.ResponseTime > context.CachedResponseHeaders.Expires) { return false; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingHttpContextExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingHttpContextExtensions.cs index ae6481949c..50b5f73f52 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingHttpContextExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingHttpContextExtensions.cs @@ -2,18 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.ResponseCaching.Internal; namespace Microsoft.AspNetCore.ResponseCaching { // TODO: Temporary interface for endpoints to specify options for response caching public static class ResponseCachingHttpContextExtensions { - public static ResponseCachingState GetResponseCachingState(this HttpContext httpContext) - { - return httpContext.Features.Get(); - } - public static ResponseCachingFeature GetResponseCachingFeature(this HttpContext httpContext) { return httpContext.Features.Get(); diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingServiceCollectionExtensions.cs index 4877d34bdc..962ddf281f 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingServiceCollectionExtensions.cs @@ -40,7 +40,7 @@ namespace Microsoft.Extensions.DependencyInjection private static IServiceCollection AddResponseCachingServices(this IServiceCollection services) { - services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); services.TryAdd(ServiceDescriptor.Singleton()); return services; diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheKeyProvider.cs new file mode 100644 index 0000000000..570041e290 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheKeyProvider.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.ResponseCaching +{ + public interface ICacheKeyProvider + { + /// + /// Create a base key for storing items. + /// + /// The . + /// The created base key. + string CreateStorageBaseKey(ResponseCachingContext context); + + /// + /// Create one or more base keys for looking up items. + /// + /// The . + /// An ordered containing the base keys to try when looking up items. + IEnumerable CreateLookupBaseKeys(ResponseCachingContext context); + + /// + /// Create a vary key for storing items. + /// + /// The . + /// The created vary key. + string CreateStorageVaryKey(ResponseCachingContext context); + + /// + /// Create one or more vary keys for looking up items. + /// + /// The . + /// An ordered containing the vary keys to try when looking up items. + IEnumerable CreateLookupVaryKeys(ResponseCachingContext context); + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheabilityValidator.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheabilityValidator.cs index 01ded6818a..a260ce87d1 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheabilityValidator.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheabilityValidator.cs @@ -1,9 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Headers; - namespace Microsoft.AspNetCore.ResponseCaching { public interface ICacheabilityValidator @@ -11,23 +8,22 @@ namespace Microsoft.AspNetCore.ResponseCaching /// /// Determine the cacheability of an HTTP request. /// - /// The . + /// The . /// true if the request is cacheable; otherwise false. - bool RequestIsCacheable(HttpContext httpContext); + bool IsRequestCacheable(ResponseCachingContext context); /// /// Determine the cacheability of an HTTP response. /// - /// The . + /// The . /// true if the response is cacheable; otherwise false. - bool ResponseIsCacheable(HttpContext httpContext); + bool IsResponseCacheable(ResponseCachingContext context); /// /// Determine the freshness of the cached entry. /// - /// The . - /// The of the cached entry. + /// The . /// true if the cached entry is fresh; otherwise false. - bool CachedEntryIsFresh(HttpContext httpContext, ResponseHeaders cachedResponseHeaders); + bool IsCachedEntryFresh(ResponseCachingContext context); } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs deleted file mode 100644 index 2013b202b5..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Collections.Generic; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.ResponseCaching -{ - public interface IKeyProvider - { - /// - /// Create a base key using the HTTP context for storing items. - /// - /// The . - /// The created base key. - string CreateStorageBaseKey(HttpContext httpContext); - - /// - /// Create one or more base keys using the HTTP context for looking up items. - /// - /// The . - /// An ordered containing the base keys to try when looking up items. - IEnumerable CreateLookupBaseKey(HttpContext httpContext); - - /// - /// Create a vary key using the HTTP context and vary rules for storing items. - /// - /// The . - /// The . - /// The created vary key. - string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules); - - /// - /// Create one or more vary keys using the HTTP context and vary rules for looking up items. - /// - /// The . - /// The . - /// An ordered containing the vary keys to try when looking up items. - IEnumerable CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules); - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpContextInternalExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/InternalHttpContextExtensions.cs similarity index 51% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpContextInternalExtensions.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/InternalHttpContextExtensions.cs index 3c148ef340..640edba4a1 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpContextInternalExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/InternalHttpContextExtensions.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.ResponseCaching.Internal { - internal static class HttpContextInternalExtensions + internal static class InternalHttpContextExtensions { internal static void AddResponseCachingFeature(this HttpContext httpContext) { @@ -21,24 +21,5 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { httpContext.Features.Set(null); } - - internal static void AddResponseCachingState(this HttpContext httpContext) - { - if (httpContext.GetResponseCachingState() != null) - { - throw new InvalidOperationException($"Another instance of {nameof(ResponseCachingState)} already exists. Only one instance of {nameof(ResponseCachingMiddleware)} can be configured for an application."); - } - httpContext.Features.Set(new ResponseCachingState(httpContext)); - } - - internal static void RemoveResponseCachingState(this HttpContext httpContext) - { - httpContext.Features.Set(null); - } - - internal static ResponseCachingState GetResponseCachingState(this HttpContext httpContext) - { - return httpContext.Features.Get(); - } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs deleted file mode 100644 index 422f7c9746..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Headers; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.AspNetCore.ResponseCaching.Internal -{ - public class ResponseCachingState - { - private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); - - private readonly HttpContext _httpContext; - - private RequestHeaders _requestHeaders; - private ResponseHeaders _responseHeaders; - private CacheControlHeaderValue _requestCacheControl; - private CacheControlHeaderValue _responseCacheControl; - - internal ResponseCachingState(HttpContext httpContext) - { - _httpContext = httpContext; - } - - public bool ShouldCacheResponse { get; internal set; } - - public string StorageBaseKey { get; internal set; } - - public string StorageVaryKey { get; internal set; } - - public DateTimeOffset ResponseTime { get; internal set; } - - public TimeSpan CachedEntryAge { get; internal set; } - - public TimeSpan CachedResponseValidFor { get; internal set; } - - internal CachedResponse CachedResponse { get; set; } - - internal CachedVaryRules CachedVaryRules { get; set; } - - public RequestHeaders RequestHeaders - { - get - { - if (_requestHeaders == null) - { - _requestHeaders = _httpContext.Request.GetTypedHeaders(); - } - return _requestHeaders; - } - } - - public ResponseHeaders ResponseHeaders - { - get - { - if (_responseHeaders == null) - { - _responseHeaders = _httpContext.Response.GetTypedHeaders(); - } - return _responseHeaders; - } - } - - public CacheControlHeaderValue RequestCacheControl - { - get - { - if (_requestCacheControl == null) - { - _requestCacheControl = RequestHeaders.CacheControl ?? EmptyCacheControl; - } - return _requestCacheControl; - } - } - - public CacheControlHeaderValue ResponseCacheControl - { - get - { - if (_responseCacheControl == null) - { - _responseCacheControl = ResponseHeaders.CacheControl ?? EmptyCacheControl; - } - return _responseCacheControl; - } - } - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index 55dc1824ca..bea436517e 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -3,356 +3,102 @@ using System; using System.IO; -using System.Globalization; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; -using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.ResponseCaching { - internal class ResponseCachingContext + public class ResponseCachingContext { - private readonly HttpContext _httpContext; - private readonly IResponseCache _cache; - private readonly ResponseCachingOptions _options; - private readonly ICacheabilityValidator _cacheabilityValidator; - private readonly IKeyProvider _keyProvider; + private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); - private ResponseCachingState _state; + private RequestHeaders _requestHeaders; + private ResponseHeaders _responseHeaders; + private CacheControlHeaderValue _requestCacheControl; + private CacheControlHeaderValue _responseCacheControl; internal ResponseCachingContext( - HttpContext httpContext, - IResponseCache cache, - ResponseCachingOptions options, - ICacheabilityValidator cacheabilityValidator, - IKeyProvider keyProvider) + HttpContext httpContext) { - _httpContext = httpContext; - _cache = cache; - _options = options; - _cacheabilityValidator = cacheabilityValidator; - _keyProvider = keyProvider; + HttpContext = httpContext; } - internal ResponseCachingState State - { - get - { - if (_state == null) - { - _state = _httpContext.GetResponseCachingState(); - } - return _state; - } - } + public HttpContext HttpContext { get; } + + public bool ShouldCacheResponse { get; internal set; } + + public string StorageBaseKey { get; internal set; } + + public string StorageVaryKey { get; internal set; } + + public DateTimeOffset ResponseTime { get; internal set; } + + public TimeSpan CachedEntryAge { get; internal set; } + + public TimeSpan CachedResponseValidFor { get; internal set; } + + public CachedResponse CachedResponse { get; internal set; } + + public CachedVaryRules CachedVaryRules { get; internal set; } internal bool ResponseStarted { get; set; } - private Stream OriginalResponseStream { get; set; } + internal Stream OriginalResponseStream { get; set; } - private ResponseCacheStream ResponseCacheStream { get; set; } + internal ResponseCacheStream ResponseCacheStream { get; set; } - private IHttpSendFileFeature OriginalSendFileFeature { get; set; } + internal IHttpSendFileFeature OriginalSendFileFeature { get; set; } - internal async Task TryServeCachedResponseAsync(CachedResponse cachedResponse) + internal ResponseHeaders CachedResponseHeaders { get; set; } + + internal RequestHeaders TypedRequestHeaders { - State.CachedResponse = cachedResponse; - var cachedResponseHeaders = new ResponseHeaders(State.CachedResponse.Headers); - - State.ResponseTime = _options.SystemClock.UtcNow; - var cachedEntryAge = State.ResponseTime - State.CachedResponse.Created; - State.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero; - - if (_cacheabilityValidator.CachedEntryIsFresh(_httpContext, cachedResponseHeaders)) + get { - // Check conditional request rules - if (ConditionalRequestSatisfied(cachedResponseHeaders)) + if (_requestHeaders == null) { - _httpContext.Response.StatusCode = StatusCodes.Status304NotModified; + _requestHeaders = HttpContext.Request.GetTypedHeaders(); } - else - { - var response = _httpContext.Response; - // Copy the cached status code and response headers - response.StatusCode = State.CachedResponse.StatusCode; - foreach (var header in State.CachedResponse.Headers) - { - response.Headers.Add(header); - } - - response.Headers[HeaderNames.Age] = State.CachedEntryAge.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture); - - var body = State.CachedResponse.Body ?? - ((CachedResponseBody)_cache.Get(State.CachedResponse.BodyKeyPrefix))?.Body; - - // If the body is not found, something went wrong. - if (body == null) - { - return false; - } - - // Copy the cached response body - if (body.Length > 0) - { - // Add a content-length if required - if (response.ContentLength == null && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) - { - response.ContentLength = body.Length; - } - await response.Body.WriteAsync(body, 0, body.Length); - } - } - - return true; - } - else - { - // TODO: Validate with endpoint instead - } - - return false; - } - - internal async Task TryServeFromCacheAsync() - { - foreach (var baseKey in _keyProvider.CreateLookupBaseKey(_httpContext)) - { - var cacheEntry = _cache.Get(baseKey); - - if (cacheEntry is CachedVaryRules) - { - // Request contains vary rules, recompute key(s) and try again - State.CachedVaryRules = cacheEntry as CachedVaryRules; - - foreach (var varyKey in _keyProvider.CreateLookupVaryKey(_httpContext, State.CachedVaryRules.VaryRules)) - { - cacheEntry = _cache.Get(varyKey); - - if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(cacheEntry as CachedResponse)) - { - return true; - } - } - } - - if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(cacheEntry as CachedResponse)) - { - return true; - } - } - - - if (State.RequestCacheControl.OnlyIfCached) - { - _httpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; - return true; - } - - return false; - } - - internal bool ConditionalRequestSatisfied(ResponseHeaders cachedResponseHeaders) - { - var ifNoneMatchHeader = State.RequestHeaders.IfNoneMatch; - - if (ifNoneMatchHeader != null) - { - if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any)) - { - return true; - } - - if (cachedResponseHeaders.ETag != null) - { - foreach (var tag in ifNoneMatchHeader) - { - if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: true)) - { - return true; - } - } - } - } - else if ((cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= State.RequestHeaders.IfUnmodifiedSince) - { - return true; - } - - return false; - } - - internal void FinalizeCachingHeaders() - { - if (_cacheabilityValidator.ResponseIsCacheable(_httpContext)) - { - State.ShouldCacheResponse = true; - State.StorageBaseKey = _keyProvider.CreateStorageBaseKey(_httpContext); - - // Create the cache entry now - var response = _httpContext.Response; - var varyHeaderValue = response.Headers[HeaderNames.Vary]; - var varyParamsValue = _httpContext.GetResponseCachingFeature()?.VaryParams ?? StringValues.Empty; - State.CachedResponseValidFor = State.ResponseCacheControl.SharedMaxAge - ?? State.ResponseCacheControl.MaxAge - ?? (State.ResponseHeaders.Expires - State.ResponseTime) - // TODO: Heuristics for expiration? - ?? TimeSpan.FromSeconds(10); - - // Check if any vary rules exist - if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue)) - { - // Normalize order and casing of vary by rules - var normalizedVaryHeaderValue = GetNormalizedStringValues(varyHeaderValue); - var normalizedVaryParamsValue = GetNormalizedStringValues(varyParamsValue); - - // Update vary rules if they are different - if (State.CachedVaryRules == null || - !StringValues.Equals(State.CachedVaryRules.VaryRules.Params, normalizedVaryParamsValue) || - !StringValues.Equals(State.CachedVaryRules.VaryRules.Headers, normalizedVaryHeaderValue)) - { - var cachedVaryRules = new CachedVaryRules - { - VaryKeyPrefix = FastGuid.NewGuid().IdString, - VaryRules = new VaryRules() - { - // TODO: Vary Encoding - Headers = normalizedVaryHeaderValue, - Params = normalizedVaryParamsValue - } - }; - - State.CachedVaryRules = cachedVaryRules; - _cache.Set(State.StorageBaseKey, cachedVaryRules, State.CachedResponseValidFor); - } - - State.StorageVaryKey = _keyProvider.CreateStorageVaryKey(_httpContext, State.CachedVaryRules.VaryRules); - } - - // Ensure date header is set - if (State.ResponseHeaders.Date == null) - { - State.ResponseHeaders.Date = State.ResponseTime; - } - - // Store the response on the state - State.CachedResponse = new CachedResponse - { - BodyKeyPrefix = FastGuid.NewGuid().IdString, - Created = State.ResponseHeaders.Date.Value, - StatusCode = _httpContext.Response.StatusCode - }; - - foreach (var header in State.ResponseHeaders.Headers) - { - if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase)) - { - State.CachedResponse.Headers.Add(header); - } - } - } - else - { - ResponseCacheStream.DisableBuffering(); + return _requestHeaders; } } - internal void FinalizeCachingBody() + internal ResponseHeaders TypedResponseHeaders { - if (State.ShouldCacheResponse && - ResponseCacheStream.BufferingEnabled && - (State.ResponseHeaders.ContentLength == null || - State.ResponseHeaders.ContentLength == ResponseCacheStream.BufferedStream.Length)) + get { - if (ResponseCacheStream.BufferedStream.Length >= _options.MinimumSplitBodySize) + if (_responseHeaders == null) { - // Store response and response body separately - _cache.Set(State.StorageVaryKey ?? State.StorageBaseKey, State.CachedResponse, State.CachedResponseValidFor); - - var cachedResponseBody = new CachedResponseBody() - { - Body = ResponseCacheStream.BufferedStream.ToArray() - }; - - _cache.Set(State.CachedResponse.BodyKeyPrefix, cachedResponseBody, State.CachedResponseValidFor); + _responseHeaders = HttpContext.Response.GetTypedHeaders(); } - else + return _responseHeaders; + } + } + + internal CacheControlHeaderValue RequestCacheControlHeaderValue + { + get + { + if (_requestCacheControl == null) { - // Store response and response body together - State.CachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray(); - _cache.Set(State.StorageVaryKey ?? State.StorageBaseKey, State.CachedResponse, State.CachedResponseValidFor); + _requestCacheControl = TypedRequestHeaders.CacheControl ?? EmptyCacheControl; } + return _requestCacheControl; } } - internal void OnResponseStarting() + internal CacheControlHeaderValue ResponseCacheControlHeaderValue { - if (!ResponseStarted) + get { - ResponseStarted = true; - State.ResponseTime = _options.SystemClock.UtcNow; - - FinalizeCachingHeaders(); - } - } - - internal void ShimResponseStream() - { - // TODO: Consider caching large responses on disk and serving them from there. - - // Shim response stream - OriginalResponseStream = _httpContext.Response.Body; - ResponseCacheStream = new ResponseCacheStream(OriginalResponseStream, _options.MaximumCachedBodySize); - _httpContext.Response.Body = ResponseCacheStream; - - // Shim IHttpSendFileFeature - OriginalSendFileFeature = _httpContext.Features.Get(); - if (OriginalSendFileFeature != null) - { - _httpContext.Features.Set(new SendFileFeatureWrapper(OriginalSendFileFeature, ResponseCacheStream)); - } - - // TODO: Move this temporary interface with endpoint to HttpAbstractions - _httpContext.AddResponseCachingFeature(); - } - - internal void UnshimResponseStream() - { - // Unshim response stream - _httpContext.Response.Body = OriginalResponseStream; - - // Unshim IHttpSendFileFeature - _httpContext.Features.Set(OriginalSendFileFeature); - - // TODO: Move this temporary interface with endpoint to HttpAbstractions - _httpContext.RemoveResponseCachingFeature(); - } - - // Normalize order and casing - internal static StringValues GetNormalizedStringValues(StringValues stringVales) - { - if (stringVales.Count == 1) - { - return new StringValues(stringVales.ToString().ToUpperInvariant()); - } - else - { - var originalArray = stringVales.ToArray(); - var newArray = new string[originalArray.Length]; - - for (int i = 0; i < originalArray.Length; i++) + if (_responseCacheControl == null) { - newArray[i] = originalArray[i].ToUpperInvariant(); + _responseCacheControl = TypedResponseHeaders.CacheControl ?? EmptyCacheControl; } - - // Since the casing has already been normalized, use Ordinal comparison - Array.Sort(newArray, StringComparer.Ordinal); - - return new StringValues(newArray); + return _responseCacheControl; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index f5b62fd2ae..8e0d6c910f 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -2,35 +2,37 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.ResponseCaching { public class ResponseCachingMiddleware { - private static readonly Func OnStartingCallback = state => - { - ((ResponseCachingContext)state).OnResponseStarting(); - return TaskCache.CompletedTask; - }; + private static readonly TimeSpan DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10); private readonly RequestDelegate _next; private readonly IResponseCache _cache; private readonly ResponseCachingOptions _options; private readonly ICacheabilityValidator _cacheabilityValidator; - private readonly IKeyProvider _keyProvider; + private readonly ICacheKeyProvider _cacheKeyProvider; + private readonly Func _onStartingCallback; public ResponseCachingMiddleware( RequestDelegate next, IResponseCache cache, IOptions options, ICacheabilityValidator cacheabilityValidator, - IKeyProvider keyProvider) + ICacheKeyProvider cacheKeyProvider) { if (next == null) { @@ -48,70 +50,352 @@ namespace Microsoft.AspNetCore.ResponseCaching { throw new ArgumentNullException(nameof(cacheabilityValidator)); } - if (keyProvider == null) + if (cacheKeyProvider == null) { - throw new ArgumentNullException(nameof(keyProvider)); + throw new ArgumentNullException(nameof(cacheKeyProvider)); } _next = next; _cache = cache; _options = options.Value; _cacheabilityValidator = cacheabilityValidator; - _keyProvider = keyProvider; + _cacheKeyProvider = cacheKeyProvider; + _onStartingCallback = state => + { + OnResponseStarting((ResponseCachingContext)state); + return TaskCache.CompletedTask; + }; } - public async Task Invoke(HttpContext context) + public async Task Invoke(HttpContext httpContext) { - context.AddResponseCachingState(); + var context = new ResponseCachingContext(httpContext); - try + // Should we attempt any caching logic? + if (_cacheabilityValidator.IsRequestCacheable(context)) { - var cachingContext = new ResponseCachingContext( - context, - _cache, - _options, - _cacheabilityValidator, - _keyProvider); - - // Should we attempt any caching logic? - if (_cacheabilityValidator.RequestIsCacheable(context)) + // Can this request be served from cache? + if (await TryServeFromCacheAsync(context)) { - // Can this request be served from cache? - if (await cachingContext.TryServeFromCacheAsync()) - { - return; - } + return; + } - // Hook up to listen to the response stream - cachingContext.ShimResponseStream(); + // Hook up to listen to the response stream + ShimResponseStream(context); - try - { - // Subscribe to OnStarting event - context.Response.OnStarting(OnStartingCallback, cachingContext); + try + { + // Subscribe to OnStarting event + httpContext.Response.OnStarting(_onStartingCallback, context); - await _next(context); + await _next(httpContext); - // If there was no response body, check the response headers now. We can cache things like redirects. - cachingContext.OnResponseStarting(); + // If there was no response body, check the response headers now. We can cache things like redirects. + OnResponseStarting(context); - // Finalize the cache entry - cachingContext.FinalizeCachingBody(); - } - finally - { - cachingContext.UnshimResponseStream(); - } + // Finalize the cache entry + FinalizeCachingBody(context); + } + finally + { + UnshimResponseStream(context); + } + } + else + { + // TODO: Invalidate resources for successful unsafe methods? Required by RFC + await _next(httpContext); + } + } + + internal async Task TryServeCachedResponseAsync(ResponseCachingContext context, CachedResponse cachedResponse) + { + context.CachedResponse = cachedResponse; + context.CachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers); + context.ResponseTime = _options.SystemClock.UtcNow; + var cachedEntryAge = context.ResponseTime - context.CachedResponse.Created; + context.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero; + + if (_cacheabilityValidator.IsCachedEntryFresh(context)) + { + // Check conditional request rules + if (ConditionalRequestSatisfied(context)) + { + context.HttpContext.Response.StatusCode = StatusCodes.Status304NotModified; } else { - // TODO: Invalidate resources for successful unsafe methods? Required by RFC - await _next(context); + 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.Add(header); + } + + response.Headers[HeaderNames.Age] = context.CachedEntryAge.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture); + + var body = context.CachedResponse.Body ?? + ((CachedResponseBody)_cache.Get(context.CachedResponse.BodyKeyPrefix))?.Body; + + // If the body is not found, something went wrong. + if (body == null) + { + return false; + } + + // Copy the cached response body + if (body.Length > 0) + { + // Add a content-length if required + if (response.ContentLength == null && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) + { + response.ContentLength = body.Length; + } + await response.Body.WriteAsync(body, 0, body.Length); + } + } + + return true; + } + else + { + // TODO: Validate with endpoint instead + } + + return false; + } + + internal async Task TryServeFromCacheAsync(ResponseCachingContext context) + { + foreach (var baseKey in _cacheKeyProvider.CreateLookupBaseKeys(context)) + { + var cacheEntry = _cache.Get(baseKey); + + if (cacheEntry is CachedVaryRules) + { + // Request contains vary rules, recompute key(s) and try again + context.CachedVaryRules = (CachedVaryRules)cacheEntry; + + foreach (var varyKey in _cacheKeyProvider.CreateLookupVaryKeys(context)) + { + cacheEntry = _cache.Get(varyKey); + + if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(context, (CachedResponse)cacheEntry)) + { + return true; + } + } + } + + if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(context, (CachedResponse)cacheEntry)) + { + return true; } } - finally + + + if (context.RequestCacheControlHeaderValue.OnlyIfCached) { - context.RemoveResponseCachingState(); + context.HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; + return true; + } + + return false; + } + + internal void FinalizeCachingHeaders(ResponseCachingContext context) + { + if (_cacheabilityValidator.IsResponseCacheable(context)) + { + context.ShouldCacheResponse = true; + context.StorageBaseKey = _cacheKeyProvider.CreateStorageBaseKey(context); + + // Create the cache entry now + var response = context.HttpContext.Response; + var varyHeaderValue = response.Headers[HeaderNames.Vary]; + var varyParamsValue = context.HttpContext.GetResponseCachingFeature()?.VaryParams ?? StringValues.Empty; + context.CachedResponseValidFor = context.ResponseCacheControlHeaderValue.SharedMaxAge ?? + context.ResponseCacheControlHeaderValue.MaxAge ?? + (context.TypedResponseHeaders.Expires - context.ResponseTime) ?? + DefaultExpirationTimeSpan; + + // Check if any vary rules exist + if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue)) + { + // Normalize order and casing of vary by rules + var normalizedVaryHeaderValue = GetNormalizedStringValues(varyHeaderValue); + var normalizedVaryParamsValue = GetNormalizedStringValues(varyParamsValue); + + // Update vary rules if they are different + if (context.CachedVaryRules == null || + !StringValues.Equals(context.CachedVaryRules.Params, normalizedVaryParamsValue) || + !StringValues.Equals(context.CachedVaryRules.Headers, normalizedVaryHeaderValue)) + { + context.CachedVaryRules = new CachedVaryRules + { + VaryKeyPrefix = FastGuid.NewGuid().IdString, + Headers = normalizedVaryHeaderValue, + Params = normalizedVaryParamsValue + }; + + _cache.Set(context.StorageBaseKey, context.CachedVaryRules, context.CachedResponseValidFor); + } + + context.StorageVaryKey = _cacheKeyProvider.CreateStorageVaryKey(context); + } + + // Ensure date header is set + if (context.TypedResponseHeaders.Date == null) + { + context.TypedResponseHeaders.Date = context.ResponseTime; + } + + // Store the response on the state + context.CachedResponse = new CachedResponse + { + BodyKeyPrefix = FastGuid.NewGuid().IdString, + Created = context.TypedResponseHeaders.Date.Value, + StatusCode = context.HttpContext.Response.StatusCode + }; + + foreach (var header in context.TypedResponseHeaders.Headers) + { + if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase)) + { + context.CachedResponse.Headers.Add(header); + } + } + } + else + { + context.ResponseCacheStream.DisableBuffering(); + } + } + + internal void FinalizeCachingBody(ResponseCachingContext context) + { + if (context.ShouldCacheResponse && + context.ResponseCacheStream.BufferingEnabled && + (context.TypedResponseHeaders.ContentLength == null || + context.TypedResponseHeaders.ContentLength == context.ResponseCacheStream.BufferedStream.Length)) + { + if (context.ResponseCacheStream.BufferedStream.Length >= _options.MinimumSplitBodySize) + { + // Store response and response body separately + _cache.Set(context.StorageVaryKey ?? context.StorageBaseKey, context.CachedResponse, context.CachedResponseValidFor); + + var cachedResponseBody = new CachedResponseBody() + { + Body = context.ResponseCacheStream.BufferedStream.ToArray() + }; + + _cache.Set(context.CachedResponse.BodyKeyPrefix, cachedResponseBody, context.CachedResponseValidFor); + } + else + { + // Store response and response body together + context.CachedResponse.Body = context.ResponseCacheStream.BufferedStream.ToArray(); + _cache.Set(context.StorageVaryKey ?? context.StorageBaseKey, context.CachedResponse, context.CachedResponseValidFor); + } + } + } + + internal void OnResponseStarting(ResponseCachingContext context) + { + if (!context.ResponseStarted) + { + context.ResponseStarted = true; + context.ResponseTime = _options.SystemClock.UtcNow; + + FinalizeCachingHeaders(context); + } + } + + internal void ShimResponseStream(ResponseCachingContext context) + { + // TODO: Consider caching large responses on disk and serving them from there. + + // Shim response stream + context.OriginalResponseStream = context.HttpContext.Response.Body; + context.ResponseCacheStream = new ResponseCacheStream(context.OriginalResponseStream, _options.MaximumCachedBodySize); + context.HttpContext.Response.Body = context.ResponseCacheStream; + + // Shim IHttpSendFileFeature + context.OriginalSendFileFeature = context.HttpContext.Features.Get(); + if (context.OriginalSendFileFeature != null) + { + context.HttpContext.Features.Set(new SendFileFeatureWrapper(context.OriginalSendFileFeature, context.ResponseCacheStream)); + } + + // TODO: Move this temporary interface with endpoint to HttpAbstractions + context.HttpContext.AddResponseCachingFeature(); + } + + internal static void UnshimResponseStream(ResponseCachingContext context) + { + // Unshim response stream + context.HttpContext.Response.Body = context.OriginalResponseStream; + + // Unshim IHttpSendFileFeature + context.HttpContext.Features.Set(context.OriginalSendFileFeature); + + // TODO: Move this temporary interface with endpoint to HttpAbstractions + context.HttpContext.RemoveResponseCachingFeature(); + } + + internal static bool ConditionalRequestSatisfied(ResponseCachingContext context) + { + var cachedResponseHeaders = context.CachedResponseHeaders; + var ifNoneMatchHeader = context.TypedRequestHeaders.IfNoneMatch; + + if (ifNoneMatchHeader != null) + { + if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any)) + { + return true; + } + + if (cachedResponseHeaders.ETag != null) + { + foreach (var tag in ifNoneMatchHeader) + { + if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: true)) + { + return true; + } + } + } + } + else if ((cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= context.TypedRequestHeaders.IfUnmodifiedSince) + { + return true; + } + + return false; + } + + // Normalize order and casing + internal static StringValues GetNormalizedStringValues(StringValues stringValues) + { + if (stringValues.Count == 1) + { + return new StringValues(stringValues.ToString().ToUpperInvariant()); + } + else + { + var originalArray = stringValues.ToArray(); + var newArray = new string[originalArray.Length]; + + for (int i = 0; i < originalArray.Length; i++) + { + newArray[i] = originalArray[i].ToUpperInvariant(); + } + + // Since the casing has already been normalized, use Ordinal comparison + Array.Sort(newArray, StringComparer.Ordinal); + + return new StringValues(newArray); } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/VaryRules.cs b/src/Microsoft.AspNetCore.ResponseCaching/VaryRules.cs deleted file mode 100644 index 46d9419528..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/VaryRules.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNetCore.ResponseCaching -{ - public class VaryRules - { - internal StringValues Headers { get; set; } - internal StringValues Params { get; set; } - } -} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs index bd2ad03bb1..a2b1269855 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs @@ -81,8 +81,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var cachedVaryRule = new CachedVaryRules() { - VaryKeyPrefix = FastGuid.NewGuid().IdString, - VaryRules = new VaryRules() + VaryKeyPrefix = FastGuid.NewGuid().IdString }; AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); @@ -95,10 +94,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var cachedVaryRule = new CachedVaryRules() { VaryKeyPrefix = FastGuid.NewGuid().IdString, - VaryRules = new VaryRules() - { - Headers = headers - } + Headers = headers }; AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); @@ -111,10 +107,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var cachedVaryRule = new CachedVaryRules() { VaryKeyPrefix = FastGuid.NewGuid().IdString, - VaryRules = new VaryRules() - { - Params = param - } + Params = param }; AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); @@ -128,11 +121,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var cachedVaryRule = new CachedVaryRules() { VaryKeyPrefix = FastGuid.NewGuid().IdString, - VaryRules = new VaryRules() - { - Headers = headers, - Params = param - } + Headers = headers, + Params = param }; AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); @@ -145,10 +135,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var cachedVaryRule = new CachedVaryRules() { VaryKeyPrefix = FastGuid.NewGuid().IdString, - VaryRules = new VaryRules() - { - Headers = headers - } + Headers = headers }; var serializedEntry = CacheEntrySerializer.Serialize(cachedVaryRule); Array.Reverse(serializedEntry); @@ -188,8 +175,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.NotNull(actual); Assert.NotNull(expected); Assert.Equal(expected.VaryKeyPrefix, actual.VaryKeyPrefix); - Assert.Equal(expected.VaryRules.Headers, actual.VaryRules.Headers); - Assert.Equal(expected.VaryRules.Params, actual.VaryRules.Params); + Assert.Equal(expected.Headers, actual.Headers); + Assert.Equal(expected.Params, actual.Params); } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheabilityValidatorTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheabilityValidatorTests.cs index e3cd724195..88be66b80a 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheabilityValidatorTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheabilityValidatorTests.cs @@ -4,7 +4,6 @@ using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Headers; -using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Net.Http.Headers; using Xunit; @@ -17,10 +16,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [InlineData("HEAD")] public void RequestIsCacheable_CacheableMethods_Allowed(string method) { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = method; + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = method; - Assert.True(new CacheabilityValidator().RequestIsCacheable(httpContext)); + Assert.True(new CacheabilityValidator().IsRequestCacheable(context)); } [Theory] @@ -34,182 +33,182 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [InlineData(null)] public void RequestIsCacheable_UncacheableMethods_NotAllowed(string method) { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = method; + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = method; - Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsRequestCacheable(context)); } [Fact] public void RequestIsCacheable_AuthorizationHeaders_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW"; + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = "GET"; + context.HttpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW"; - Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsRequestCacheable(context)); } [Fact] public void RequestIsCacheable_NoCache_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = "GET"; + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { NoCache = true }; - Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsRequestCacheable(context)); } [Fact] public void RequestIsCacheable_NoStore_Allowed() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = "GET"; + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { NoStore = true }; - Assert.True(new CacheabilityValidator().RequestIsCacheable(httpContext)); + Assert.True(new CacheabilityValidator().IsRequestCacheable(context)); } [Fact] public void RequestIsCacheable_LegacyDirectives_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = "GET"; + context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; - Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsRequestCacheable(context)); } [Fact] public void RequestIsCacheable_LegacyDirectives_OverridenByCacheControl() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; - httpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10"; + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = "GET"; + context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10"; - Assert.True(new CacheabilityValidator().RequestIsCacheable(httpContext)); + Assert.True(new CacheabilityValidator().IsRequestCacheable(context)); } [Fact] public void ResponseIsCacheable_NoPublic_NotAllowed() { - var httpContext = CreateDefaultContext(); + var context = TestUtils.CreateTestContext(); - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_Public_Allowed() { - var httpContext = CreateDefaultContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true }; - Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.True(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_NoCache_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true, NoCache = true }; - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_RequestNoStore_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { NoStore = true }; - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true }; - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_ResponseNoStore_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true, NoStore = true }; - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_SetCookieHeader_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true }; - httpContext.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; + context.HttpContext.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_VaryHeaderByStar_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true }; - httpContext.Response.Headers[HeaderNames.Vary] = "*"; + context.HttpContext.Response.Headers[HeaderNames.Vary] = "*"; - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_Private_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true, Private = true }; - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Theory] [InlineData(StatusCodes.Status200OK)] public void ResponseIsCacheable_SuccessStatusCodes_Allowed(int statusCode) { - var httpContext = CreateDefaultContext(); - httpContext.Response.StatusCode = statusCode; - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.StatusCode = statusCode; + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true }; - Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.True(new CacheabilityValidator().IsResponseCacheable(context)); } [Theory] @@ -263,146 +262,141 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [InlineData(StatusCodes.Status507InsufficientStorage)] public void ResponseIsCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode) { - var httpContext = CreateDefaultContext(); - httpContext.Response.StatusCode = statusCode; - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.StatusCode = statusCode; + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true }; - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_NoExpiryRequirements_IsAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Response.StatusCode = StatusCodes.Status200OK; - var headers = httpContext.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true }; var utcNow = DateTimeOffset.UtcNow; - headers.Date = utcNow; - httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; + context.TypedResponseHeaders.Date = utcNow; + context.ResponseTime = DateTimeOffset.MaxValue; - Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.True(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_PastExpiry_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Response.StatusCode = StatusCodes.Status200OK; - var headers = httpContext.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true }; var utcNow = DateTimeOffset.UtcNow; - headers.Expires = utcNow; + context.TypedResponseHeaders.Expires = utcNow; - headers.Date = utcNow; - httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; + context.TypedResponseHeaders.Date = utcNow; + context.ResponseTime = DateTimeOffset.MaxValue; - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_MaxAgeOverridesExpiry_ToAllowed() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - httpContext.Response.StatusCode = StatusCodes.Status200OK; - var headers = httpContext.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10) }; - headers.Expires = utcNow; - headers.Date = utcNow; - httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(9); + context.TypedResponseHeaders.Expires = utcNow; + context.TypedResponseHeaders.Date = utcNow; + context.ResponseTime = utcNow + TimeSpan.FromSeconds(9); - Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.True(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_MaxAgeOverridesExpiry_ToNotAllowed() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - httpContext.Response.StatusCode = StatusCodes.Status200OK; - var headers = httpContext.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10) }; - headers.Expires = utcNow; - headers.Date = utcNow; - httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(11); + context.TypedResponseHeaders.Expires = utcNow; + context.TypedResponseHeaders.Date = utcNow; + context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - httpContext.Response.StatusCode = StatusCodes.Status200OK; - var headers = httpContext.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10), SharedMaxAge = TimeSpan.FromSeconds(15) }; - headers.Date = utcNow; - httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(11); + context.TypedResponseHeaders.Date = utcNow; + context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); - Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.True(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_SharedMaxAgeOverridesMaxAge_ToNotFresh() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - httpContext.Response.StatusCode = StatusCodes.Status200OK; - var headers = httpContext.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10), SharedMaxAge = TimeSpan.FromSeconds(5) }; - headers.Date = utcNow; - httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(6); + context.TypedResponseHeaders.Date = utcNow; + context.ResponseTime = utcNow + TimeSpan.FromSeconds(6); - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void EntryIsFresh_NoCachedCacheControl_FallsbackToEmptyCacheControl() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; + var context = TestUtils.CreateTestContext(); + context.ResponseTime = DateTimeOffset.MaxValue; + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); - Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, new ResponseHeaders(new HeaderDictionary()))); + Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_NoExpiryRequirements_IsFresh() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + var context = TestUtils.CreateTestContext(); + context.ResponseTime = DateTimeOffset.MaxValue; + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { @@ -410,15 +404,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } }; - Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_PastExpiry_IsNotFresh() { - var httpContext = CreateDefaultContext(); - httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + var context = TestUtils.CreateTestContext(); + context.ResponseTime = DateTimeOffset.MaxValue; + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { @@ -427,18 +421,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = DateTimeOffset.UtcNow }; - Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_MaxAgeOverridesExpiry_ToFresh() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - var state = httpContext.GetResponseCachingState(); - state.CachedEntryAge = TimeSpan.FromSeconds(9); - state.ResponseTime = utcNow + state.CachedEntryAge; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + var context = TestUtils.CreateTestContext(); + context.CachedEntryAge = TimeSpan.FromSeconds(9); + context.ResponseTime = utcNow + context.CachedEntryAge; + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { @@ -448,18 +441,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = utcNow }; - Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_MaxAgeOverridesExpiry_ToNotFresh() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - var state = httpContext.GetResponseCachingState(); - state.CachedEntryAge = TimeSpan.FromSeconds(11); - state.ResponseTime = utcNow + state.CachedEntryAge; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + var context = TestUtils.CreateTestContext(); + context.CachedEntryAge = TimeSpan.FromSeconds(11); + context.ResponseTime = utcNow + context.CachedEntryAge; + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { @@ -469,18 +461,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = utcNow }; - Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToFresh() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - var state = httpContext.GetResponseCachingState(); - state.CachedEntryAge = TimeSpan.FromSeconds(11); - state.ResponseTime = utcNow + state.CachedEntryAge; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + var context = TestUtils.CreateTestContext(); + context.CachedEntryAge = TimeSpan.FromSeconds(11); + context.ResponseTime = utcNow + context.CachedEntryAge; + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { @@ -491,18 +482,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = utcNow }; - Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - var state = httpContext.GetResponseCachingState(); - state.CachedEntryAge = TimeSpan.FromSeconds(6); - state.ResponseTime = utcNow + state.CachedEntryAge; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + var context = TestUtils.CreateTestContext(); + context.CachedEntryAge = TimeSpan.FromSeconds(6); + context.ResponseTime = utcNow + context.CachedEntryAge; + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { @@ -513,18 +503,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = utcNow }; - Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_MinFreshReducesFreshness_ToNotFresh() { - var httpContext = CreateDefaultContext(); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { MinFresh = TimeSpan.FromSeconds(3) }; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { @@ -532,64 +522,64 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests SharedMaxAge = TimeSpan.FromSeconds(5) } }; - httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(3); + context.CachedEntryAge = TimeSpan.FromSeconds(3); - Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_RequestMaxAgeRestrictAge_ToNotFresh() { - var httpContext = CreateDefaultContext(); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5) }; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(10), } }; - httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(6); + context.CachedEntryAge = TimeSpan.FromSeconds(6); - Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_MaxStaleOverridesFreshness_ToFresh() { - var httpContext = CreateDefaultContext(); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5), MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit MaxStaleLimit = TimeSpan.FromSeconds(10) }; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5), } }; - httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(6); + context.CachedEntryAge = TimeSpan.FromSeconds(6); - Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh() { - var httpContext = CreateDefaultContext(); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5), MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit MaxStaleLimit = TimeSpan.FromSeconds(10) }; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { @@ -597,21 +587,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests MustRevalidate = true } }; - httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(6); + context.CachedEntryAge = TimeSpan.FromSeconds(6); - Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_IgnoresRequestVerificationWhenSpecified() { - var httpContext = CreateDefaultContext(); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { MinFresh = TimeSpan.FromSeconds(1), MaxAge = TimeSpan.FromSeconds(3) }; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { @@ -619,16 +609,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests SharedMaxAge = TimeSpan.FromSeconds(5) } }; - httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(3); + context.CachedEntryAge = TimeSpan.FromSeconds(3); - Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); - } - - private static HttpContext CreateDefaultContext() - { - var context = new DefaultHttpContext(); - context.AddResponseCachingState(); - return context; + Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context)); } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs index 74e15534bf..5bf1cbf5b7 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs @@ -21,17 +21,5 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests // Should throw Assert.ThrowsAny(() => httpContext.AddResponseCachingFeature()); } - - [Fact] - public void AddingSecondResponseCachingState_Throws() - { - var httpContext = new DefaultHttpContext(); - - // Should not throw - httpContext.AddResponseCachingState(); - - // Should throw - Assert.ThrowsAny(() => httpContext.AddResponseCachingState()); - } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs index 98e1450b9b..1239b2c901 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs @@ -1,11 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.ResponseCaching.Internal; -using Microsoft.Extensions.ObjectPool; -using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.AspNetCore.ResponseCaching.Tests @@ -13,156 +12,155 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public class DefaultKeyProviderTests { private static readonly char KeyDelimiter = '\x1e'; - private static readonly CachedVaryRules TestVaryRules = new CachedVaryRules() - { - VaryKeyPrefix = FastGuid.NewGuid().IdString - }; [Fact] public void DefaultKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodAndPath() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = "head"; - httpContext.Request.Path = "/path/subpath"; - httpContext.Request.Scheme = "https"; - httpContext.Request.Host = new HostString("example.com", 80); - httpContext.Request.PathBase = "/pathBase"; - httpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b"); - var keyProvider = CreateTestKeyProvider(); + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = "head"; + context.HttpContext.Request.Path = "/path/subpath"; + context.HttpContext.Request.Scheme = "https"; + context.HttpContext.Request.Host = new HostString("example.com", 80); + context.HttpContext.Request.PathBase = "/pathBase"; + context.HttpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b"); - Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", keyProvider.CreateStorageBaseKey(httpContext)); + Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", cacheKeyProvider.CreateStorageBaseKey(context)); } [Fact] public void DefaultKeyProvider_CreateStorageBaseKey_CaseInsensitivePath_NormalizesPath() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Path = "/Path"; - var keyProvider = CreateTestKeyProvider(new ResponseCachingOptions() + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions() { CaseSensitivePaths = false }); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = "GET"; + context.HttpContext.Request.Path = "/Path"; - Assert.Equal($"GET{KeyDelimiter}/PATH", keyProvider.CreateStorageBaseKey(httpContext)); + Assert.Equal($"GET{KeyDelimiter}/PATH", cacheKeyProvider.CreateStorageBaseKey(context)); } [Fact] public void DefaultKeyProvider_CreateStorageBaseKey_CaseSensitivePath_PreservesPathCase() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Path = "/Path"; - var keyProvider = CreateTestKeyProvider(new ResponseCachingOptions() + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions() { CaseSensitivePaths = true }); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = "GET"; + context.HttpContext.Request.Path = "/Path"; - Assert.Equal($"GET{KeyDelimiter}/Path", keyProvider.CreateStorageBaseKey(httpContext)); + Assert.Equal($"GET{KeyDelimiter}/Path", cacheKeyProvider.CreateStorageBaseKey(context)); } [Fact] - public void DefaultKeyProvider_CreateStorageVaryKey_ReturnsCachedVaryGuid_IfVaryRulesIsNullOrEmpty() + public void DefaultKeyProvider_CreateStorageVaryKey_Throws_IfVaryRulesIsNull() { - var httpContext = CreateDefaultContext(); - var keyProvider = CreateTestKeyProvider(); + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); - Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateStorageVaryKey(httpContext, null)); - Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateStorageVaryKey(httpContext, new VaryRules())); + Assert.Throws(() => cacheKeyProvider.CreateStorageVaryKey(context)); + } + + [Fact] + public void DefaultKeyProvider_CreateStorageVaryKey_ReturnsCachedVaryGuid_IfVaryRulesIsEmpty() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.CachedVaryRules = new CachedVaryRules() + { + VaryKeyPrefix = FastGuid.NewGuid().IdString + }; + + Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}", cacheKeyProvider.CreateStorageVaryKey(context)); } [Fact] public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Headers["HeaderA"] = "ValueA"; - httpContext.Request.Headers["HeaderB"] = "ValueB"; - var keyProvider = CreateTestKeyProvider(); + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; + context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; + context.CachedVaryRules = new CachedVaryRules() + { + Headers = new string[] { "HeaderA", "HeaderC" } + }; - Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null", - keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() - { - Headers = new string[] { "HeaderA", "HeaderC" } - })); + Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=", + cacheKeyProvider.CreateStorageVaryKey(context)); } [Fact] public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedParamsOnly() { - var httpContext = CreateDefaultContext(); - httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); - var keyProvider = CreateTestKeyProvider(); + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); + context.CachedVaryRules = new CachedVaryRules() + { + VaryKeyPrefix = FastGuid.NewGuid().IdString, + Params = new string[] { "ParamA", "ParamC" } + }; - Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", - keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() - { - Params = new string[] { "ParamA", "ParamC" } - })); + Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=", + cacheKeyProvider.CreateStorageVaryKey(context)); } [Fact] public void DefaultKeyProvider_CreateStorageVaryKey_IncludesParams_ParamNameCaseInsensitive_UseParamCasing() { - var httpContext = CreateDefaultContext(); - httpContext.Request.QueryString = new QueryString("?parama=ValueA¶mB=ValueB"); - var keyProvider = CreateTestKeyProvider(); + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.QueryString = new QueryString("?parama=ValueA¶mB=ValueB"); + context.CachedVaryRules = new CachedVaryRules() + { + VaryKeyPrefix = FastGuid.NewGuid().IdString, + Params = new string[] { "ParamA", "ParamC" } + }; - Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", - keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() - { - Params = new string[] { "ParamA", "ParamC" } - })); + Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=", + cacheKeyProvider.CreateStorageVaryKey(context)); } [Fact] public void DefaultKeyProvider_CreateStorageVaryKey_IncludesAllQueryParamsGivenAsterisk() { - var httpContext = CreateDefaultContext(); - httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); - var keyProvider = CreateTestKeyProvider(); + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); + context.CachedVaryRules = new CachedVaryRules() + { + VaryKeyPrefix = FastGuid.NewGuid().IdString, + Params = new string[] { "*" } + }; // To support case insensitivity, all param keys are converted to upper case. // Explicit params uses the casing specified in the setting. - Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB", - keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() - { - Params = new string[] { "*" } - })); + Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB", + cacheKeyProvider.CreateStorageVaryKey(context)); } [Fact] public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndParams() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Headers["HeaderA"] = "ValueA"; - httpContext.Request.Headers["HeaderB"] = "ValueB"; - httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); - var keyProvider = CreateTestKeyProvider(); + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; + context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; + context.HttpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); + context.CachedVaryRules = new CachedVaryRules() + { + VaryKeyPrefix = FastGuid.NewGuid().IdString, + Headers = new string[] { "HeaderA", "HeaderC" }, + Params = new string[] { "ParamA", "ParamC" } + }; - Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", - keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() - { - Headers = new string[] { "HeaderA", "HeaderC" }, - Params = new string[] { "ParamA", "ParamC" } - })); - } - - private static HttpContext CreateDefaultContext() - { - var context = new DefaultHttpContext(); - context.AddResponseCachingState(); - context.GetResponseCachingState().CachedVaryRules = TestVaryRules; - return context; - } - - private static IKeyProvider CreateTestKeyProvider() - { - return CreateTestKeyProvider(new ResponseCachingOptions()); - } - - private static IKeyProvider CreateTestKeyProvider(ResponseCachingOptions options) - { - return new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); + Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=", + cacheKeyProvider.CreateStorageVaryKey(context)); } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs deleted file mode 100644 index c900f3161e..0000000000 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs +++ /dev/null @@ -1,742 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Http.Headers; -using Microsoft.AspNetCore.ResponseCaching.Internal; -using Microsoft.Extensions.Internal; -using Microsoft.Extensions.ObjectPool; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; -using Xunit; - -namespace Microsoft.AspNetCore.ResponseCaching.Tests -{ - public class ResponseCachingContextTests - { - [Fact] - public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504() - { - var cache = new TestResponseCache(); - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider()); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - OnlyIfCached = true - }; - - Assert.True(await context.TryServeFromCacheAsync()); - Assert.Equal(StatusCodes.Status504GatewayTimeout, httpContext.Response.StatusCode); - } - - [Fact] - public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails() - { - var cache = new TestResponseCache(); - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" })); - - Assert.False(await context.TryServeFromCacheAsync()); - Assert.Equal(2, cache.GetCount); - } - - [Fact] - public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds() - { - var cache = new TestResponseCache(); - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" })); - - cache.Set( - "BaseKey2", - new CachedResponse() - { - Body = new byte[0] - }, - TimeSpan.Zero); - - Assert.True(await context.TryServeFromCacheAsync()); - Assert.Equal(2, cache.GetCount); - } - - [Fact] - public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseNotFound_Fails() - { - var cache = new TestResponseCache(); - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" })); - - cache.Set( - "BaseKey2", - new CachedVaryRules(), - TimeSpan.Zero); - - Assert.False(await context.TryServeFromCacheAsync()); - Assert.Equal(2, cache.GetCount); - } - - [Fact] - public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseFound_Succeeds() - { - var cache = new TestResponseCache(); - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }, new[] { "VaryKey", "VaryKey2" })); - - cache.Set( - "BaseKey2", - new CachedVaryRules(), - TimeSpan.Zero); - cache.Set( - "BaseKey2VaryKey2", - new CachedResponse() - { - Body = new byte[0] - }, - TimeSpan.Zero); - - Assert.True(await context.TryServeFromCacheAsync()); - Assert.Equal(6, cache.GetCount); - } - - [Fact] - public void ConditionalRequestSatisfied_NotConditionalRequest_Fails() - { - var context = CreateTestContext(new DefaultHttpContext()); - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()); - - Assert.False(context.ConditionalRequestSatisfied(cachedHeaders)); - } - - [Fact] - public void ConditionalRequestSatisfied_IfUnmodifiedSince_FallsbackToDateHeader() - { - var utcNow = DateTimeOffset.UtcNow; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()); - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - - httpContext.Request.GetTypedHeaders().IfUnmodifiedSince = utcNow; - - // Verify modifications in the past succeeds - cachedHeaders.Date = utcNow - TimeSpan.FromSeconds(10); - Assert.True(context.ConditionalRequestSatisfied(cachedHeaders)); - - // Verify modifications at present succeeds - cachedHeaders.Date = utcNow; - Assert.True(context.ConditionalRequestSatisfied(cachedHeaders)); - - // Verify modifications in the future fails - cachedHeaders.Date = utcNow + TimeSpan.FromSeconds(10); - Assert.False(context.ConditionalRequestSatisfied(cachedHeaders)); - } - - [Fact] - public void ConditionalRequestSatisfied_IfUnmodifiedSince_LastModifiedOverridesDateHeader() - { - var utcNow = DateTimeOffset.UtcNow; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()); - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - - httpContext.Request.GetTypedHeaders().IfUnmodifiedSince = utcNow; - - // Verify modifications in the past succeeds - cachedHeaders.Date = utcNow + TimeSpan.FromSeconds(10); - cachedHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); - Assert.True(context.ConditionalRequestSatisfied(cachedHeaders)); - - // Verify modifications at present - cachedHeaders.Date = utcNow + TimeSpan.FromSeconds(10); - cachedHeaders.LastModified = utcNow; - Assert.True(context.ConditionalRequestSatisfied(cachedHeaders)); - - // Verify modifications in the future fails - cachedHeaders.Date = utcNow - TimeSpan.FromSeconds(10); - cachedHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); - Assert.False(context.ConditionalRequestSatisfied(cachedHeaders)); - } - - [Fact] - public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToPass() - { - var utcNow = DateTimeOffset.UtcNow; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()); - var httpContext = new DefaultHttpContext(); - var requestHeaders = httpContext.Request.GetTypedHeaders(); - var context = CreateTestContext(httpContext); - - // This would fail the IfUnmodifiedSince checks - requestHeaders.IfUnmodifiedSince = utcNow; - cachedHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); - - requestHeaders.IfNoneMatch = new List(new[] { EntityTagHeaderValue.Any }); - Assert.True(context.ConditionalRequestSatisfied(cachedHeaders)); - } - - [Fact] - public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToFail() - { - var utcNow = DateTimeOffset.UtcNow; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()); - var httpContext = new DefaultHttpContext(); - var requestHeaders = httpContext.Request.GetTypedHeaders(); - var context = CreateTestContext(httpContext); - - // This would pass the IfUnmodifiedSince checks - requestHeaders.IfUnmodifiedSince = utcNow; - cachedHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); - - requestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); - Assert.False(context.ConditionalRequestSatisfied(cachedHeaders)); - } - - [Fact] - public void ConditionalRequestSatisfied_IfNoneMatch_AnyWithoutETagInResponse_Passes() - { - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()); - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - - httpContext.Request.GetTypedHeaders().IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); - - Assert.False(context.ConditionalRequestSatisfied(cachedHeaders)); - } - - [Fact] - public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithMatch_Passes() - { - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) - { - ETag = new EntityTagHeaderValue("\"E1\"") - }; - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - - httpContext.Request.GetTypedHeaders().IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); - - Assert.True(context.ConditionalRequestSatisfied(cachedHeaders)); - } - - [Fact] - public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithoutMatch_Fails() - { - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) - { - ETag = new EntityTagHeaderValue("\"E2\"") - }; - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - - httpContext.Request.GetTypedHeaders().IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); - - Assert.False(context.ConditionalRequestSatisfied(cachedHeaders)); - } - - [Fact] - public void FinalizeCachingHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() - { - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext, cacheabilityValidator: new CacheabilityValidator()); - var state = httpContext.GetResponseCachingState(); - - Assert.False(state.ShouldCacheResponse); - - context.ShimResponseStream(); - context.FinalizeCachingHeaders(); - - Assert.False(state.ShouldCacheResponse); - } - - [Fact] - public void FinalizeCachingHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable() - { - var httpContext = new DefaultHttpContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true - }; - var context = CreateTestContext(httpContext, cacheabilityValidator: new CacheabilityValidator()); - var state = httpContext.GetResponseCachingState(); - - Assert.False(state.ShouldCacheResponse); - - context.FinalizeCachingHeaders(); - - Assert.True(state.ShouldCacheResponse); - } - - [Fact] - public void FinalizeCachingHeaders_DefaultResponseValidity_Is10Seconds() - { - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - - context.FinalizeCachingHeaders(); - - Assert.Equal(TimeSpan.FromSeconds(10), httpContext.GetResponseCachingState().CachedResponseValidFor); - } - - [Fact] - public void FinalizeCachingHeaders_ResponseValidity_UseExpiryIfAvailable() - { - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - - var state = httpContext.GetResponseCachingState(); - var utcNow = DateTimeOffset.MinValue; - state.ResponseTime = utcNow; - state.ResponseHeaders.Expires = utcNow + TimeSpan.FromSeconds(11); - - context.FinalizeCachingHeaders(); - - Assert.Equal(TimeSpan.FromSeconds(11), state.CachedResponseValidFor); - } - - [Fact] - public void FinalizeCachingHeaders_ResponseValidity_UseMaxAgeIfAvailable() - { - var httpContext = new DefaultHttpContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(12) - }; - var context = CreateTestContext(httpContext); - - var state = httpContext.GetResponseCachingState(); - state.ResponseTime = DateTimeOffset.UtcNow; - state.ResponseHeaders.Expires = state.ResponseTime + TimeSpan.FromSeconds(11); - - context.FinalizeCachingHeaders(); - - Assert.Equal(TimeSpan.FromSeconds(12), state.CachedResponseValidFor); - } - - [Fact] - public void FinalizeCachingHeaders_ResponseValidity_UseSharedMaxAgeIfAvailable() - { - var httpContext = new DefaultHttpContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(12), - SharedMaxAge = TimeSpan.FromSeconds(13) - }; - var context = CreateTestContext(httpContext); - - var state = httpContext.GetResponseCachingState(); - state.ResponseTime = DateTimeOffset.UtcNow; - state.ResponseHeaders.Expires = state.ResponseTime + TimeSpan.FromSeconds(11); - - context.FinalizeCachingHeaders(); - - Assert.Equal(TimeSpan.FromSeconds(13), state.CachedResponseValidFor); - } - - [Fact] - public void FinalizeCachingHeaders_UpdateCachedVaryRules_IfNotEquivalentToPrevious() - { - var httpContext = new DefaultHttpContext(); - var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache); - var state = httpContext.GetResponseCachingState(); - - httpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" }); - httpContext.AddResponseCachingFeature(); - httpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMAA" }); - var cachedVaryRules = new CachedVaryRules() - { - VaryRules = new VaryRules() - { - Headers = new StringValues(new[] { "HeaderA", "HeaderB" }), - Params = new StringValues(new[] { "ParamA", "ParamB" }) - } - }; - state.CachedVaryRules = cachedVaryRules; - - context.FinalizeCachingHeaders(); - - Assert.Equal(1, cache.SetCount); - Assert.NotSame(cachedVaryRules, state.CachedVaryRules); - } - - [Fact] - public void FinalizeCachingHeaders_DoNotUpdateCachedVaryRules_IfEquivalentToPrevious() - { - var httpContext = new DefaultHttpContext(); - var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache); - var state = httpContext.GetResponseCachingState(); - - httpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB" }); - httpContext.AddResponseCachingFeature(); - httpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMA" }); - var cachedVaryRules = new CachedVaryRules() - { - VaryKeyPrefix = FastGuid.NewGuid().IdString, - VaryRules = new VaryRules() - { - Headers = new StringValues(new[] { "HEADERA", "HEADERB" }), - Params = new StringValues(new[] { "PARAMA", "PARAMB" }) - } - }; - state.CachedVaryRules = cachedVaryRules; - - context.FinalizeCachingHeaders(); - - Assert.Equal(0, cache.SetCount); - Assert.Same(cachedVaryRules, state.CachedVaryRules); - } - - [Fact] - public void FinalizeCachingHeaders_DoNotAddDate_IfSpecified() - { - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - var state = httpContext.GetResponseCachingState(); - var utcNow = DateTimeOffset.MinValue; - state.ResponseTime = utcNow; - - Assert.Null(state.ResponseHeaders.Date); - - context.FinalizeCachingHeaders(); - - Assert.Equal(utcNow, state.ResponseHeaders.Date); - } - - [Fact] - public void FinalizeCachingHeaders_AddsDate_IfNoneSpecified() - { - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - var state = httpContext.GetResponseCachingState(); - var utcNow = DateTimeOffset.MinValue; - state.ResponseHeaders.Date = utcNow; - state.ResponseTime = utcNow + TimeSpan.FromSeconds(10); - - Assert.Equal(utcNow, state.ResponseHeaders.Date); - - context.FinalizeCachingHeaders(); - - Assert.Equal(utcNow, state.ResponseHeaders.Date); - } - - [Fact] - public void FinalizeCachingHeaders_StoresCachedResponse_InState() - { - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - var state = httpContext.GetResponseCachingState(); - - Assert.Null(state.CachedResponse); - - context.FinalizeCachingHeaders(); - - Assert.NotNull(state.CachedResponse); - } - - [Fact] - public async Task FinalizeCachingBody_StoreResponseBodySeparately_IfLargerThanLimit() - { - var httpContext = new DefaultHttpContext(); - var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache); - - context.ShimResponseStream(); - await httpContext.Response.WriteAsync(new string('0', 70 * 1024)); - - var state = httpContext.GetResponseCachingState(); - state.ShouldCacheResponse = true; - state.CachedResponse = new CachedResponse() - { - BodyKeyPrefix = FastGuid.NewGuid().IdString - }; - state.StorageBaseKey = "BaseKey"; - state.CachedResponseValidFor = TimeSpan.FromSeconds(10); - - context.FinalizeCachingBody(); - - Assert.Equal(2, cache.SetCount); - } - - [Fact] - public async Task FinalizeCachingBody_StoreResponseBodyInCachedResponse_IfSmallerThanLimit() - { - var httpContext = new DefaultHttpContext(); - var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache); - - context.ShimResponseStream(); - await httpContext.Response.WriteAsync(new string('0', 70 * 1024 - 1)); - - var state = httpContext.GetResponseCachingState(); - state.ShouldCacheResponse = true; - state.CachedResponse = new CachedResponse() - { - BodyKeyPrefix = FastGuid.NewGuid().IdString - }; - state.StorageBaseKey = "BaseKey"; - state.CachedResponseValidFor = TimeSpan.FromSeconds(10); - - context.FinalizeCachingBody(); - - Assert.Equal(1, cache.SetCount); - } - - [Fact] - public async Task FinalizeCachingBody_StoreResponseBodySeparately_LimitIsConfigurable() - { - var httpContext = new DefaultHttpContext(); - var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions() - { - MinimumSplitBodySize = 2048 - }); - - context.ShimResponseStream(); - await httpContext.Response.WriteAsync(new string('0', 1024)); - - var state = httpContext.GetResponseCachingState(); - state.ShouldCacheResponse = true; - state.CachedResponse = new CachedResponse() - { - BodyKeyPrefix = FastGuid.NewGuid().IdString - }; - state.StorageBaseKey = "BaseKey"; - state.CachedResponseValidFor = TimeSpan.FromSeconds(10); - - context.FinalizeCachingBody(); - - Assert.Equal(1, cache.SetCount); - } - - [Fact] - public async Task FinalizeCachingBody_Cache_IfContentLengthMatches() - { - var httpContext = new DefaultHttpContext(); - var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache); - - context.ShimResponseStream(); - httpContext.Response.ContentLength = 10; - await httpContext.Response.WriteAsync(new string('0', 10)); - - var state = httpContext.GetResponseCachingState(); - state.ShouldCacheResponse = true; - state.CachedResponse = new CachedResponse() - { - BodyKeyPrefix = FastGuid.NewGuid().IdString - }; - state.StorageBaseKey = "BaseKey"; - state.CachedResponseValidFor = TimeSpan.FromSeconds(10); - - context.FinalizeCachingBody(); - - Assert.Equal(1, cache.SetCount); - } - - [Fact] - public async Task FinalizeCachingBody_DoNotCache_IfContentLengthMismatches() - { - var httpContext = new DefaultHttpContext(); - var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache); - - context.ShimResponseStream(); - httpContext.Response.ContentLength = 9; - await httpContext.Response.WriteAsync(new string('0', 10)); - - var state = httpContext.GetResponseCachingState(); - state.ShouldCacheResponse = true; - state.CachedResponse = new CachedResponse() - { - BodyKeyPrefix = FastGuid.NewGuid().IdString - }; - state.StorageBaseKey = "BaseKey"; - state.CachedResponseValidFor = TimeSpan.FromSeconds(10); - - context.FinalizeCachingBody(); - - Assert.Equal(0, cache.SetCount); - } - - [Fact] - public async Task FinalizeCachingBody_Cache_IfContentLengthAbsent() - { - var httpContext = new DefaultHttpContext(); - var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache); - - context.ShimResponseStream(); - await httpContext.Response.WriteAsync(new string('0', 10)); - - var state = httpContext.GetResponseCachingState(); - state.ShouldCacheResponse = true; - state.CachedResponse = new CachedResponse() - { - BodyKeyPrefix = FastGuid.NewGuid().IdString - }; - state.StorageBaseKey = "BaseKey"; - state.CachedResponseValidFor = TimeSpan.FromSeconds(10); - - context.FinalizeCachingBody(); - - Assert.Equal(1, cache.SetCount); - } - - [Fact] - public void NormalizeStringValues_NormalizesCasingToUpper() - { - var uppercaseStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); - var lowercaseStrings = new StringValues(new[] { "stringA", "stringB" }); - - var normalizedStrings = ResponseCachingContext.GetNormalizedStringValues(lowercaseStrings); - - Assert.Equal(uppercaseStrings, normalizedStrings); - } - - [Fact] - public void NormalizeStringValues_NormalizesOrder() - { - var orderedStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); - var reverseOrderStrings = new StringValues(new[] { "STRINGB", "STRINGA" }); - - var normalizedStrings = ResponseCachingContext.GetNormalizedStringValues(reverseOrderStrings); - - Assert.Equal(orderedStrings, normalizedStrings); - } - - private static ResponseCachingContext CreateTestContext( - HttpContext httpContext, - IResponseCache responseCache = null, - ResponseCachingOptions options = null, - IKeyProvider keyProvider = null, - ICacheabilityValidator cacheabilityValidator = null) - { - if (responseCache == null) - { - responseCache = new TestResponseCache(); - } - if (options == null) - { - options = new ResponseCachingOptions(); - } - if (keyProvider == null) - { - keyProvider = new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); - } - if (cacheabilityValidator == null) - { - cacheabilityValidator = new TestCacheabilityValidator(); - } - - httpContext.AddResponseCachingState(); - - return new ResponseCachingContext( - httpContext, - responseCache, - options, - cacheabilityValidator, - keyProvider); - } - - private class TestCacheabilityValidator : ICacheabilityValidator - { - public bool CachedEntryIsFresh(HttpContext httpContext, ResponseHeaders cachedResponseHeaders) => true; - - public bool RequestIsCacheable(HttpContext httpContext) => true; - - public bool ResponseIsCacheable(HttpContext httpContext) => true; - } - - private class TestKeyProvider : IKeyProvider - { - private readonly StringValues _baseKey; - private readonly StringValues _varyKey; - - public TestKeyProvider(StringValues? lookupBaseKey = null, StringValues? lookupVaryKey = null) - { - if (lookupBaseKey.HasValue) - { - _baseKey = lookupBaseKey.Value; - } - if (lookupVaryKey.HasValue) - { - _varyKey = lookupVaryKey.Value; - } - } - - public IEnumerable CreateLookupBaseKey(HttpContext httpContext) => _baseKey; - - - public IEnumerable CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules) - { - foreach (var baseKey in _baseKey) - { - foreach (var varyKey in _varyKey) - { - yield return baseKey + varyKey; - } - } - } - - public string CreateBodyKey(HttpContext httpContext) - { - throw new NotImplementedException(); - } - - public string CreateStorageBaseKey(HttpContext httpContext) - { - throw new NotImplementedException(); - } - - public string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules) - { - throw new NotImplementedException(); - } - } - - private class TestResponseCache : IResponseCache - { - private readonly IDictionary _storage = new Dictionary(); - public int GetCount { get; private set; } - public int SetCount { get; private set; } - - public object Get(string key) - { - GetCount++; - try - { - return _storage[key]; - } - catch - { - return null; - } - } - - public void Remove(string key) - { - } - - public void Set(string key, object entry, TimeSpan validFor) - { - SetCount++; - _storage[key] = entry; - } - } - - private class TestHttpSendFileFeature : IHttpSendFileFeature - { - public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation) - { - return TaskCache.CompletedTask; - } - } - } -} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs new file mode 100644 index 0000000000..b480724d4b --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs @@ -0,0 +1,578 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Headers; +using Microsoft.AspNetCore.ResponseCaching.Internal; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.ResponseCaching.Tests +{ + public class ResponseCachingMiddlewareTests + { + [Fact] + public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider()); + var context = TestUtils.CreateTestContext(); + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + { + OnlyIfCached = true + }; + + Assert.True(await middleware.TryServeFromCacheAsync(context)); + Assert.Equal(StatusCodes.Status504GatewayTimeout, context.HttpContext.Response.StatusCode); + } + + [Fact] + public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" })); + var context = TestUtils.CreateTestContext(); + + Assert.False(await middleware.TryServeFromCacheAsync(context)); + Assert.Equal(2, cache.GetCount); + } + + [Fact] + public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" })); + var context = TestUtils.CreateTestContext(); + + cache.Set( + "BaseKey2", + new CachedResponse() + { + Body = new byte[0] + }, + TimeSpan.Zero); + + Assert.True(await middleware.TryServeFromCacheAsync(context)); + Assert.Equal(2, cache.GetCount); + } + + [Fact] + public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseNotFound_Fails() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" })); + var context = TestUtils.CreateTestContext(); + + cache.Set( + "BaseKey2", + new CachedVaryRules(), + TimeSpan.Zero); + + Assert.False(await middleware.TryServeFromCacheAsync(context)); + Assert.Equal(2, cache.GetCount); + } + + [Fact] + public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseFound_Succeeds() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }, new[] { "VaryKey", "VaryKey2" })); + var context = TestUtils.CreateTestContext(); + + cache.Set( + "BaseKey2", + new CachedVaryRules(), + TimeSpan.Zero); + cache.Set( + "BaseKey2VaryKey2", + new CachedResponse() + { + Body = new byte[0] + }, + TimeSpan.Zero); + + Assert.True(await middleware.TryServeFromCacheAsync(context)); + Assert.Equal(6, cache.GetCount); + } + + [Fact] + public void ConditionalRequestSatisfied_NotConditionalRequest_Fails() + { + var context = TestUtils.CreateTestContext(); + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + + Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfUnmodifiedSince_FallsbackToDateHeader() + { + var utcNow = DateTimeOffset.UtcNow; + var context = TestUtils.CreateTestContext(); + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + + context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; + + // Verify modifications in the past succeeds + context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10); + Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + + // Verify modifications at present succeeds + context.CachedResponseHeaders.Date = utcNow; + Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + + // Verify modifications in the future fails + context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); + Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfUnmodifiedSince_LastModifiedOverridesDateHeader() + { + var utcNow = DateTimeOffset.UtcNow; + var context = TestUtils.CreateTestContext(); + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + + context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; + + // Verify modifications in the past succeeds + context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); + context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); + Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + + // Verify modifications at present + context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); + context.CachedResponseHeaders.LastModified = utcNow; + Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + + // Verify modifications in the future fails + context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10); + context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); + Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToPass() + { + var utcNow = DateTimeOffset.UtcNow; + var context = TestUtils.CreateTestContext(); + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + + // This would fail the IfUnmodifiedSince checks + context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; + context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); + + context.TypedRequestHeaders.IfNoneMatch = new List(new[] { EntityTagHeaderValue.Any }); + Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToFail() + { + var utcNow = DateTimeOffset.UtcNow; + var context = TestUtils.CreateTestContext(); + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + + // This would pass the IfUnmodifiedSince checks + context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; + context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); + + context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfNoneMatch_AnyWithoutETagInResponse_Passes() + { + var context = TestUtils.CreateTestContext(); + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + + context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + + Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithMatch_Passes() + { + var context = TestUtils.CreateTestContext(); + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + { + ETag = new EntityTagHeaderValue("\"E1\"") + }; + + context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + + Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithoutMatch_Fails() + { + var context = TestUtils.CreateTestContext(); + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + { + ETag = new EntityTagHeaderValue("\"E2\"") + }; + + context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + + Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + } + + [Fact] + public void FinalizeCachingHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() + { + var middleware = TestUtils.CreateTestMiddleware(cacheabilityValidator: new CacheabilityValidator()); + var context = TestUtils.CreateTestContext(); + + Assert.False(context.ShouldCacheResponse); + + middleware.ShimResponseStream(context); + middleware.FinalizeCachingHeaders(context); + + Assert.False(context.ShouldCacheResponse); + } + + [Fact] + public void FinalizeCachingHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable() + { + var middleware = TestUtils.CreateTestMiddleware(cacheabilityValidator: new CacheabilityValidator()); + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + + Assert.False(context.ShouldCacheResponse); + + middleware.FinalizeCachingHeaders(context); + + Assert.True(context.ShouldCacheResponse); + } + + [Fact] + public void FinalizeCachingHeaders_DefaultResponseValidity_Is10Seconds() + { + var middleware = TestUtils.CreateTestMiddleware(); + var context = TestUtils.CreateTestContext(); + + middleware.FinalizeCachingHeaders(context); + + Assert.Equal(TimeSpan.FromSeconds(10), context.CachedResponseValidFor); + } + + [Fact] + public void FinalizeCachingHeaders_ResponseValidity_UseExpiryIfAvailable() + { + var utcNow = DateTimeOffset.MinValue; + var middleware = TestUtils.CreateTestMiddleware(); + var context = TestUtils.CreateTestContext(); + + context.ResponseTime = utcNow; + context.TypedResponseHeaders.Expires = utcNow + TimeSpan.FromSeconds(11); + + middleware.FinalizeCachingHeaders(context); + + Assert.Equal(TimeSpan.FromSeconds(11), context.CachedResponseValidFor); + } + + [Fact] + public void FinalizeCachingHeaders_ResponseValidity_UseMaxAgeIfAvailable() + { + var middleware = TestUtils.CreateTestMiddleware(); + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(12) + }; + + context.ResponseTime = DateTimeOffset.UtcNow; + context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11); + + middleware.FinalizeCachingHeaders(context); + + Assert.Equal(TimeSpan.FromSeconds(12), context.CachedResponseValidFor); + } + + [Fact] + public void FinalizeCachingHeaders_ResponseValidity_UseSharedMaxAgeIfAvailable() + { + var middleware = TestUtils.CreateTestMiddleware(); + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(12), + SharedMaxAge = TimeSpan.FromSeconds(13) + }; + + context.ResponseTime = DateTimeOffset.UtcNow; + context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11); + + middleware.FinalizeCachingHeaders(context); + + Assert.Equal(TimeSpan.FromSeconds(13), context.CachedResponseValidFor); + } + + [Fact] + public void FinalizeCachingHeaders_UpdateCachedVaryRules_IfNotEquivalentToPrevious() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(cache); + var context = TestUtils.CreateTestContext(); + + context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" }); + context.HttpContext.AddResponseCachingFeature(); + context.HttpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMAA" }); + var cachedVaryRules = new CachedVaryRules() + { + Headers = new StringValues(new[] { "HeaderA", "HeaderB" }), + Params = new StringValues(new[] { "ParamA", "ParamB" }) + }; + context.CachedVaryRules = cachedVaryRules; + + middleware.FinalizeCachingHeaders(context); + + Assert.Equal(1, cache.SetCount); + Assert.NotSame(cachedVaryRules, context.CachedVaryRules); + } + + [Fact] + public void FinalizeCachingHeaders_DoNotUpdateCachedVaryRules_IfEquivalentToPrevious() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(cache); + var context = TestUtils.CreateTestContext(); + + context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB" }); + context.HttpContext.AddResponseCachingFeature(); + context.HttpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMA" }); + var cachedVaryRules = new CachedVaryRules() + { + VaryKeyPrefix = FastGuid.NewGuid().IdString, + Headers = new StringValues(new[] { "HEADERA", "HEADERB" }), + Params = new StringValues(new[] { "PARAMA", "PARAMB" }) + }; + context.CachedVaryRules = cachedVaryRules; + + middleware.FinalizeCachingHeaders(context); + + Assert.Equal(0, cache.SetCount); + Assert.Same(cachedVaryRules, context.CachedVaryRules); + } + + [Fact] + public void FinalizeCachingHeaders_DoNotAddDate_IfSpecified() + { + var utcNow = DateTimeOffset.MinValue; + var middleware = TestUtils.CreateTestMiddleware(); + var context = TestUtils.CreateTestContext(); + context.ResponseTime = utcNow; + + Assert.Null(context.TypedResponseHeaders.Date); + + middleware.FinalizeCachingHeaders(context); + + Assert.Equal(utcNow, context.TypedResponseHeaders.Date); + } + + [Fact] + public void FinalizeCachingHeaders_AddsDate_IfNoneSpecified() + { + var utcNow = DateTimeOffset.MinValue; + var middleware = TestUtils.CreateTestMiddleware(); + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.Date = utcNow; + context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); + + Assert.Equal(utcNow, context.TypedResponseHeaders.Date); + + middleware.FinalizeCachingHeaders(context); + + Assert.Equal(utcNow, context.TypedResponseHeaders.Date); + } + + [Fact] + public void FinalizeCachingHeaders_StoresCachedResponse_InState() + { + var middleware = TestUtils.CreateTestMiddleware(); + var context = TestUtils.CreateTestContext(); + + Assert.Null(context.CachedResponse); + + middleware.FinalizeCachingHeaders(context); + + Assert.NotNull(context.CachedResponse); + } + + [Fact] + public async Task FinalizeCachingBody_StoreResponseBodySeparately_IfLargerThanLimit() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(cache); + var context = TestUtils.CreateTestContext(); + + middleware.ShimResponseStream(context); + await context.HttpContext.Response.WriteAsync(new string('0', 70 * 1024)); + + context.ShouldCacheResponse = true; + context.CachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString + }; + context.StorageBaseKey = "BaseKey"; + context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + middleware.FinalizeCachingBody(context); + + Assert.Equal(2, cache.SetCount); + } + + [Fact] + public async Task FinalizeCachingBody_StoreResponseBodyInCachedResponse_IfSmallerThanLimit() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(cache); + var context = TestUtils.CreateTestContext(); + + middleware.ShimResponseStream(context); + await context.HttpContext.Response.WriteAsync(new string('0', 70 * 1024 - 1)); + + context.ShouldCacheResponse = true; + context.CachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString + }; + context.StorageBaseKey = "BaseKey"; + context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + middleware.FinalizeCachingBody(context); + + Assert.Equal(1, cache.SetCount); + } + + [Fact] + public async Task FinalizeCachingBody_StoreResponseBodySeparately_LimitIsConfigurable() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(cache, new ResponseCachingOptions() + { + MinimumSplitBodySize = 2048 + }); + var context = TestUtils.CreateTestContext(); + + middleware.ShimResponseStream(context); + await context.HttpContext.Response.WriteAsync(new string('0', 1024)); + + context.ShouldCacheResponse = true; + context.CachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString + }; + context.StorageBaseKey = "BaseKey"; + context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + middleware.FinalizeCachingBody(context); + + Assert.Equal(1, cache.SetCount); + } + + [Fact] + public async Task FinalizeCachingBody_Cache_IfContentLengthMatches() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(cache); + var context = TestUtils.CreateTestContext(); + + middleware.ShimResponseStream(context); + context.HttpContext.Response.ContentLength = 10; + await context.HttpContext.Response.WriteAsync(new string('0', 10)); + + context.ShouldCacheResponse = true; + context.CachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString + }; + context.StorageBaseKey = "BaseKey"; + context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + middleware.FinalizeCachingBody(context); + + Assert.Equal(1, cache.SetCount); + } + + [Fact] + public async Task FinalizeCachingBody_DoNotCache_IfContentLengthMismatches() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(cache); + var context = TestUtils.CreateTestContext(); + + middleware.ShimResponseStream(context); + context.HttpContext.Response.ContentLength = 9; + await context.HttpContext.Response.WriteAsync(new string('0', 10)); + + context.ShouldCacheResponse = true; + context.CachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString + }; + context.StorageBaseKey = "BaseKey"; + context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + middleware.FinalizeCachingBody(context); + + Assert.Equal(0, cache.SetCount); + } + + [Fact] + public async Task FinalizeCachingBody_Cache_IfContentLengthAbsent() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(cache); + var context = TestUtils.CreateTestContext(); + + middleware.ShimResponseStream(context); + await context.HttpContext.Response.WriteAsync(new string('0', 10)); + + context.ShouldCacheResponse = true; + context.CachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString + }; + context.StorageBaseKey = "BaseKey"; + context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + middleware.FinalizeCachingBody(context); + + Assert.Equal(1, cache.SetCount); + } + + [Fact] + public void NormalizeStringValues_NormalizesCasingToUpper() + { + var uppercaseStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); + var lowercaseStrings = new StringValues(new[] { "stringA", "stringB" }); + + var normalizedStrings = ResponseCachingMiddleware.GetNormalizedStringValues(lowercaseStrings); + + Assert.Equal(uppercaseStrings, normalizedStrings); + } + + [Fact] + public void NormalizeStringValues_NormalizesOrder() + { + var orderedStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); + var reverseOrderStrings = new StringValues(new[] { "STRINGB", "STRINGA" }); + + var normalizedStrings = ResponseCachingMiddleware.GetNormalizedStringValues(reverseOrderStrings); + + Assert.Equal(orderedStrings, normalizedStrings); + } + } +} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs index d37d76e30e..e910ad5478 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs @@ -6,11 +6,9 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; using Xunit; @@ -21,7 +19,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfAvailable() { - var builder = CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -36,7 +34,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfNotAvailable() { - var builder = CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -51,10 +49,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryHeader_Matches() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -71,10 +69,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryHeader_Mismatches() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -92,10 +90,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryParams_Matches() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.GetResponseCachingFeature().VaryParams = "param"; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -111,10 +109,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryParamsExplicit_Matches_ParamNameCaseInsensitive() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "paramb" }; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -130,10 +128,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryParamsStar_Matches_ParamNameCaseInsensitive() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.GetResponseCachingFeature().VaryParams = new[] { "*" }; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -149,10 +147,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryParamsExplicit_Matches_OrderInsensitive() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.GetResponseCachingFeature().VaryParams = new[] { "ParamB", "ParamA" }; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -168,10 +166,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryParamsStar_Matches_OrderInsensitive() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.GetResponseCachingFeature().VaryParams = new[] { "*" }; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -187,10 +185,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryParams_Mismatches() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.GetResponseCachingFeature().VaryParams = "param"; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -206,10 +204,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryParamsExplicit_Mismatch_ParamValueCaseSensitive() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "ParamB" }; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -225,10 +223,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryParamsStar_Mismatch_ParamValueCaseSensitive() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.GetResponseCachingFeature().VaryParams = new[] { "*" }; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -244,7 +242,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfRequestRequirements_NotMet() { - var builder = CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -263,7 +261,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void Serves504_IfOnlyIfCachedHeader_IsSpecified() { - var builder = CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -283,10 +281,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfSetCookie_IsSpecified() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { var headers = context.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -302,7 +300,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfIHttpSendFileFeature_NotUsed() { - var builder = CreateBuilderWithResponseCaching(app => + var builder = TestUtils.CreateBuilderWithResponseCaching(app => { app.Use(async (context, next) => { @@ -324,7 +322,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfIHttpSendFileFeature_Used() { - var builder = CreateBuilderWithResponseCaching( + var builder = TestUtils.CreateBuilderWithResponseCaching( app => { app.Use(async (context, next) => @@ -333,10 +331,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await next.Invoke(); }); }, - async (context) => + requestDelegate: async (context) => { await context.Features.Get().SendFileAsync("dummy", 0, 0, CancellationToken.None); - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -352,7 +350,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfSubsequentRequest_ContainsNoStore() { - var builder = CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -371,7 +369,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfInitialRequestContains_NoStore() { - var builder = CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -390,7 +388,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void Serves304_IfIfModifiedSince_Satisfied() { - var builder = CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -407,7 +405,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfIfModifiedSince_NotSatisfied() { - var builder = CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -423,10 +421,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void Serves304_IfIfNoneMatch_Satisfied() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -444,10 +442,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfIfNoneMatch_NotSatisfied() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -464,7 +462,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfBodySize_IsCacheable() { - var builder = CreateBuilderWithResponseCaching(new ResponseCachingOptions() + var builder = TestUtils.CreateBuilderWithResponseCaching(options: new ResponseCachingOptions() { MaximumCachedBodySize = 100 }); @@ -482,7 +480,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfBodySize_IsNotCacheable() { - var builder = CreateBuilderWithResponseCaching(new ResponseCachingOptions() + var builder = TestUtils.CreateBuilderWithResponseCaching(options: new ResponseCachingOptions() { MaximumCachedBodySize = 1 }); @@ -500,10 +498,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_WithoutReplacingCachedVaryBy_OnCacheMiss() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -541,57 +539,5 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); } - - private static RequestDelegate DefaultRequestDelegate = async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); - }; - - private static IWebHostBuilder CreateBuilderWithResponseCaching() => - CreateBuilderWithResponseCaching(app => { }, new ResponseCachingOptions(), DefaultRequestDelegate); - - private static IWebHostBuilder CreateBuilderWithResponseCaching(ResponseCachingOptions options) => - CreateBuilderWithResponseCaching(app => { }, options, DefaultRequestDelegate); - - private static IWebHostBuilder CreateBuilderWithResponseCaching(RequestDelegate requestDelegate) => - CreateBuilderWithResponseCaching(app => { }, new ResponseCachingOptions(), requestDelegate); - - private static IWebHostBuilder CreateBuilderWithResponseCaching(Action configureDelegate) => - CreateBuilderWithResponseCaching(configureDelegate, new ResponseCachingOptions(), DefaultRequestDelegate); - - private static IWebHostBuilder CreateBuilderWithResponseCaching(Action configureDelegate, RequestDelegate requestDelegate) => - CreateBuilderWithResponseCaching(configureDelegate, new ResponseCachingOptions(), requestDelegate); - - private static IWebHostBuilder CreateBuilderWithResponseCaching(Action configureDelegate, ResponseCachingOptions options, RequestDelegate requestDelegate) - { - return new WebHostBuilder() - .ConfigureServices(services => - { - services.AddDistributedResponseCache(); - }) - .Configure(app => - { - configureDelegate(app); - app.UseResponseCaching(options); - app.Run(requestDelegate); - }); - } - - private class DummySendFileFeature : IHttpSendFileFeature - { - public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation) - { - return Task.FromResult(0); - } - } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs new file mode 100644 index 0000000000..23c80cddf4 --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs @@ -0,0 +1,211 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.ResponseCaching.Tests +{ + internal class TestUtils + { + internal static RequestDelegate DefaultRequestDelegate = async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + await context.Response.WriteAsync(uniqueId); + }; + + internal static ICacheKeyProvider CreateTestKeyProvider() + { + return CreateTestKeyProvider(new ResponseCachingOptions()); + } + + internal static ICacheKeyProvider CreateTestKeyProvider(ResponseCachingOptions options) + { + return new CacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); + } + + internal static IWebHostBuilder CreateBuilderWithResponseCaching( + Action configureDelegate = null, + ResponseCachingOptions options = null, + RequestDelegate requestDelegate = null) + { + if (configureDelegate == null) + { + configureDelegate = app => { }; + } + if (options == null) + { + options = new ResponseCachingOptions(); + } + if (requestDelegate == null) + { + requestDelegate = DefaultRequestDelegate; + } + + return new WebHostBuilder() + .ConfigureServices(services => + { + services.AddDistributedResponseCache(); + }) + .Configure(app => + { + configureDelegate(app); + app.UseResponseCaching(options); + app.Run(requestDelegate); + }); + } + + internal static ResponseCachingMiddleware CreateTestMiddleware( + IResponseCache responseCache = null, + ResponseCachingOptions options = null, + ICacheKeyProvider cacheKeyProvider = null, + ICacheabilityValidator cacheabilityValidator = null) + { + if (responseCache == null) + { + responseCache = new TestResponseCache(); + } + if (options == null) + { + options = new ResponseCachingOptions(); + } + if (cacheKeyProvider == null) + { + cacheKeyProvider = new CacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); + } + if (cacheabilityValidator == null) + { + cacheabilityValidator = new TestCacheabilityValidator(); + } + + return new ResponseCachingMiddleware( + httpContext => TaskCache.CompletedTask, + responseCache, + Options.Create(options), + cacheabilityValidator, + cacheKeyProvider); + } + + internal static ResponseCachingContext CreateTestContext() + { + return new ResponseCachingContext(new DefaultHttpContext()); + } + } + + internal class DummySendFileFeature : IHttpSendFileFeature + { + public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation) + { + return TaskCache.CompletedTask; + } + } + + internal class TestCacheabilityValidator : ICacheabilityValidator + { + public bool IsCachedEntryFresh(ResponseCachingContext context) => true; + + public bool IsRequestCacheable(ResponseCachingContext context) => true; + + public bool IsResponseCacheable(ResponseCachingContext context) => true; + } + + internal class TestKeyProvider : ICacheKeyProvider + { + private readonly StringValues _baseKey; + private readonly StringValues _varyKey; + + public TestKeyProvider(StringValues? lookupBaseKey = null, StringValues? lookupVaryKey = null) + { + if (lookupBaseKey.HasValue) + { + _baseKey = lookupBaseKey.Value; + } + if (lookupVaryKey.HasValue) + { + _varyKey = lookupVaryKey.Value; + } + } + + public IEnumerable CreateLookupBaseKeys(ResponseCachingContext context) => _baseKey; + + + public IEnumerable CreateLookupVaryKeys(ResponseCachingContext context) + { + foreach (var baseKey in _baseKey) + { + foreach (var varyKey in _varyKey) + { + yield return baseKey + varyKey; + } + } + } + + public string CreateStorageBaseKey(ResponseCachingContext context) + { + throw new NotImplementedException(); + } + + public string CreateStorageVaryKey(ResponseCachingContext context) + { + throw new NotImplementedException(); + } + } + + internal class TestResponseCache : IResponseCache + { + private readonly IDictionary _storage = new Dictionary(); + public int GetCount { get; private set; } + public int SetCount { get; private set; } + + public object Get(string key) + { + GetCount++; + try + { + return _storage[key]; + } + catch + { + return null; + } + } + + public void Remove(string key) + { + } + + public void Set(string key, object entry, TimeSpan validFor) + { + SetCount++; + _storage[key] = entry; + } + } + + internal class TestHttpSendFileFeature : IHttpSendFileFeature + { + public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation) + { + return TaskCache.CompletedTask; + } + } +}