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:
parent
02b8fb3bbc
commit
7300d9e936
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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¶mB=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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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¶mB=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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue