Refactoring ResponseCacheContext

- Extract key creationg into a service
- Extract cacheability checks into a service
- Add a ResponseCachingState feature to preserve response cache context between operations
- Recognize Set-Cookie as not-cacheable
This commit is contained in:
John Luo 2016-09-02 20:32:45 -07:00
parent 02b8fb3bbc
commit 7300d9e936
27 changed files with 1720 additions and 1487 deletions

View File

@ -0,0 +1,206 @@
// 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.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.ResponseCaching
{
public class CacheabilityValidator : ICacheabilityValidator
{
public virtual bool RequestIsCacheable(HttpContext httpContext)
{
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;
if (!string.Equals("GET", request.Method, StringComparison.OrdinalIgnoreCase) &&
!string.Equals("HEAD", request.Method, StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Verify existence of authorization headers
// TODO: The server may indicate that the response to these request are cacheable
if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.Authorization]))
{
return false;
}
// Verify request cache-control parameters
// TODO: no-cache requests can be retrieved upon validation with origin
if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl]))
{
if (state.RequestCacheControl.NoCache)
{
return false;
}
}
else
{
// Support for legacy HTTP 1.0 cache directive
var pragmaHeaderValues = request.Headers[HeaderNames.Pragma];
foreach (var directive in pragmaHeaderValues)
{
if (string.Equals("no-cache", directive, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
}
// TODO: Verify global middleware settings? Explicit ignore list, range requests, etc.
return true;
}
public virtual bool ResponseIsCacheable(HttpContext httpContext)
{
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)
{
return false;
}
// Check no-store
if (state.RequestCacheControl.NoStore || state.ResponseCacheControl.NoStore)
{
return false;
}
// Check no-cache
// TODO: Handle no-cache with headers
if (state.ResponseCacheControl.NoCache)
{
return false;
}
var response = httpContext.Response;
// Do not cache responses with Set-Cookie headers
if (!StringValues.IsNullOrEmpty(response.Headers[HeaderNames.SetCookie]))
{
return false;
}
// Do not cache responses varying by *
var varyHeader = response.Headers[HeaderNames.Vary];
if (varyHeader.Count == 1 && string.Equals(varyHeader, "*", StringComparison.OrdinalIgnoreCase))
{
return false;
}
// TODO: public MAY override the cacheability checks for private and status codes
// Check private
if (state.ResponseCacheControl.Private)
{
return false;
}
// Check response code
// TODO: RFC also lists 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 as cacheable by default
if (response.StatusCode != StatusCodes.Status200OK)
{
return false;
}
// Check response freshness
// TODO: apparent age vs corrected age value
if (state.ResponseHeaders.Date == null)
{
if (state.ResponseCacheControl.SharedMaxAge == null &&
state.ResponseCacheControl.MaxAge == null &&
state.ResponseTime > state.ResponseHeaders.Expires)
{
return false;
}
}
else
{
var age = state.ResponseTime - state.ResponseHeaders.Date.Value;
// Validate shared max age
if (age > state.ResponseCacheControl.SharedMaxAge)
{
return false;
}
else if (state.ResponseCacheControl.SharedMaxAge == null)
{
// Validate max age
if (age > state.ResponseCacheControl.MaxAge)
{
return false;
}
else if (state.ResponseCacheControl.MaxAge == null)
{
// Validate expiration
if (state.ResponseTime > state.ResponseHeaders.Expires)
{
return false;
}
}
}
}
return true;
}
public virtual bool CachedEntryIsFresh(HttpContext httpContext, ResponseHeaders cachedResponseHeaders)
{
var state = httpContext.GetResponseCachingState();
var age = state.CachedEntryAge;
// Add min-fresh requirements
if (state.RequestCacheControl.MinFresh != null)
{
age += state.RequestCacheControl.MinFresh.Value;
}
// Validate shared max age, this overrides any max age settings for shared caches
if (age > cachedResponseHeaders.CacheControl.SharedMaxAge)
{
// shared max age implies must revalidate
return false;
}
else if (cachedResponseHeaders.CacheControl.SharedMaxAge == null)
{
// Validate max age
if (age > cachedResponseHeaders.CacheControl.MaxAge || age > state.RequestCacheControl.MaxAge)
{
// Must revalidate
if (cachedResponseHeaders.CacheControl.MustRevalidate)
{
return false;
}
// Request allows stale values
if (age < state.RequestCacheControl.MaxStaleLimit)
{
// TODO: Add warning header indicating the response is stale
return true;
}
return false;
}
else if (cachedResponseHeaders.CacheControl.MaxAge == null && state.RequestCacheControl.MaxAge == null)
{
// Validate expiration
if (state.ResponseTime > cachedResponseHeaders.Expires)
{
return false;
}
}
}
return true;
}
}
}

View File

@ -2,25 +2,21 @@
// 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 void AddResponseCachingFeature(this HttpContext httpContext)
public static ResponseCachingState GetResponseCachingState(this HttpContext httpContext)
{
httpContext.Features.Set(new ResponseCachingFeature());
}
public static void RemoveResponseCachingFeature(this HttpContext httpContext)
{
httpContext.Features.Set<ResponseCachingFeature>(null);
return httpContext.Features.Get<ResponseCachingState>();
}
public static ResponseCachingFeature GetResponseCachingFeature(this HttpContext httpContext)
{
return httpContext.Features.Get<ResponseCachingFeature>() ?? new ResponseCachingFeature();
return httpContext.Features.Get<ResponseCachingFeature>();
}
}
}

View File

@ -16,7 +16,7 @@ namespace Microsoft.Extensions.DependencyInjection
{
throw new ArgumentNullException(nameof(services));
}
services.AddMemoryCache();
services.AddResponseCachingServices();
services.TryAdd(ServiceDescriptor.Singleton<IResponseCache, MemoryResponseCache>());
@ -30,7 +30,7 @@ namespace Microsoft.Extensions.DependencyInjection
{
throw new ArgumentNullException(nameof(services));
}
services.AddDistributedMemoryCache();
services.AddResponseCachingServices();
services.TryAdd(ServiceDescriptor.Singleton<IResponseCache, DistributedResponseCache>());
@ -40,8 +40,8 @@ namespace Microsoft.Extensions.DependencyInjection
private static IServiceCollection AddResponseCachingServices(this IServiceCollection services)
{
services.TryAdd(ServiceDescriptor.Singleton<IResponseCachingCacheKeyModifier, NoopCacheKeyModifier>());
services.TryAdd(ServiceDescriptor.Singleton<IResponseCachingCacheabilityValidator, NoopCacheabilityValidator>());
services.TryAdd(ServiceDescriptor.Singleton<IKeyProvider, KeyProvider>());
services.TryAdd(ServiceDescriptor.Singleton<ICacheabilityValidator, CacheabilityValidator>());
return services;
}

View File

@ -0,0 +1,33 @@
// 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
{
/// <summary>
/// Determine the cacheability of an HTTP request.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
/// <returns><c>true</c> if the request is cacheable; otherwise <c>false</c>.</returns>
bool RequestIsCacheable(HttpContext httpContext);
/// <summary>
/// Determine the cacheability of an HTTP response.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
/// <returns><c>true</c> if the response is cacheable; otherwise <c>false</c>.</returns>
bool ResponseIsCacheable(HttpContext httpContext);
/// <summary>
/// Determine the freshness of the cached entry.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
/// <param name="cachedResponseHeaders">The <see cref="ResponseHeaders"/> of the cached entry.</param>
/// <returns><c>true</c> if the cached entry is fresh; otherwise <c>false</c>.</returns>
bool CachedEntryIsFresh(HttpContext httpContext, ResponseHeaders cachedResponseHeaders);
}
}

View File

@ -0,0 +1,25 @@
// 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;
namespace Microsoft.AspNetCore.ResponseCaching
{
public interface IKeyProvider
{
/// <summary>
/// Create a key using the HTTP request.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
/// <returns>The created base key.</returns>
string CreateBaseKey(HttpContext httpContext);
/// <summary>
/// Create a key using the HTTP context and vary rules.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
/// <param name="varyRules">The <see cref="VaryRules"/>.</param>
/// <returns>The created base key.</returns>
string CreateVaryKey(HttpContext httpContext, VaryRules varyRules);
}
}

View File

@ -1,17 +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.AspNetCore.Http;
namespace Microsoft.AspNetCore.ResponseCaching
{
public interface IResponseCachingCacheKeyModifier
{
/// <summary>
/// Create a key segment that is prepended to the default cache key.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
/// <returns>The key segment that will be prepended to the default cache key.</returns>
string CreatKeyPrefix(HttpContext httpContext);
}
}

View File

@ -1,24 +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.AspNetCore.Http;
namespace Microsoft.AspNetCore.ResponseCaching
{
public interface IResponseCachingCacheabilityValidator
{
/// <summary>
/// Override default behavior for determining cacheability of an HTTP request.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
/// <returns>The <see cref="OverrideResult"/>.</returns>
OverrideResult RequestIsCacheableOverride(HttpContext httpContext);
/// <summary>
/// Override default behavior for determining cacheability of an HTTP response.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
/// <returns>The <see cref="OverrideResult"/>.</returns>
OverrideResult ResponseIsCacheableOverride(HttpContext httpContext);
}
}

View File

@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
internal static class DefaultResponseCacheSerializer
internal static class CacheEntrySerializer
{
private const int FormatVersion = 1;
@ -37,8 +37,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
// Serialization Format
// Format version (int)
// Type (char: 'R' for CachedResponse, 'V' for CachedVaryBy)
// Type-dependent data (see CachedResponse and CachedVaryBy)
// Type (char: 'R' for CachedResponse, 'V' for CachedVaryRules)
// Type-dependent data (see CachedResponse and CachedVaryRules)
public static object Read(BinaryReader reader)
{
if (reader == null)
@ -60,11 +60,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
}
else if (type == 'V')
{
var cachedResponse = ReadCachedVaryBy(reader);
return cachedResponse;
var cachedVaryRules = ReadCachedVaryRules(reader);
return cachedVaryRules;
}
// Unable to read as CachedResponse or CachedVaryBy
// Unable to read as CachedResponse or CachedVaryRules
return null;
}
@ -96,12 +96,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
}
// Serialization Format
// Headers count
// Headers if count > 0 (comma separated string)
// Params count
// Params if count > 0 (comma separated string)
private static CachedVaryBy ReadCachedVaryBy(BinaryReader reader)
// ContainsVaryRules (bool)
// If containing vary rules:
// Headers count
// Headers if count > 0 (comma separated string)
// Params count
// Params if count > 0 (comma separated string)
private static CachedVaryRules ReadCachedVaryRules(BinaryReader reader)
{
if (!reader.ReadBoolean())
{
return new CachedVaryRules();
}
var headerCount = reader.ReadInt32();
var headers = new string[headerCount];
for (var index = 0; index < headerCount; index++)
@ -115,7 +122,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
param[index] = reader.ReadString();
}
return new CachedVaryBy { Headers = headers, Params = param };
return new CachedVaryRules { VaryRules = new VaryRules() { Headers = headers, Params = param } };
}
// See serialization format above
@ -132,14 +139,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
}
writer.Write(FormatVersion);
if (entry is CachedResponse)
{
writer.Write('R');
WriteCachedResponse(writer, entry as CachedResponse);
}
else if (entry is CachedVaryBy)
else if (entry is CachedVaryRules)
{
WriteCachedVaryBy(writer, entry as CachedVaryBy);
writer.Write('V');
WriteCachedVaryRules(writer, entry as CachedVaryRules);
}
else
{
@ -150,7 +159,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
// See serialization format above
private static void WriteCachedResponse(BinaryWriter writer, CachedResponse entry)
{
writer.Write('R');
writer.Write(entry.Created.UtcTicks);
writer.Write(entry.StatusCode);
writer.Write(entry.Headers.Count);
@ -165,20 +173,26 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
}
// See serialization format above
private static void WriteCachedVaryBy(BinaryWriter writer, CachedVaryBy entry)
private static void WriteCachedVaryRules(BinaryWriter writer, CachedVaryRules varyRules)
{
writer.Write('V');
writer.Write(entry.Headers.Count);
foreach (var header in entry.Headers)
if (varyRules.VaryRules == null)
{
writer.Write(header);
writer.Write(false);
}
writer.Write(entry.Params.Count);
foreach (var param in entry.Params)
else
{
writer.Write(param);
writer.Write(true);
writer.Write(varyRules.VaryRules.Headers.Count);
foreach (var header in varyRules.VaryRules.Headers)
{
writer.Write(header);
}
writer.Write(varyRules.VaryRules.Params.Count);
foreach (var param in varyRules.VaryRules.Params)
{
writer.Write(param);
}
}
}
}

View File

@ -1,12 +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 Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
internal class NoopCacheKeyModifier : IResponseCachingCacheKeyModifier
internal class CachedVaryRules
{
public string CreatKeyPrefix(HttpContext httpContext) => null;
internal VaryRules VaryRules;
}
}

View File

@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
try
{
return DefaultResponseCacheSerializer.Deserialize(_cache.Get(key));
return CacheEntrySerializer.Deserialize(_cache.Get(key));
}
catch
{
@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
_cache.Set(
key,
DefaultResponseCacheSerializer.Serialize(entry),
CacheEntrySerializer.Serialize(entry),
new DistributedCacheEntryOptions()
{
AbsoluteExpirationRelativeToNow = validFor

View File

@ -0,0 +1,44 @@
// 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;
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
internal static class HttpContextInternalExtensions
{
internal static void AddResponseCachingFeature(this HttpContext httpContext)
{
if (httpContext.GetResponseCachingFeature() != null)
{
throw new InvalidOperationException($"Another instance of {nameof(ResponseCachingFeature)} already exists. Only one instance of {nameof(ResponseCachingMiddleware)} can be configured for an application.");
}
httpContext.Features.Set(new ResponseCachingFeature());
}
internal static void RemoveResponseCachingFeature(this HttpContext httpContext)
{
httpContext.Features.Set<ResponseCachingFeature>(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<ResponseCachingState>(null);
}
internal static ResponseCachingState GetResponseCachingState(this HttpContext httpContext)
{
return httpContext.Features.Get<ResponseCachingState>();
}
}
}

View File

@ -1,14 +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.AspNetCore.Http;
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
internal class NoopCacheabilityValidator : IResponseCachingCacheabilityValidator
{
public OverrideResult RequestIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic;
public OverrideResult ResponseIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic;
}
}

View File

@ -0,0 +1,89 @@
// 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 BaseKey { get; internal set; }
public string VaryKey { 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; }
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;
}
}
}
}

View File

@ -0,0 +1,157 @@
// 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.Linq;
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.ResponseCaching
{
public class KeyProvider : IKeyProvider
{
// Use the record separator for delimiting components of the cache key to avoid possible collisions
private static readonly char KeyDelimiter = '\x1e';
private readonly ObjectPool<StringBuilder> _builderPool;
private readonly ResponseCachingOptions _options;
public KeyProvider(ObjectPoolProvider poolProvider, IOptions<ResponseCachingOptions> options)
{
if (poolProvider == null)
{
throw new ArgumentNullException(nameof(poolProvider));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_builderPool = poolProvider.CreateStringBuilderPool();
_options = options.Value;
}
// GET<delimiter>/PATH
// TODO: Method invariant retrieval? E.g. HEAD after GET to the same resource.
public virtual string CreateBaseKey(HttpContext httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
var request = httpContext.Request;
var builder = _builderPool.Get();
try
{
builder
.Append(request.Method.ToUpperInvariant())
.Append(KeyDelimiter)
.Append(_options.CaseSensitivePaths ? request.Path.Value : request.Path.Value.ToUpperInvariant());
return builder.ToString();;
}
finally
{
_builderPool.Return(builder);
}
}
// BaseKey<delimiter>H<delimiter>HeaderName=HeaderValue<delimiter>Q<delimiter>QueryName=QueryValue
public virtual string CreateVaryKey(HttpContext httpContext, VaryRules varyRules)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
if (varyRules == null)
{
// TODO: replace this with a GUID
return httpContext.GetResponseCachingState()?.BaseKey ?? CreateBaseKey(httpContext);
}
var request = httpContext.Request;
var builder = _builderPool.Get();
try
{
// TODO: replace this with a GUID
builder.Append(httpContext.GetResponseCachingState()?.BaseKey ?? CreateBaseKey(httpContext));
// Vary by headers
if (varyRules?.Headers.Count > 0)
{
// Append a group separator for the header segment of the cache key
builder.Append(KeyDelimiter)
.Append('H');
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);
}
}
// Vary by query params
if (varyRules?.Params.Count > 0)
{
// Append a group separator for the query parameter segment of the cache key
builder.Append(KeyDelimiter)
.Append('Q');
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))
{
builder.Append(KeyDelimiter)
.Append(query.Key.ToUpperInvariant())
.Append("=")
.Append(query.Value);
}
}
else
{
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);
}
}
}
return builder.ToString();
}
finally
{
_builderPool.Return(builder);
}
}
}
}

View File

@ -1,23 +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.
namespace Microsoft.AspNetCore.ResponseCaching
{
public enum OverrideResult
{
/// <summary>
/// Use the default logic for determining cacheability.
/// </summary>
UseDefaultLogic,
/// <summary>
/// Ignore default logic and do not cache.
/// </summary>
DoNotCache,
/// <summary>
/// Ignore default logic and cache.
/// </summary>
Cache
}
}

View File

@ -4,15 +4,12 @@
using System;
using System.IO;
using System.Globalization;
using System.Linq;
using System.Text;
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.ObjectPool;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
@ -20,54 +17,37 @@ namespace Microsoft.AspNetCore.ResponseCaching
{
internal class ResponseCachingContext
{
private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue();
// Use the record separator for delimiting components of the cache key to avoid possible collisions
private static readonly char KeyDelimiter = '\x1e';
private readonly HttpContext _httpContext;
private readonly IResponseCache _cache;
private readonly ResponseCachingOptions _options;
private readonly ObjectPool<StringBuilder> _builderPool;
private readonly IResponseCachingCacheabilityValidator _cacheabilityValidator;
private readonly IResponseCachingCacheKeyModifier _cacheKeyModifier;
private readonly ICacheabilityValidator _cacheabilityValidator;
private readonly IKeyProvider _keyProvider;
private string _cacheKey;
private ResponseType? _responseType;
private RequestHeaders _requestHeaders;
private ResponseHeaders _responseHeaders;
private CacheControlHeaderValue _requestCacheControl;
private CacheControlHeaderValue _responseCacheControl;
private bool? _cacheResponse;
private CachedResponse _cachedResponse;
private TimeSpan _cachedResponseValidFor;
internal DateTimeOffset _responseTime;
private ResponseCachingState _state;
// Internal for testing
internal ResponseCachingContext(
HttpContext httpContext,
IResponseCache cache,
ResponseCachingOptions options,
ObjectPool<StringBuilder> builderPool,
IResponseCachingCacheabilityValidator cacheabilityValidator,
IResponseCachingCacheKeyModifier cacheKeyModifier)
ICacheabilityValidator cacheabilityValidator,
IKeyProvider keyProvider)
{
_httpContext = httpContext;
_cache = cache;
_options = options;
_builderPool = builderPool;
_cacheabilityValidator = cacheabilityValidator;
_cacheKeyModifier = cacheKeyModifier;
_keyProvider = keyProvider;
}
internal bool CacheResponse
internal ResponseCachingState State
{
get
{
if (_cacheResponse == null)
if (_state == null)
{
_cacheResponse = ResponseIsCacheable();
_state = _httpContext.GetResponseCachingState();
}
return _cacheResponse.Value;
return _state;
}
}
@ -79,349 +59,17 @@ namespace Microsoft.AspNetCore.ResponseCaching
private IHttpSendFileFeature OriginalSendFileFeature { get; set; }
private RequestHeaders RequestHeaders
{
get
{
if (_requestHeaders == null)
{
_requestHeaders = _httpContext.Request.GetTypedHeaders();
}
return _requestHeaders;
}
}
private ResponseHeaders ResponseHeaders
{
get
{
if (_responseHeaders == null)
{
_responseHeaders = _httpContext.Response.GetTypedHeaders();
}
return _responseHeaders;
}
}
private CacheControlHeaderValue RequestCacheControl
{
get
{
if (_requestCacheControl == null)
{
_requestCacheControl = RequestHeaders.CacheControl ?? EmptyCacheControl;
}
return _requestCacheControl;
}
}
private CacheControlHeaderValue ResponseCacheControl
{
get
{
if (_responseCacheControl == null)
{
_responseCacheControl = ResponseHeaders.CacheControl ?? EmptyCacheControl;
}
return _responseCacheControl;
}
}
// GET;/PATH;VaryBy
// TODO: Method invariant retrieval? E.g. HEAD after GET to the same resource.
internal string CreateCacheKey()
{
return CreateCacheKey(varyBy: null);
}
internal string CreateCacheKey(CachedVaryBy varyBy)
{
var request = _httpContext.Request;
var builder = _builderPool.Get();
try
{
// Prepend custom cache key prefix
var customKeyPrefix = _cacheKeyModifier.CreatKeyPrefix(_httpContext);
if (!string.IsNullOrEmpty(customKeyPrefix))
{
builder.Append(customKeyPrefix)
.Append(KeyDelimiter);
}
// Default key
builder
.Append(request.Method.ToUpperInvariant())
.Append(KeyDelimiter)
.Append(_options.CaseSensitivePaths ? request.Path.Value : request.Path.Value.ToUpperInvariant());
// Vary by headers
if (varyBy?.Headers.Count > 0)
{
// Append a group separator for the header segment of the cache key
builder.Append(KeyDelimiter)
.Append('H');
// TODO: resolve key format and delimiters
foreach (var header in varyBy.Headers)
{
// TODO: Normalization of order, case?
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);
}
}
// Vary by query params
if (varyBy?.Params.Count > 0)
{
// Append a group separator for the query parameter segment of the cache key
builder.Append(KeyDelimiter)
.Append('Q');
if (varyBy.Params.Count == 1 && string.Equals(varyBy.Params[0], "*", StringComparison.Ordinal))
{
// Vary by all available query params
foreach (var query in _httpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase))
{
builder.Append(KeyDelimiter)
.Append(query.Key.ToUpperInvariant())
.Append("=")
.Append(query.Value);
}
}
else
{
// TODO: resolve key format and delimiters
foreach (var param in varyBy.Params)
{
// TODO: Normalization of order, case?
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);
}
}
}
return builder.ToString();
}
finally
{
_builderPool.Return(builder);
}
}
internal bool RequestIsCacheable()
{
// Use optional override if specified by user
switch(_cacheabilityValidator.RequestIsCacheableOverride(_httpContext))
{
case OverrideResult.UseDefaultLogic:
break;
case OverrideResult.DoNotCache:
return false;
case OverrideResult.Cache:
return true;
default:
throw new NotSupportedException($"Unrecognized result from {nameof(_cacheabilityValidator.RequestIsCacheableOverride)}.");
}
// 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;
if (string.Equals("GET", request.Method, StringComparison.OrdinalIgnoreCase))
{
_responseType = ResponseType.FullReponse;
}
else if (string.Equals("HEAD", request.Method, StringComparison.OrdinalIgnoreCase))
{
_responseType = ResponseType.HeadersOnly;
}
else
{
return false;
}
// Verify existence of authorization headers
// TODO: The server may indicate that the response to these request are cacheable
if (!string.IsNullOrEmpty(request.Headers[HeaderNames.Authorization]))
{
return false;
}
// Verify request cache-control parameters
// TODO: no-cache requests can be retrieved upon validation with origin
if (!string.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl]))
{
if (RequestCacheControl.NoCache)
{
return false;
}
}
else
{
// Support for legacy HTTP 1.0 cache directive
var pragmaHeaderValues = request.Headers[HeaderNames.Pragma];
foreach (var directive in pragmaHeaderValues)
{
if (string.Equals("no-cache", directive, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
}
// TODO: Verify global middleware settings? Explicit ignore list, range requests, etc.
return true;
}
internal bool ResponseIsCacheable()
{
// Use optional override if specified by user
switch (_cacheabilityValidator.ResponseIsCacheableOverride(_httpContext))
{
case OverrideResult.UseDefaultLogic:
break;
case OverrideResult.DoNotCache:
return false;
case OverrideResult.Cache:
return true;
default:
throw new NotSupportedException($"Unrecognized result from {nameof(_cacheabilityValidator.ResponseIsCacheableOverride)}.");
}
// Only cache pages explicitly marked with public
// TODO: Consider caching responses that are not marked as public but otherwise cacheable?
if (!ResponseCacheControl.Public)
{
return false;
}
// Check no-store
if (RequestCacheControl.NoStore || ResponseCacheControl.NoStore)
{
return false;
}
// Check no-cache
// TODO: Handle no-cache with headers
if (ResponseCacheControl.NoCache)
{
return false;
}
var response = _httpContext.Response;
// Do not cache responses varying by *
if (string.Equals(response.Headers[HeaderNames.Vary], "*", StringComparison.OrdinalIgnoreCase))
{
return false;
}
// TODO: public MAY override the cacheability checks for private and status codes
// Check private
if (ResponseCacheControl.Private)
{
return false;
}
// Check response code
// TODO: RFC also lists 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 as cacheable by default
if (response.StatusCode != StatusCodes.Status200OK)
{
return false;
}
// Check response freshness
// TODO: apparent age vs corrected age value
var responseAge = _responseTime - ResponseHeaders.Date ?? TimeSpan.Zero;
if (!EntryIsFresh(ResponseHeaders, responseAge, verifyAgainstRequest: false))
{
return false;
}
return true;
}
internal bool EntryIsFresh(ResponseHeaders responseHeaders, TimeSpan age, bool verifyAgainstRequest)
{
var responseCacheControl = responseHeaders.CacheControl ?? EmptyCacheControl;
// Add min-fresh requirements
if (verifyAgainstRequest)
{
age += RequestCacheControl.MinFresh ?? TimeSpan.Zero;
}
// Validate shared max age, this overrides any max age settings for shared caches
if (age > responseCacheControl.SharedMaxAge)
{
// shared max age implies must revalidate
return false;
}
else if (responseCacheControl.SharedMaxAge == null)
{
// Validate max age
if (age > responseCacheControl.MaxAge || (verifyAgainstRequest && age > RequestCacheControl.MaxAge))
{
// Must revalidate
if (responseCacheControl.MustRevalidate)
{
return false;
}
// Request allows stale values
if (verifyAgainstRequest && age < RequestCacheControl.MaxStaleLimit)
{
// TODO: Add warning header indicating the response is stale
return true;
}
return false;
}
else if (responseCacheControl.MaxAge == null && (!verifyAgainstRequest || RequestCacheControl.MaxAge == null))
{
// Validate expiration
if (_responseTime > responseHeaders.Expires)
{
return false;
}
}
}
return true;
}
internal async Task<bool> TryServeFromCacheAsync()
{
_cacheKey = CreateCacheKey();
var cacheEntry = _cache.Get(_cacheKey);
State.BaseKey = _keyProvider.CreateBaseKey(_httpContext);
var cacheEntry = _cache.Get(State.BaseKey);
var responseServed = false;
if (cacheEntry is CachedVaryBy)
if (cacheEntry is CachedVaryRules)
{
// Request contains VaryBy rules, recompute key and try again
_cacheKey = CreateCacheKey(cacheEntry as CachedVaryBy);
cacheEntry = _cache.Get(_cacheKey);
// Request contains vary rules, recompute key and try again
var varyKey = _keyProvider.CreateVaryKey(_httpContext, ((CachedVaryRules)cacheEntry).VaryRules);
cacheEntry = _cache.Get(varyKey);
}
if (cacheEntry is CachedResponse)
@ -429,17 +77,18 @@ namespace Microsoft.AspNetCore.ResponseCaching
var cachedResponse = cacheEntry as CachedResponse;
var cachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers);
_responseTime = _options.SystemClock.UtcNow;
var age = _responseTime - cachedResponse.Created;
age = age > TimeSpan.Zero ? age : TimeSpan.Zero;
State.ResponseTime = _options.SystemClock.UtcNow;
var cachedEntryAge = State.ResponseTime - cachedResponse.Created;
State.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero;
if (EntryIsFresh(cachedResponseHeaders, age, verifyAgainstRequest: true))
if (_cacheabilityValidator.CachedEntryIsFresh(_httpContext, cachedResponseHeaders))
{
responseServed = true;
// Check conditional request rules
if (ConditionalRequestSatisfied(cachedResponseHeaders))
{
_httpContext.Response.StatusCode = StatusCodes.Status304NotModified;
responseServed = true;
}
else
{
@ -451,33 +100,20 @@ namespace Microsoft.AspNetCore.ResponseCaching
response.Headers.Add(header);
}
response.Headers[HeaderNames.Age] = age.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture);
response.Headers[HeaderNames.Age] = State.CachedEntryAge.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture);
if (_responseType == ResponseType.HeadersOnly)
{
responseServed = true;
}
else if (_responseType == ResponseType.FullReponse)
{
// Copy the cached response body
var body = cachedResponse.Body;
var body = cachedResponse.Body;
// Copy the cached response body
if (body.Length > 0)
{
// Add a content-length if required
if (response.ContentLength == null && string.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding]))
if (response.ContentLength == null && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding]))
{
response.ContentLength = body.Length;
}
if (body.Length > 0)
{
await response.Body.WriteAsync(body, 0, body.Length);
}
responseServed = true;
}
else
{
throw new InvalidOperationException($"{nameof(_responseType)} not specified or is unrecognized.");
await response.Body.WriteAsync(body, 0, body.Length);
}
}
}
@ -487,7 +123,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
}
}
if (!responseServed && RequestCacheControl.OnlyIfCached)
if (!responseServed && State.RequestCacheControl.OnlyIfCached)
{
_httpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
responseServed = true;
@ -498,7 +134,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
internal bool ConditionalRequestSatisfied(ResponseHeaders cachedResponseHeaders)
{
var ifNoneMatchHeader = RequestHeaders.IfNoneMatch;
var ifNoneMatchHeader = State.RequestHeaders.IfNoneMatch;
if (ifNoneMatchHeader != null)
{
@ -518,7 +154,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
}
}
}
else if ((cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= RequestHeaders.IfUnmodifiedSince)
else if ((cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= State.RequestHeaders.IfUnmodifiedSince)
{
return true;
}
@ -528,57 +164,56 @@ namespace Microsoft.AspNetCore.ResponseCaching
internal void FinalizeCachingHeaders()
{
if (CacheResponse)
if (_cacheabilityValidator.ResponseIsCacheable(_httpContext))
{
State.ShouldCacheResponse = true;
// Create the cache entry now
var response = _httpContext.Response;
var varyHeaderValue = response.Headers[HeaderNames.Vary];
var varyParamsValue = _httpContext.GetResponseCachingFeature().VaryByParams;
_cachedResponseValidFor = ResponseCacheControl.SharedMaxAge
?? ResponseCacheControl.MaxAge
?? (ResponseHeaders.Expires - _responseTime)
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 VaryBy rules exist
// Check if any vary rules exist
if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue))
{
if (varyParamsValue.Count > 1)
var cachedVaryRules = new CachedVaryRules
{
Array.Sort(varyParamsValue.ToArray(), StringComparer.OrdinalIgnoreCase);
}
var cachedVaryBy = new CachedVaryBy
{
// TODO: VaryBy Encoding
Headers = varyHeaderValue,
Params = varyParamsValue
VaryRules = new VaryRules()
{
// TODO: Vary Encoding
Headers = varyHeaderValue,
Params = varyParamsValue
}
};
// TODO: Overwrite?
_cache.Set(_cacheKey, cachedVaryBy, _cachedResponseValidFor);
_cacheKey = CreateCacheKey(cachedVaryBy);
_cache.Set(State.BaseKey, cachedVaryRules, State.CachedResponseValidFor);
State.VaryKey = _keyProvider.CreateVaryKey(_httpContext, cachedVaryRules.VaryRules);
}
// Ensure date header is set
if (ResponseHeaders.Date == null)
if (State.ResponseHeaders.Date == null)
{
ResponseHeaders.Date = _responseTime;
State.ResponseHeaders.Date = State.ResponseTime;
}
// Store the response to cache
_cachedResponse = new CachedResponse
State.CachedResponse = new CachedResponse
{
Created = ResponseHeaders.Date.Value,
Created = State.ResponseHeaders.Date.Value,
StatusCode = _httpContext.Response.StatusCode
};
foreach (var header in ResponseHeaders.Headers)
foreach (var header in State.ResponseHeaders.Headers)
{
if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase)
&& !string.Equals(header.Key, HeaderNames.SetCookie, StringComparison.OrdinalIgnoreCase))
if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase))
{
_cachedResponse.Headers.Add(header);
State.CachedResponse.Headers.Add(header);
}
}
}
@ -590,11 +225,11 @@ namespace Microsoft.AspNetCore.ResponseCaching
internal void FinalizeCachingBody()
{
if (CacheResponse && ResponseCacheStream.BufferingEnabled)
if (State.ShouldCacheResponse && ResponseCacheStream.BufferingEnabled)
{
_cachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray();
State.CachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray();
_cache.Set(_cacheKey, _cachedResponse, _cachedResponseValidFor);
_cache.Set(State.VaryKey ?? State.BaseKey, State.CachedResponse, State.CachedResponseValidFor);
}
}
@ -603,7 +238,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
if (!ResponseStarted)
{
ResponseStarted = true;
_responseTime = _options.SystemClock.UtcNow;
State.ResponseTime = _options.SystemClock.UtcNow;
FinalizeCachingHeaders();
}
@ -640,11 +275,5 @@ namespace Microsoft.AspNetCore.ResponseCaching
// TODO: Move this temporary interface with endpoint to HttpAbstractions
_httpContext.RemoveResponseCachingFeature();
}
private enum ResponseType
{
HeadersOnly = 0,
FullReponse = 1
}
}
}

View File

@ -8,6 +8,6 @@ namespace Microsoft.AspNetCore.ResponseCaching
// TODO: Temporary interface for endpoints to specify options for response caching
public class ResponseCachingFeature
{
public StringValues VaryByParams { get; set; }
public StringValues VaryParams { get; set; }
}
}

View File

@ -2,12 +2,11 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.ResponseCaching
@ -23,17 +22,15 @@ namespace Microsoft.AspNetCore.ResponseCaching
private readonly RequestDelegate _next;
private readonly IResponseCache _cache;
private readonly ResponseCachingOptions _options;
private readonly ObjectPool<StringBuilder> _builderPool;
private readonly IResponseCachingCacheabilityValidator _cacheabilityValidator;
private readonly IResponseCachingCacheKeyModifier _cacheKeyModifier;
private readonly ICacheabilityValidator _cacheabilityValidator;
private readonly IKeyProvider _keyProvider;
public ResponseCachingMiddleware(
RequestDelegate next,
IResponseCache cache,
IOptions<ResponseCachingOptions> options,
ObjectPoolProvider poolProvider,
IResponseCachingCacheabilityValidator cacheabilityValidator,
IResponseCachingCacheKeyModifier cacheKeyModifier)
ICacheabilityValidator cacheabilityValidator,
IKeyProvider keyProvider)
{
if (next == null)
{
@ -47,71 +44,74 @@ namespace Microsoft.AspNetCore.ResponseCaching
{
throw new ArgumentNullException(nameof(options));
}
if (poolProvider == null)
{
throw new ArgumentNullException(nameof(poolProvider));
}
if (cacheabilityValidator == null)
{
throw new ArgumentNullException(nameof(cacheabilityValidator));
}
if (cacheKeyModifier == null)
if (keyProvider == null)
{
throw new ArgumentNullException(nameof(cacheKeyModifier));
throw new ArgumentNullException(nameof(keyProvider));
}
_next = next;
_cache = cache;
_options = options.Value;
_builderPool = poolProvider.CreateStringBuilderPool();
_cacheabilityValidator = cacheabilityValidator;
_cacheKeyModifier = cacheKeyModifier;
_keyProvider = keyProvider;
}
public async Task Invoke(HttpContext context)
{
var cachingContext = new ResponseCachingContext(
context,
_cache,
_options,
_builderPool,
_cacheabilityValidator,
_cacheKeyModifier);
context.AddResponseCachingState();
// Should we attempt any caching logic?
if (cachingContext.RequestIsCacheable())
try
{
// Can this request be served from cache?
if (await cachingContext.TryServeFromCacheAsync())
var cachingContext = new ResponseCachingContext(
context,
_cache,
_options,
_cacheabilityValidator,
_keyProvider);
// Should we attempt any caching logic?
if (_cacheabilityValidator.RequestIsCacheable(context))
{
return;
// Can this request be served from cache?
if (await cachingContext.TryServeFromCacheAsync())
{
return;
}
// Hook up to listen to the response stream
cachingContext.ShimResponseStream();
try
{
// Subscribe to OnStarting event
context.Response.OnStarting(OnStartingCallback, cachingContext);
await _next(context);
// If there was no response body, check the response headers now. We can cache things like redirects.
cachingContext.OnResponseStarting();
// Finalize the cache entry
cachingContext.FinalizeCachingBody();
}
finally
{
cachingContext.UnshimResponseStream();
}
}
// Hook up to listen to the response stream
cachingContext.ShimResponseStream();
try
else
{
// Subscribe to OnStarting event
context.Response.OnStarting(OnStartingCallback, cachingContext);
// TODO: Invalidate resources for successful unsafe methods? Required by RFC
await _next(context);
// If there was no response body, check the response headers now. We can cache things like redirects.
cachingContext.OnResponseStarting();
// Finalize the cache entry
cachingContext.FinalizeCachingBody();
}
finally
{
cachingContext.UnshimResponseStream();
}
}
else
finally
{
// TODO: Invalidate resources for successful unsafe methods? Required by RFC
await _next(context);
context.RemoveResponseCachingState();
}
}
}

View File

@ -3,9 +3,9 @@
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.ResponseCaching.Internal
namespace Microsoft.AspNetCore.ResponseCaching
{
internal class CachedVaryBy
public class VaryRules
{
internal StringValues Headers { get; set; }
internal StringValues Params { get; set; }

View File

@ -0,0 +1,157 @@
// 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.Linq;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.ResponseCaching.Internal;
using Xunit;
namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
public class CacheEntrySerializerTests
{
[Fact]
public void Serialize_NullObject_Throws()
{
Assert.Throws<ArgumentNullException>(() => CacheEntrySerializer.Serialize(null));
}
[Fact]
public void Serialize_UnknownObject_Throws()
{
Assert.Throws<NotSupportedException>(() => CacheEntrySerializer.Serialize(new object()));
}
[Fact]
public void RoundTrip_CachedResponses_Succeeds()
{
var headers = new HeaderDictionary();
headers["keyA"] = "valueA";
headers["keyB"] = "valueB";
var cachedEntry = new CachedResponse()
{
Created = DateTimeOffset.UtcNow,
StatusCode = StatusCodes.Status200OK,
Body = Encoding.ASCII.GetBytes("Hello world"),
Headers = headers
};
AssertCachedResponsesEqual(cachedEntry, (CachedResponse)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedEntry)));
}
[Fact]
public void RoundTrip_Empty_CachedVaryRules_Succeeds()
{
var cachedVaryRules = new CachedVaryRules();
AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules)));
}
[Fact]
public void RoundTrip_CachedVaryRules_EmptyRules_Succeeds()
{
var cachedVaryRules = new CachedVaryRules()
{
VaryRules = new VaryRules()
};
AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules)));
}
[Fact]
public void RoundTrip_HeadersOnly_CachedVaryRules_Succeeds()
{
var headers = new[] { "headerA", "headerB" };
var cachedVaryRules = new CachedVaryRules()
{
VaryRules = new VaryRules()
{
Headers = headers
}
};
AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules)));
}
[Fact]
public void RoundTrip_ParamsOnly_CachedVaryRules_Succeeds()
{
var param = new[] { "paramA", "paramB" };
var cachedVaryRules = new CachedVaryRules()
{
VaryRules = new VaryRules()
{
Params = param
}
};
AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules)));
}
[Fact]
public void RoundTrip_HeadersAndParams_CachedVaryRules_Succeeds()
{
var headers = new[] { "headerA", "headerB" };
var param = new[] { "paramA", "paramB" };
var cachedVaryRules = new CachedVaryRules()
{
VaryRules = new VaryRules()
{
Headers = headers,
Params = param
}
};
AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules)));
}
[Fact]
public void Deserialize_InvalidEntries_ReturnsNull()
{
var headers = new[] { "headerA", "headerB" };
var cachedVaryRules = new CachedVaryRules()
{
VaryRules = new VaryRules()
{
Headers = headers
}
};
var serializedEntry = CacheEntrySerializer.Serialize(cachedVaryRules);
Array.Reverse(serializedEntry);
Assert.Null(CacheEntrySerializer.Deserialize(serializedEntry));
}
private static void AssertCachedResponsesEqual(CachedResponse expected, CachedResponse actual)
{
Assert.NotNull(actual);
Assert.NotNull(expected);
Assert.Equal(expected.Created, actual.Created);
Assert.Equal(expected.StatusCode, actual.StatusCode);
Assert.Equal(expected.Headers.Count, actual.Headers.Count);
foreach (var expectedHeader in expected.Headers)
{
Assert.Equal(expectedHeader.Value, actual.Headers[expectedHeader.Key]);
}
Assert.True(expected.Body.SequenceEqual(actual.Body));
}
private static void AssertCachedVaryRulesEqual(CachedVaryRules expected, CachedVaryRules actual)
{
Assert.NotNull(actual);
Assert.NotNull(expected);
if (expected.VaryRules == null)
{
Assert.Null(actual.VaryRules);
}
else
{
Assert.NotNull(actual.VaryRules);
Assert.Equal(expected.VaryRules.Headers, actual.VaryRules.Headers);
Assert.Equal(expected.VaryRules.Params, actual.VaryRules.Params);
}
}
}
}

View File

@ -0,0 +1,624 @@
// 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.AspNetCore.ResponseCaching.Internal;
using Microsoft.Net.Http.Headers;
using Xunit;
namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
public class CacheabilityValidatorTests
{
[Theory]
[InlineData("GET")]
[InlineData("HEAD")]
public void RequestIsCacheable_CacheableMethods_Allowed(string method)
{
var httpContext = CreateDefaultContext();
httpContext.Request.Method = method;
Assert.True(new CacheabilityValidator().RequestIsCacheable(httpContext));
}
[Theory]
[InlineData("POST")]
[InlineData("OPTIONS")]
[InlineData("PUT")]
[InlineData("DELETE")]
[InlineData("TRACE")]
[InlineData("CONNECT")]
[InlineData("")]
[InlineData(null)]
public void RequestIsCacheable_UncacheableMethods_NotAllowed(string method)
{
var httpContext = CreateDefaultContext();
httpContext.Request.Method = method;
Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext));
}
[Fact]
public void RequestIsCacheable_AuthorizationHeaders_NotAllowed()
{
var httpContext = CreateDefaultContext();
httpContext.Request.Method = "GET";
httpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW";
Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext));
}
[Fact]
public void RequestIsCacheable_NoCache_NotAllowed()
{
var httpContext = CreateDefaultContext();
httpContext.Request.Method = "GET";
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
NoCache = true
};
Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext));
}
[Fact]
public void RequestIsCacheable_NoStore_Allowed()
{
var httpContext = CreateDefaultContext();
httpContext.Request.Method = "GET";
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
NoStore = true
};
Assert.True(new CacheabilityValidator().RequestIsCacheable(httpContext));
}
[Fact]
public void RequestIsCacheable_LegacyDirectives_NotAllowed()
{
var httpContext = CreateDefaultContext();
httpContext.Request.Method = "GET";
httpContext.Request.Headers[HeaderNames.Pragma] = "no-cache";
Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext));
}
[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";
Assert.True(new CacheabilityValidator().RequestIsCacheable(httpContext));
}
[Fact]
public void ResponseIsCacheable_NoPublic_NotAllowed()
{
var httpContext = CreateDefaultContext();
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
}
[Fact]
public void ResponseIsCacheable_Public_Allowed()
{
var httpContext = CreateDefaultContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true
};
Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext));
}
[Fact]
public void ResponseIsCacheable_NoCache_NotAllowed()
{
var httpContext = CreateDefaultContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true,
NoCache = true
};
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
}
[Fact]
public void ResponseIsCacheable_RequestNoStore_NotAllowed()
{
var httpContext = CreateDefaultContext();
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
NoStore = true
};
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true
};
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
}
[Fact]
public void ResponseIsCacheable_ResponseNoStore_NotAllowed()
{
var httpContext = CreateDefaultContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true,
NoStore = true
};
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
}
[Fact]
public void ResponseIsCacheable_SetCookieHeader_NotAllowed()
{
var httpContext = CreateDefaultContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true
};
httpContext.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue";
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
}
[Fact]
public void ResponseIsCacheable_VaryHeaderByStar_NotAllowed()
{
var httpContext = CreateDefaultContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true
};
httpContext.Response.Headers[HeaderNames.Vary] = "*";
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
}
[Fact]
public void ResponseIsCacheable_Private_NotAllowed()
{
var httpContext = CreateDefaultContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true,
Private = true
};
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
}
[Theory]
[InlineData(StatusCodes.Status200OK)]
public void ResponseIsCacheable_SuccessStatusCodes_Allowed(int statusCode)
{
var httpContext = CreateDefaultContext();
httpContext.Response.StatusCode = statusCode;
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true
};
Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext));
}
[Theory]
[InlineData(StatusCodes.Status201Created)]
[InlineData(StatusCodes.Status202Accepted)]
[InlineData(StatusCodes.Status203NonAuthoritative)]
[InlineData(StatusCodes.Status204NoContent)]
[InlineData(StatusCodes.Status205ResetContent)]
[InlineData(StatusCodes.Status206PartialContent)]
[InlineData(StatusCodes.Status207MultiStatus)]
[InlineData(StatusCodes.Status300MultipleChoices)]
[InlineData(StatusCodes.Status301MovedPermanently)]
[InlineData(StatusCodes.Status302Found)]
[InlineData(StatusCodes.Status303SeeOther)]
[InlineData(StatusCodes.Status304NotModified)]
[InlineData(StatusCodes.Status305UseProxy)]
[InlineData(StatusCodes.Status306SwitchProxy)]
[InlineData(StatusCodes.Status307TemporaryRedirect)]
[InlineData(StatusCodes.Status308PermanentRedirect)]
[InlineData(StatusCodes.Status400BadRequest)]
[InlineData(StatusCodes.Status401Unauthorized)]
[InlineData(StatusCodes.Status402PaymentRequired)]
[InlineData(StatusCodes.Status403Forbidden)]
[InlineData(StatusCodes.Status404NotFound)]
[InlineData(StatusCodes.Status405MethodNotAllowed)]
[InlineData(StatusCodes.Status406NotAcceptable)]
[InlineData(StatusCodes.Status407ProxyAuthenticationRequired)]
[InlineData(StatusCodes.Status408RequestTimeout)]
[InlineData(StatusCodes.Status409Conflict)]
[InlineData(StatusCodes.Status410Gone)]
[InlineData(StatusCodes.Status411LengthRequired)]
[InlineData(StatusCodes.Status412PreconditionFailed)]
[InlineData(StatusCodes.Status413RequestEntityTooLarge)]
[InlineData(StatusCodes.Status414RequestUriTooLong)]
[InlineData(StatusCodes.Status415UnsupportedMediaType)]
[InlineData(StatusCodes.Status416RequestedRangeNotSatisfiable)]
[InlineData(StatusCodes.Status417ExpectationFailed)]
[InlineData(StatusCodes.Status418ImATeapot)]
[InlineData(StatusCodes.Status419AuthenticationTimeout)]
[InlineData(StatusCodes.Status422UnprocessableEntity)]
[InlineData(StatusCodes.Status423Locked)]
[InlineData(StatusCodes.Status424FailedDependency)]
[InlineData(StatusCodes.Status451UnavailableForLegalReasons)]
[InlineData(StatusCodes.Status500InternalServerError)]
[InlineData(StatusCodes.Status501NotImplemented)]
[InlineData(StatusCodes.Status502BadGateway)]
[InlineData(StatusCodes.Status503ServiceUnavailable)]
[InlineData(StatusCodes.Status504GatewayTimeout)]
[InlineData(StatusCodes.Status505HttpVersionNotsupported)]
[InlineData(StatusCodes.Status506VariantAlsoNegotiates)]
[InlineData(StatusCodes.Status507InsufficientStorage)]
public void ResponseIsCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode)
{
var httpContext = CreateDefaultContext();
httpContext.Response.StatusCode = statusCode;
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true
};
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
}
[Fact]
public void ResponseIsCacheable_NoExpiryRequirements_IsAllowed()
{
var httpContext = CreateDefaultContext();
httpContext.Response.StatusCode = StatusCodes.Status200OK;
var headers = httpContext.Response.GetTypedHeaders();
headers.CacheControl = new CacheControlHeaderValue()
{
Public = true
};
var utcNow = DateTimeOffset.UtcNow;
headers.Date = utcNow;
httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue;
Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext));
}
[Fact]
public void ResponseIsCacheable_PastExpiry_NotAllowed()
{
var httpContext = CreateDefaultContext();
httpContext.Response.StatusCode = StatusCodes.Status200OK;
var headers = httpContext.Response.GetTypedHeaders();
headers.CacheControl = new CacheControlHeaderValue()
{
Public = true
};
var utcNow = DateTimeOffset.UtcNow;
headers.Expires = utcNow;
headers.Date = utcNow;
httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue;
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
}
[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()
{
Public = true,
MaxAge = TimeSpan.FromSeconds(10)
};
headers.Expires = utcNow;
headers.Date = utcNow;
httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(9);
Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext));
}
[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()
{
Public = true,
MaxAge = TimeSpan.FromSeconds(10)
};
headers.Expires = utcNow;
headers.Date = utcNow;
httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(11);
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
}
[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()
{
Public = true,
MaxAge = TimeSpan.FromSeconds(10),
SharedMaxAge = TimeSpan.FromSeconds(15)
};
headers.Date = utcNow;
httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(11);
Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext));
}
[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()
{
Public = true,
MaxAge = TimeSpan.FromSeconds(10),
SharedMaxAge = TimeSpan.FromSeconds(5)
};
headers.Date = utcNow;
httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(6);
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
}
[Fact]
public void EntryIsFresh_NoExpiryRequirements_IsFresh()
{
var utcNow = DateTimeOffset.UtcNow;
var httpContext = CreateDefaultContext();
httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue;
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
{
CacheControl = new CacheControlHeaderValue()
{
Public = true
}
};
Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
}
[Fact]
public void EntryIsFresh_PastExpiry_IsNotFresh()
{
var httpContext = CreateDefaultContext();
httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue;
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
{
CacheControl = new CacheControlHeaderValue()
{
Public = true
},
Expires = DateTimeOffset.UtcNow
};
Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
}
[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())
{
CacheControl = new CacheControlHeaderValue()
{
Public = true,
MaxAge = TimeSpan.FromSeconds(10)
},
Expires = utcNow
};
Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
}
[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())
{
CacheControl = new CacheControlHeaderValue()
{
Public = true,
MaxAge = TimeSpan.FromSeconds(10)
},
Expires = utcNow
};
Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
}
[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())
{
CacheControl = new CacheControlHeaderValue()
{
Public = true,
MaxAge = TimeSpan.FromSeconds(10),
SharedMaxAge = TimeSpan.FromSeconds(15)
},
Expires = utcNow
};
Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
}
[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())
{
CacheControl = new CacheControlHeaderValue()
{
Public = true,
MaxAge = TimeSpan.FromSeconds(10),
SharedMaxAge = TimeSpan.FromSeconds(5)
},
Expires = utcNow
};
Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
}
[Fact]
public void EntryIsFresh_MinFreshReducesFreshness_ToNotFresh()
{
var httpContext = CreateDefaultContext();
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
MinFresh = TimeSpan.FromSeconds(3)
};
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
{
CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(10),
SharedMaxAge = TimeSpan.FromSeconds(5)
}
};
httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(3);
Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
}
[Fact]
public void EntryIsFresh_RequestMaxAgeRestrictAge_ToNotFresh()
{
var httpContext = CreateDefaultContext();
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(5)
};
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
{
CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(10),
}
};
httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(6);
Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
}
[Fact]
public void EntryIsFresh_MaxStaleOverridesFreshness_ToFresh()
{
var httpContext = CreateDefaultContext();
httpContext.Request.GetTypedHeaders().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())
{
CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(5),
}
};
httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(6);
Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
}
[Fact]
public void EntryIsFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh()
{
var httpContext = CreateDefaultContext();
httpContext.Request.GetTypedHeaders().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())
{
CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(5),
MustRevalidate = true
}
};
httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(6);
Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
}
[Fact]
public void EntryIsFresh_IgnoresRequestVerificationWhenSpecified()
{
var httpContext = CreateDefaultContext();
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
MinFresh = TimeSpan.FromSeconds(1),
MaxAge = TimeSpan.FromSeconds(3)
};
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
{
CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(10),
SharedMaxAge = TimeSpan.FromSeconds(5)
}
};
httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(3);
Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
}
private static HttpContext CreateDefaultContext()
{
var context = new DefaultHttpContext();
context.AddResponseCachingState();
return context;
}
}
}

View File

@ -1,126 +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.Linq;
using Microsoft.AspNetCore.ResponseCaching.Internal;
using Xunit;
using Microsoft.AspNetCore.Http;
using System.Text;
namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
public class DefaultResponseCacheEntrySerializerTests
{
[Fact]
public void Serialize_NullObject_Throws()
{
Assert.Throws<ArgumentNullException>(() => DefaultResponseCacheSerializer.Serialize(null));
}
[Fact]
public void Serialize_UnknownObject_Throws()
{
Assert.Throws<NotSupportedException>(() => DefaultResponseCacheSerializer.Serialize(new object()));
}
[Fact]
public void RoundTrip_CachedResponses_Succeeds()
{
var headers = new HeaderDictionary();
headers["keyA"] = "valueA";
headers["keyB"] = "valueB";
var cachedEntry = new CachedResponse()
{
Created = DateTimeOffset.UtcNow,
StatusCode = StatusCodes.Status200OK,
Body = Encoding.ASCII.GetBytes("Hello world"),
Headers = headers
};
AssertCachedResponsesEqual(cachedEntry, (CachedResponse)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedEntry)));
}
[Fact]
public void RoundTrip_Empty_CachedVaryBy_Succeeds()
{
var cachedVaryBy = new CachedVaryBy();
AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy)));
}
[Fact]
public void RoundTrip_HeadersOnly_CachedVaryBy_Succeeds()
{
var headers = new[] { "headerA", "headerB" };
var cachedVaryBy = new CachedVaryBy()
{
Headers = headers
};
AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy)));
}
[Fact]
public void RoundTrip_ParamsOnly_CachedVaryBy_Succeeds()
{
var param = new[] { "paramA", "paramB" };
var cachedVaryBy = new CachedVaryBy()
{
Params = param
};
AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy)));
}
[Fact]
public void RoundTrip_HeadersAndParams_CachedVaryBy_Succeeds()
{
var headers = new[] { "headerA", "headerB" };
var param = new[] { "paramA", "paramB" };
var cachedVaryBy = new CachedVaryBy()
{
Headers = headers,
Params = param
};
AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy)));
}
[Fact]
public void Deserialize_InvalidEntries_ReturnsNull()
{
var headers = new[] { "headerA", "headerB" };
var cachedVaryBy = new CachedVaryBy()
{
Headers = headers
};
var serializedEntry = DefaultResponseCacheSerializer.Serialize(cachedVaryBy);
Array.Reverse(serializedEntry);
Assert.Null(DefaultResponseCacheSerializer.Deserialize(serializedEntry));
}
private static void AssertCachedResponsesEqual(CachedResponse expected, CachedResponse actual)
{
Assert.NotNull(actual);
Assert.NotNull(expected);
Assert.Equal(expected.Created, actual.Created);
Assert.Equal(expected.StatusCode, actual.StatusCode);
Assert.Equal(expected.Headers.Count, actual.Headers.Count);
foreach (var expectedHeader in expected.Headers)
{
Assert.Equal(expectedHeader.Value, actual.Headers[expectedHeader.Key]);
}
Assert.True(expected.Body.SequenceEqual(actual.Body));
}
private static void AssertCachedVarybyEqual(CachedVaryBy expected, CachedVaryBy actual)
{
Assert.NotNull(actual);
Assert.NotNull(expected);
Assert.Equal(expected.Headers, actual.Headers);
Assert.Equal(expected.Params, actual.Params);
}
}
}

View File

@ -0,0 +1,37 @@
// 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.ResponseCaching.Internal;
using Xunit;
namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
public class HttpContextInternalExtensionTests
{
[Fact]
public void AddingSecondResponseCachingFeature_Throws()
{
var httpContext = new DefaultHttpContext();
// Should not throw
httpContext.AddResponseCachingFeature();
// Should throw
Assert.ThrowsAny<InvalidOperationException>(() => httpContext.AddResponseCachingFeature());
}
[Fact]
public void AddingSecondResponseCachingState_Throws()
{
var httpContext = new DefaultHttpContext();
// Should not throw
httpContext.AddResponseCachingState();
// Should throw
Assert.ThrowsAny<InvalidOperationException>(() => httpContext.AddResponseCachingState());
}
}
}

View File

@ -0,0 +1,155 @@
// 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.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
public class DefaultKeyProviderTests
{
private static readonly char KeyDelimiter = '\x1e';
[Fact]
public void DefaultKeyProvider_CreateBaseKey_IncludesOnlyNormalizedMethodAndPath()
{
var httpContext = new DefaultHttpContext();
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();
Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", keyProvider.CreateBaseKey(httpContext));
}
[Fact]
public void DefaultKeyProvider_CreateBaseKey_CaseInsensitivePath_NormalizesPath()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.Path = "/Path";
var keyProvider = CreateTestKeyProvider(new ResponseCachingOptions()
{
CaseSensitivePaths = false
});
Assert.Equal($"GET{KeyDelimiter}/PATH", keyProvider.CreateBaseKey(httpContext));
}
[Fact]
public void DefaultKeyProvider_CreateBaseKey_CaseSensitivePath_PreservesPathCase()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.Path = "/Path";
var keyProvider = CreateTestKeyProvider(new ResponseCachingOptions()
{
CaseSensitivePaths = true
});
Assert.Equal($"GET{KeyDelimiter}/Path", keyProvider.CreateBaseKey(httpContext));
}
[Fact]
public void DefaultKeyProvider_CreateVaryKey_IncludesListedHeadersOnly()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.Path = "/";
httpContext.Request.Headers["HeaderA"] = "ValueA";
httpContext.Request.Headers["HeaderB"] = "ValueB";
var keyProvider = CreateTestKeyProvider();
Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null",
keyProvider.CreateVaryKey(httpContext, new VaryRules()
{
Headers = new string[] { "HeaderA", "HeaderC" }
}));
}
[Fact]
public void DefaultKeyProvider_CreateVaryKey_IncludesListedParamsOnly()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.Path = "/";
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
var keyProvider = CreateTestKeyProvider();
Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null",
keyProvider.CreateVaryKey(httpContext, new VaryRules()
{
Params = new string[] { "ParamA", "ParamC" }
}));
}
[Fact]
public void DefaultKeyProvider_CreateVaryKey_IncludesParams_ParamNameCaseInsensitive_UseParamCasing()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.Path = "/";
httpContext.Request.QueryString = new QueryString("?parama=ValueA&paramB=ValueB");
var keyProvider = CreateTestKeyProvider();
Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null",
keyProvider.CreateVaryKey(httpContext, new VaryRules()
{
Params = new string[] { "ParamA", "ParamC" }
}));
}
[Fact]
public void DefaultKeyProvider_CreateVaryKey_IncludesAllQueryParamsGivenAsterisk()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.Path = "/";
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
var keyProvider = CreateTestKeyProvider();
// To support case insensitivity, all param keys are converted to upper case.
// Explicit params uses the casing specified in the setting.
Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB",
keyProvider.CreateVaryKey(httpContext, new VaryRules()
{
Params = new string[] { "*" }
}));
}
[Fact]
public void DefaultKeyProvider_CreateVaryKey_IncludesListedHeadersAndParams()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.Path = "/";
httpContext.Request.Headers["HeaderA"] = "ValueA";
httpContext.Request.Headers["HeaderB"] = "ValueB";
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
var keyProvider = CreateTestKeyProvider();
Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null",
keyProvider.CreateVaryKey(httpContext, new VaryRules()
{
Headers = new string[] { "HeaderA", "HeaderC" },
Params = new string[] { "ParamA", "ParamC" }
}));
}
private static IKeyProvider CreateTestKeyProvider()
{
return CreateTestKeyProvider(new ResponseCachingOptions());
}
private static IKeyProvider CreateTestKeyProvider(ResponseCachingOptions options)
{
return new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options));
}
}
}

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
@ -12,6 +11,7 @@ using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Xunit;
@ -19,708 +19,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
public class ResponseCachingContextTests
{
private static readonly char KeyDelimiter = '\x1e';
[Theory]
[InlineData("GET")]
[InlineData("HEAD")]
public void RequestIsCacheable_CacheableMethods_Allowed(string method)
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = method;
var context = CreateTestContext(httpContext);
Assert.True(context.RequestIsCacheable());
}
[Theory]
[InlineData("POST")]
[InlineData("OPTIONS")]
[InlineData("PUT")]
[InlineData("DELETE")]
[InlineData("TRACE")]
[InlineData("CONNECT")]
[InlineData("")]
[InlineData(null)]
public void RequestIsCacheable_UncacheableMethods_NotAllowed(string method)
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = method;
var context = CreateTestContext(httpContext);
Assert.False(context.RequestIsCacheable());
}
[Fact]
public void RequestIsCacheable_AuthorizationHeaders_NotAllowed()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW";
var context = CreateTestContext(httpContext);
Assert.False(context.RequestIsCacheable());
}
[Fact]
public void RequestIsCacheable_NoCache_NotAllowed()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
NoCache = true
};
var context = CreateTestContext(httpContext);
Assert.False(context.RequestIsCacheable());
}
[Fact]
public void RequestIsCacheable_NoStore_Allowed()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
NoStore = true
};
var context = CreateTestContext(httpContext);
Assert.True(context.RequestIsCacheable());
}
[Fact]
public void RequestIsCacheable_LegacyDirectives_NotAllowed()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.Headers[HeaderNames.Pragma] = "no-cache";
var context = CreateTestContext(httpContext);
Assert.False(context.RequestIsCacheable());
}
[Fact]
public void RequestIsCacheable_LegacyDirectives_OverridenByCacheControl()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.Headers[HeaderNames.Pragma] = "no-cache";
httpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10";
var context = CreateTestContext(httpContext);
Assert.True(context.RequestIsCacheable());
}
private class AllowUnrecognizedHTTPMethodRequests : IResponseCachingCacheabilityValidator
{
public OverrideResult RequestIsCacheableOverride(HttpContext httpContext) =>
httpContext.Request.Method == "UNRECOGNIZED" ? OverrideResult.Cache : OverrideResult.DoNotCache;
public OverrideResult ResponseIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic;
}
[Fact]
public void RequestIsCacheableOverride_OverridesDefaultBehavior_ToAllowed()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "UNRECOGNIZED";
var responseCachingContext = CreateTestContext(httpContext, new AllowUnrecognizedHTTPMethodRequests());
Assert.True(responseCachingContext.RequestIsCacheable());
}
private class DisallowGetHTTPMethodRequests : IResponseCachingCacheabilityValidator
{
public OverrideResult RequestIsCacheableOverride(HttpContext httpContext) =>
httpContext.Request.Method == "GET" ? OverrideResult.DoNotCache : OverrideResult.Cache;
public OverrideResult ResponseIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic;
}
[Fact]
public void RequestIsCacheableOverride_OverridesDefaultBehavior_ToNotAllowed()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
var responseCachingContext = CreateTestContext(httpContext, new DisallowGetHTTPMethodRequests());
Assert.False(responseCachingContext.RequestIsCacheable());
}
[Fact]
public void RequestIsCacheableOverride_IgnoreFallsBackToDefaultBehavior()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
var responseCachingContext = CreateTestContext(httpContext, new NoopCacheabilityValidator());
Assert.True(responseCachingContext.RequestIsCacheable());
httpContext.Request.Method = "UNRECOGNIZED";
Assert.False(responseCachingContext.RequestIsCacheable());
}
[Fact]
public void CreateCacheKey_Includes_UppercaseMethodAndPath()
{
var httpContext = new DefaultHttpContext();
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 context = CreateTestContext(httpContext);
Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", context.CreateCacheKey());
}
[Fact]
public void CreateCacheKey_Includes_ListedVaryByHeadersOnly()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.Path = "/";
httpContext.Request.Headers["HeaderA"] = "ValueA";
httpContext.Request.Headers["HeaderB"] = "ValueB";
var context = CreateTestContext(httpContext);
Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null", context.CreateCacheKey(new CachedVaryBy()
{
Headers = new string[] { "HeaderA", "HeaderC" }
}));
}
[Fact]
public void CreateCacheKey_Includes_ListedVaryByParamsOnly()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.Path = "/";
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
var context = CreateTestContext(httpContext);
Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", context.CreateCacheKey(new CachedVaryBy()
{
Params = new string[] { "ParamA", "ParamC" }
}));
}
[Fact]
public void CreateCacheKey_Includes_VaryByParams_ParamNameCaseInsensitive_UseVaryByCasing()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.Path = "/";
httpContext.Request.QueryString = new QueryString("?parama=ValueA&paramB=ValueB");
var context = CreateTestContext(httpContext);
Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", context.CreateCacheKey(new CachedVaryBy()
{
Params = new string[] { "ParamA", "ParamC" }
}));
}
[Fact]
public void CreateCacheKey_Includes_AllQueryParamsGivenAsterisk()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.Path = "/";
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
var context = CreateTestContext(httpContext);
// To support case insensitivity, all param keys are converted to lower case.
// Explicit VaryBy uses the casing specified in the setting.
Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB", context.CreateCacheKey(new CachedVaryBy()
{
Params = new string[] { "*" }
}));
}
[Fact]
public void CreateCacheKey_Includes_ListedVaryByHeadersAndParams()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.Path = "/";
httpContext.Request.Headers["HeaderA"] = "ValueA";
httpContext.Request.Headers["HeaderB"] = "ValueB";
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
var context = CreateTestContext(httpContext);
Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", context.CreateCacheKey(new CachedVaryBy()
{
Headers = new string[] { "HeaderA", "HeaderC" },
Params = new string[] { "ParamA", "ParamC" }
}));
}
private class KeyModifier : IResponseCachingCacheKeyModifier
{
public string CreatKeyPrefix(HttpContext httpContext) => "CustomizedKeyPrefix";
}
[Fact]
public void CreateCacheKey_CacheKeyModifier_AddsPrefix()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.Path = "/";
httpContext.Request.Headers["HeaderA"] = "ValueA";
httpContext.Request.Headers["HeaderB"] = "ValueB";
var responseCachingContext = CreateTestContext(httpContext, new KeyModifier());
Assert.Equal($"CustomizedKeyPrefix{KeyDelimiter}GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null", responseCachingContext.CreateCacheKey(new CachedVaryBy()
{
Headers = new string[] { "HeaderA", "HeaderC" }
}));
}
[Fact]
public void CreateCacheKey_CaseInsensitivePath_NormalizesPath()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.Path = "/Path";
var context = CreateTestContext(httpContext, new ResponseCachingOptions()
{
CaseSensitivePaths = false
});
Assert.Equal($"GET{KeyDelimiter}/PATH", context.CreateCacheKey());
}
[Fact]
public void CreateCacheKey_CaseSensitivePath_PreservesPathCase()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.Method = "GET";
httpContext.Request.Path = "/Path";
var context = CreateTestContext(httpContext, new ResponseCachingOptions()
{
CaseSensitivePaths = true
});
Assert.Equal($"GET{KeyDelimiter}/Path", context.CreateCacheKey());
}
[Fact]
public void ResponseIsCacheable_NoPublic_NotAllowed()
{
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext);
Assert.False(context.ResponseIsCacheable());
}
[Fact]
public void ResponseIsCacheable_Public_Allowed()
{
var httpContext = new DefaultHttpContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true
};
var context = CreateTestContext(httpContext);
Assert.True(context.ResponseIsCacheable());
}
[Fact]
public void ResponseIsCacheable_NoCache_NotAllowed()
{
var httpContext = new DefaultHttpContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true,
NoCache = true
};
var context = CreateTestContext(httpContext);
Assert.False(context.ResponseIsCacheable());
}
[Fact]
public void ResponseIsCacheable_RequestNoStore_NotAllowed()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
NoStore = true
};
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true
};
var context = CreateTestContext(httpContext);
Assert.False(context.ResponseIsCacheable());
}
[Fact]
public void ResponseIsCacheable_ResponseNoStore_NotAllowed()
{
var httpContext = new DefaultHttpContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true,
NoStore = true
};
var context = CreateTestContext(httpContext);
Assert.False(context.ResponseIsCacheable());
}
[Fact]
public void ResponseIsCacheable_VaryByStar_NotAllowed()
{
var httpContext = new DefaultHttpContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true
};
httpContext.Response.Headers[HeaderNames.Vary] = "*";
var context = CreateTestContext(httpContext);
Assert.False(context.ResponseIsCacheable());
}
[Fact]
public void ResponseIsCacheable_Private_NotAllowed()
{
var httpContext = new DefaultHttpContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true,
Private = true
};
var context = CreateTestContext(httpContext);
Assert.False(context.ResponseIsCacheable());
}
[Theory]
[InlineData(StatusCodes.Status200OK)]
public void ResponseIsCacheable_SuccessStatusCodes_Allowed(int statusCode)
{
var httpContext = new DefaultHttpContext();
httpContext.Response.StatusCode = statusCode;
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true
};
var context = CreateTestContext(httpContext);
Assert.True(context.ResponseIsCacheable());
}
[Theory]
[InlineData(StatusCodes.Status201Created)]
[InlineData(StatusCodes.Status202Accepted)]
[InlineData(StatusCodes.Status203NonAuthoritative)]
[InlineData(StatusCodes.Status204NoContent)]
[InlineData(StatusCodes.Status205ResetContent)]
[InlineData(StatusCodes.Status206PartialContent)]
[InlineData(StatusCodes.Status207MultiStatus)]
[InlineData(StatusCodes.Status300MultipleChoices)]
[InlineData(StatusCodes.Status301MovedPermanently)]
[InlineData(StatusCodes.Status302Found)]
[InlineData(StatusCodes.Status303SeeOther)]
[InlineData(StatusCodes.Status304NotModified)]
[InlineData(StatusCodes.Status305UseProxy)]
[InlineData(StatusCodes.Status306SwitchProxy)]
[InlineData(StatusCodes.Status307TemporaryRedirect)]
[InlineData(StatusCodes.Status308PermanentRedirect)]
[InlineData(StatusCodes.Status400BadRequest)]
[InlineData(StatusCodes.Status401Unauthorized)]
[InlineData(StatusCodes.Status402PaymentRequired)]
[InlineData(StatusCodes.Status403Forbidden)]
[InlineData(StatusCodes.Status404NotFound)]
[InlineData(StatusCodes.Status405MethodNotAllowed)]
[InlineData(StatusCodes.Status406NotAcceptable)]
[InlineData(StatusCodes.Status407ProxyAuthenticationRequired)]
[InlineData(StatusCodes.Status408RequestTimeout)]
[InlineData(StatusCodes.Status409Conflict)]
[InlineData(StatusCodes.Status410Gone)]
[InlineData(StatusCodes.Status411LengthRequired)]
[InlineData(StatusCodes.Status412PreconditionFailed)]
[InlineData(StatusCodes.Status413RequestEntityTooLarge)]
[InlineData(StatusCodes.Status414RequestUriTooLong)]
[InlineData(StatusCodes.Status415UnsupportedMediaType)]
[InlineData(StatusCodes.Status416RequestedRangeNotSatisfiable)]
[InlineData(StatusCodes.Status417ExpectationFailed)]
[InlineData(StatusCodes.Status418ImATeapot)]
[InlineData(StatusCodes.Status419AuthenticationTimeout)]
[InlineData(StatusCodes.Status422UnprocessableEntity)]
[InlineData(StatusCodes.Status423Locked)]
[InlineData(StatusCodes.Status424FailedDependency)]
[InlineData(StatusCodes.Status451UnavailableForLegalReasons)]
[InlineData(StatusCodes.Status500InternalServerError)]
[InlineData(StatusCodes.Status501NotImplemented)]
[InlineData(StatusCodes.Status502BadGateway)]
[InlineData(StatusCodes.Status503ServiceUnavailable)]
[InlineData(StatusCodes.Status504GatewayTimeout)]
[InlineData(StatusCodes.Status505HttpVersionNotsupported)]
[InlineData(StatusCodes.Status506VariantAlsoNegotiates)]
[InlineData(StatusCodes.Status507InsufficientStorage)]
public void ResponseIsCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode)
{
var httpContext = new DefaultHttpContext();
httpContext.Response.StatusCode = statusCode;
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true
};
var context = CreateTestContext(httpContext);
Assert.False(context.ResponseIsCacheable());
}
private class Allow500Response : IResponseCachingCacheabilityValidator
{
public OverrideResult RequestIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic;
public OverrideResult ResponseIsCacheableOverride(HttpContext httpContext) =>
httpContext.Response.StatusCode == StatusCodes.Status500InternalServerError ? OverrideResult.Cache : OverrideResult.DoNotCache;
}
[Fact]
public void ResponseIsCacheableOverride_OverridesDefaultBehavior_ToAllowed()
{
var httpContext = new DefaultHttpContext();
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true
};
var responseCachingContext = CreateTestContext(httpContext, new Allow500Response());
Assert.True(responseCachingContext.ResponseIsCacheable());
}
private class Disallow200Response : IResponseCachingCacheabilityValidator
{
public OverrideResult RequestIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic;
public OverrideResult ResponseIsCacheableOverride(HttpContext httpContext) =>
httpContext.Response.StatusCode == StatusCodes.Status200OK ? OverrideResult.DoNotCache : OverrideResult.Cache;
}
[Fact]
public void ResponseIsCacheableOverride_OverridesDefaultBehavior_ToNotAllowed()
{
var httpContext = new DefaultHttpContext();
httpContext.Response.StatusCode = StatusCodes.Status200OK;
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true
};
var responseCachingContext = CreateTestContext(httpContext, new Disallow200Response());
Assert.False(responseCachingContext.ResponseIsCacheable());
}
[Fact]
public void ResponseIsCacheableOverride_IgnoreFallsBackToDefaultBehavior()
{
var httpContext = new DefaultHttpContext();
httpContext.Response.StatusCode = StatusCodes.Status200OK;
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true
};
var responseCachingContext = CreateTestContext(httpContext, new NoopCacheabilityValidator());
Assert.True(responseCachingContext.ResponseIsCacheable());
httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
Assert.False(responseCachingContext.ResponseIsCacheable());
}
[Fact]
public void EntryIsFresh_NoExpiryRequirements_IsFresh()
{
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext);
Assert.True(context.EntryIsFresh(new ResponseHeaders(new HeaderDictionary()), TimeSpan.MaxValue, verifyAgainstRequest: false));
}
[Fact]
public void EntryIsFresh_PastExpiry_IsNotFresh()
{
var httpContext = new DefaultHttpContext();
var utcNow = DateTimeOffset.UtcNow;
httpContext.Response.GetTypedHeaders().Expires = utcNow;
var context = CreateTestContext(httpContext);
context._responseTime = utcNow;
Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.MaxValue, verifyAgainstRequest: false));
}
[Fact]
public void EntryIsFresh_MaxAgeOverridesExpiry_ToFresh()
{
var utcNow = DateTimeOffset.UtcNow;
var httpContext = new DefaultHttpContext();
var responseHeaders = httpContext.Response.GetTypedHeaders();
responseHeaders.Expires = utcNow;
responseHeaders.CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(10)
};
var context = CreateTestContext(httpContext);
context._responseTime = utcNow;
Assert.True(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(10), verifyAgainstRequest: false));
}
[Fact]
public void EntryIsFresh_MaxAgeOverridesExpiry_ToNotFresh()
{
var utcNow = DateTimeOffset.UtcNow;
var httpContext = new DefaultHttpContext();
var responseHeaders = httpContext.Response.GetTypedHeaders();
responseHeaders.Expires = utcNow;
responseHeaders.CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(10)
};
var context = CreateTestContext(httpContext);
context._responseTime = utcNow;
Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(11), verifyAgainstRequest: false));
}
[Fact]
public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToFresh()
{
var httpContext = new DefaultHttpContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(10),
SharedMaxAge = TimeSpan.FromSeconds(15)
};
var context = CreateTestContext(httpContext);
Assert.True(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(11), verifyAgainstRequest: false));
}
[Fact]
public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh()
{
var httpContext = new DefaultHttpContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(10),
SharedMaxAge = TimeSpan.FromSeconds(5)
};
var context = CreateTestContext(httpContext);
Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(6), verifyAgainstRequest: false));
}
[Fact]
public void EntryIsFresh_MinFreshReducesFreshness_ToNotFresh()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
MinFresh = TimeSpan.FromSeconds(3)
};
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(10),
SharedMaxAge = TimeSpan.FromSeconds(5)
};
var context = CreateTestContext(httpContext);
Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(3), verifyAgainstRequest: true));
}
[Fact]
public void EntryIsFresh_RequestMaxAgeRestrictAge_ToNotFresh()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(5)
};
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(10),
};
var context = CreateTestContext(httpContext);
Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(6), verifyAgainstRequest: true));
}
[Fact]
public void EntryIsFresh_MaxStaleOverridesFreshness_ToFresh()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.GetTypedHeaders().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)
};
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(5),
};
var context = CreateTestContext(httpContext);
Assert.True(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(6), verifyAgainstRequest: true));
}
[Fact]
public void EntryIsFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.GetTypedHeaders().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)
};
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(5),
MustRevalidate = true
};
var context = CreateTestContext(httpContext);
Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(6), verifyAgainstRequest: true));
}
[Fact]
public void EntryIsFresh_IgnoresRequestVerificationWhenSpecified()
{
var httpContext = new DefaultHttpContext();
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
MinFresh = TimeSpan.FromSeconds(1),
MaxAge = TimeSpan.FromSeconds(3)
};
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(10),
SharedMaxAge = TimeSpan.FromSeconds(5)
};
var context = CreateTestContext(httpContext);
Assert.True(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(3), verifyAgainstRequest: false));
}
[Fact]
public void ConditionalRequestSatisfied_NotConditionalRequest_Fails()
@ -861,8 +159,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
return CreateTestContext(
httpContext,
new ResponseCachingOptions(),
new NoopCacheKeyModifier(),
new NoopCacheabilityValidator());
new CacheabilityValidator());
}
private static ResponseCachingContext CreateTestContext(HttpContext httpContext, ResponseCachingOptions options)
@ -870,41 +167,30 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
return CreateTestContext(
httpContext,
options,
new NoopCacheKeyModifier(),
new NoopCacheabilityValidator());
new CacheabilityValidator());
}
private static ResponseCachingContext CreateTestContext(HttpContext httpContext, IResponseCachingCacheKeyModifier cacheKeyModifier)
private static ResponseCachingContext CreateTestContext(HttpContext httpContext, ICacheabilityValidator cacheabilityValidator)
{
return CreateTestContext(
httpContext,
new ResponseCachingOptions(),
cacheKeyModifier,
new NoopCacheabilityValidator());
}
private static ResponseCachingContext CreateTestContext(HttpContext httpContext, IResponseCachingCacheabilityValidator cacheabilityValidator)
{
return CreateTestContext(
httpContext,
new ResponseCachingOptions(),
new NoopCacheKeyModifier(),
cacheabilityValidator);
}
private static ResponseCachingContext CreateTestContext(
HttpContext httpContext,
ResponseCachingOptions options,
IResponseCachingCacheKeyModifier cacheKeyModifier,
IResponseCachingCacheabilityValidator cacheabilityValidator)
ICacheabilityValidator cacheabilityValidator)
{
httpContext.AddResponseCachingState();
return new ResponseCachingContext(
httpContext,
new TestResponseCache(),
options,
new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy()),
cacheabilityValidator,
cacheKeyModifier);
new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)));
}
private class TestResponseCache : IResponseCache

View File

@ -49,7 +49,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public async void ServesCachedContent_IfVaryByHeader_Matches()
public async void ServesCachedContent_IfVaryHeader_Matches()
{
var builder = CreateBuilderWithResponseCaching(async (context) =>
{
@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public async void ServesFreshContent_IfVaryByHeader_Mismatches()
public async void ServesFreshContent_IfVaryHeader_Mismatches()
{
var builder = CreateBuilderWithResponseCaching(async (context) =>
{
@ -90,11 +90,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public async void ServesCachedContent_IfVaryByParams_Matches()
public async void ServesCachedContent_IfVaryParams_Matches()
{
var builder = CreateBuilderWithResponseCaching(async (context) =>
{
context.GetResponseCachingFeature().VaryByParams = "param";
context.GetResponseCachingFeature().VaryParams = "param";
await DefaultRequestDelegate(context);
});
@ -109,11 +109,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public async void ServesCachedContent_IfVaryByParamsExplicit_Matches_ParamNameCaseInsensitive()
public async void ServesCachedContent_IfVaryParamsExplicit_Matches_ParamNameCaseInsensitive()
{
var builder = CreateBuilderWithResponseCaching(async (context) =>
{
context.GetResponseCachingFeature().VaryByParams = new[] { "ParamA", "paramb" };
context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "paramb" };
await DefaultRequestDelegate(context);
});
@ -128,11 +128,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public async void ServesCachedContent_IfVaryByParamsStar_Matches_ParamNameCaseInsensitive()
public async void ServesCachedContent_IfVaryParamsStar_Matches_ParamNameCaseInsensitive()
{
var builder = CreateBuilderWithResponseCaching(async (context) =>
{
context.GetResponseCachingFeature().VaryByParams = new[] { "*" };
context.GetResponseCachingFeature().VaryParams = new[] { "*" };
await DefaultRequestDelegate(context);
});
@ -147,11 +147,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public async void ServesCachedContent_IfVaryByParamsExplicit_Matches_OrderInsensitive()
public async void ServesCachedContent_IfVaryParamsExplicit_Matches_OrderInsensitive()
{
var builder = CreateBuilderWithResponseCaching(async (context) =>
{
context.GetResponseCachingFeature().VaryByParams = new[] { "ParamB", "ParamA" };
context.GetResponseCachingFeature().VaryParams = new[] { "ParamB", "ParamA" };
await DefaultRequestDelegate(context);
});
@ -166,11 +166,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public async void ServesCachedContent_IfVaryByParamsStar_Matches_OrderInsensitive()
public async void ServesCachedContent_IfVaryParamsStar_Matches_OrderInsensitive()
{
var builder = CreateBuilderWithResponseCaching(async (context) =>
{
context.GetResponseCachingFeature().VaryByParams = new[] { "*" };
context.GetResponseCachingFeature().VaryParams = new[] { "*" };
await DefaultRequestDelegate(context);
});
@ -185,11 +185,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public async void ServesFreshContent_IfVaryByParams_Mismatches()
public async void ServesFreshContent_IfVaryParams_Mismatches()
{
var builder = CreateBuilderWithResponseCaching(async (context) =>
{
context.GetResponseCachingFeature().VaryByParams = "param";
context.GetResponseCachingFeature().VaryParams = "param";
await DefaultRequestDelegate(context);
});
@ -204,11 +204,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public async void ServesFreshContent_IfVaryByParamsExplicit_Mismatch_ParamValueCaseSensitive()
public async void ServesFreshContent_IfVaryParamsExplicit_Mismatch_ParamValueCaseSensitive()
{
var builder = CreateBuilderWithResponseCaching(async (context) =>
{
context.GetResponseCachingFeature().VaryByParams = new[] { "ParamA", "ParamB" };
context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "ParamB" };
await DefaultRequestDelegate(context);
});
@ -223,11 +223,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public async void ServesFreshContent_IfVaryByParamsStar_Mismatch_ParamValueCaseSensitive()
public async void ServesFreshContent_IfVaryParamsStar_Mismatch_ParamValueCaseSensitive()
{
var builder = CreateBuilderWithResponseCaching(async (context) =>
{
context.GetResponseCachingFeature().VaryByParams = new[] { "*" };
context.GetResponseCachingFeature().VaryParams = new[] { "*" };
await DefaultRequestDelegate(context);
});
@ -281,7 +281,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public async void ServesCachedContent_WithoutSetCookie()
public async void ServesFreshContent_IfSetCookie_IsSpecified()
{
var builder = CreateBuilderWithResponseCaching(async (context) =>
{
@ -295,20 +295,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var initialResponse = await client.GetAsync("");
var subsequentResponse = await client.GetAsync("");
initialResponse.EnsureSuccessStatusCode();
subsequentResponse.EnsureSuccessStatusCode();
foreach (var header in initialResponse.Headers)
{
if (!string.Equals(HeaderNames.SetCookie, header.Key, StringComparison.OrdinalIgnoreCase))
{
Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key));
}
}
Assert.True(initialResponse.Headers.Contains(HeaderNames.SetCookie));
Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age));
Assert.False(subsequentResponse.Headers.Contains(HeaderNames.SetCookie));
Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
await AssertResponseNotCachedAsync(initialResponse, subsequentResponse);
}
}