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.
|
// 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;
|
||||||
|
using Microsoft.AspNetCore.ResponseCaching.Internal;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching
|
namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
{
|
{
|
||||||
// TODO: Temporary interface for endpoints to specify options for response caching
|
// TODO: Temporary interface for endpoints to specify options for response caching
|
||||||
public static class ResponseCachingHttpContextExtensions
|
public static class ResponseCachingHttpContextExtensions
|
||||||
{
|
{
|
||||||
public static void AddResponseCachingFeature(this HttpContext httpContext)
|
public static ResponseCachingState GetResponseCachingState(this HttpContext httpContext)
|
||||||
{
|
{
|
||||||
httpContext.Features.Set(new ResponseCachingFeature());
|
return httpContext.Features.Get<ResponseCachingState>();
|
||||||
}
|
|
||||||
|
|
||||||
public static void RemoveResponseCachingFeature(this HttpContext httpContext)
|
|
||||||
{
|
|
||||||
httpContext.Features.Set<ResponseCachingFeature>(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ResponseCachingFeature GetResponseCachingFeature(this HttpContext httpContext)
|
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));
|
throw new ArgumentNullException(nameof(services));
|
||||||
}
|
}
|
||||||
|
|
||||||
services.AddMemoryCache();
|
services.AddMemoryCache();
|
||||||
services.AddResponseCachingServices();
|
services.AddResponseCachingServices();
|
||||||
services.TryAdd(ServiceDescriptor.Singleton<IResponseCache, MemoryResponseCache>());
|
services.TryAdd(ServiceDescriptor.Singleton<IResponseCache, MemoryResponseCache>());
|
||||||
|
|
@ -30,7 +30,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(services));
|
throw new ArgumentNullException(nameof(services));
|
||||||
}
|
}
|
||||||
|
|
||||||
services.AddDistributedMemoryCache();
|
services.AddDistributedMemoryCache();
|
||||||
services.AddResponseCachingServices();
|
services.AddResponseCachingServices();
|
||||||
services.TryAdd(ServiceDescriptor.Singleton<IResponseCache, DistributedResponseCache>());
|
services.TryAdd(ServiceDescriptor.Singleton<IResponseCache, DistributedResponseCache>());
|
||||||
|
|
@ -40,8 +40,8 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
|
|
||||||
private static IServiceCollection AddResponseCachingServices(this IServiceCollection services)
|
private static IServiceCollection AddResponseCachingServices(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.TryAdd(ServiceDescriptor.Singleton<IResponseCachingCacheKeyModifier, NoopCacheKeyModifier>());
|
services.TryAdd(ServiceDescriptor.Singleton<IKeyProvider, KeyProvider>());
|
||||||
services.TryAdd(ServiceDescriptor.Singleton<IResponseCachingCacheabilityValidator, NoopCacheabilityValidator>());
|
services.TryAdd(ServiceDescriptor.Singleton<ICacheabilityValidator, CacheabilityValidator>());
|
||||||
|
|
||||||
return services;
|
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
|
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
{
|
{
|
||||||
internal static class DefaultResponseCacheSerializer
|
internal static class CacheEntrySerializer
|
||||||
{
|
{
|
||||||
private const int FormatVersion = 1;
|
private const int FormatVersion = 1;
|
||||||
|
|
||||||
|
|
@ -37,8 +37,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
|
|
||||||
// Serialization Format
|
// Serialization Format
|
||||||
// Format version (int)
|
// Format version (int)
|
||||||
// Type (char: 'R' for CachedResponse, 'V' for CachedVaryBy)
|
// Type (char: 'R' for CachedResponse, 'V' for CachedVaryRules)
|
||||||
// Type-dependent data (see CachedResponse and CachedVaryBy)
|
// Type-dependent data (see CachedResponse and CachedVaryRules)
|
||||||
public static object Read(BinaryReader reader)
|
public static object Read(BinaryReader reader)
|
||||||
{
|
{
|
||||||
if (reader == null)
|
if (reader == null)
|
||||||
|
|
@ -60,11 +60,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
}
|
}
|
||||||
else if (type == 'V')
|
else if (type == 'V')
|
||||||
{
|
{
|
||||||
var cachedResponse = ReadCachedVaryBy(reader);
|
var cachedVaryRules = ReadCachedVaryRules(reader);
|
||||||
return cachedResponse;
|
return cachedVaryRules;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unable to read as CachedResponse or CachedVaryBy
|
// Unable to read as CachedResponse or CachedVaryRules
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,12 +96,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serialization Format
|
// Serialization Format
|
||||||
// Headers count
|
// ContainsVaryRules (bool)
|
||||||
// Headers if count > 0 (comma separated string)
|
// If containing vary rules:
|
||||||
// Params count
|
// Headers count
|
||||||
// Params if count > 0 (comma separated string)
|
// Headers if count > 0 (comma separated string)
|
||||||
private static CachedVaryBy ReadCachedVaryBy(BinaryReader reader)
|
// 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 headerCount = reader.ReadInt32();
|
||||||
var headers = new string[headerCount];
|
var headers = new string[headerCount];
|
||||||
for (var index = 0; index < headerCount; index++)
|
for (var index = 0; index < headerCount; index++)
|
||||||
|
|
@ -115,7 +122,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
param[index] = reader.ReadString();
|
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
|
// See serialization format above
|
||||||
|
|
@ -132,14 +139,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.Write(FormatVersion);
|
writer.Write(FormatVersion);
|
||||||
|
|
||||||
if (entry is CachedResponse)
|
if (entry is CachedResponse)
|
||||||
{
|
{
|
||||||
|
writer.Write('R');
|
||||||
WriteCachedResponse(writer, entry as CachedResponse);
|
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
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -150,7 +159,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
// See serialization format above
|
// See serialization format above
|
||||||
private static void WriteCachedResponse(BinaryWriter writer, CachedResponse entry)
|
private static void WriteCachedResponse(BinaryWriter writer, CachedResponse entry)
|
||||||
{
|
{
|
||||||
writer.Write('R');
|
|
||||||
writer.Write(entry.Created.UtcTicks);
|
writer.Write(entry.Created.UtcTicks);
|
||||||
writer.Write(entry.StatusCode);
|
writer.Write(entry.StatusCode);
|
||||||
writer.Write(entry.Headers.Count);
|
writer.Write(entry.Headers.Count);
|
||||||
|
|
@ -165,20 +173,26 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
// See serialization format above
|
// See serialization format above
|
||||||
private static void WriteCachedVaryBy(BinaryWriter writer, CachedVaryBy entry)
|
private static void WriteCachedVaryRules(BinaryWriter writer, CachedVaryRules varyRules)
|
||||||
{
|
{
|
||||||
writer.Write('V');
|
if (varyRules.VaryRules == null)
|
||||||
|
|
||||||
writer.Write(entry.Headers.Count);
|
|
||||||
foreach (var header in entry.Headers)
|
|
||||||
{
|
{
|
||||||
writer.Write(header);
|
writer.Write(false);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
writer.Write(entry.Params.Count);
|
|
||||||
foreach (var param in entry.Params)
|
|
||||||
{
|
{
|
||||||
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.
|
// 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.
|
// 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
|
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
|
try
|
||||||
{
|
{
|
||||||
return DefaultResponseCacheSerializer.Deserialize(_cache.Get(key));
|
return CacheEntrySerializer.Deserialize(_cache.Get(key));
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
|
|
@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
{
|
{
|
||||||
_cache.Set(
|
_cache.Set(
|
||||||
key,
|
key,
|
||||||
DefaultResponseCacheSerializer.Serialize(entry),
|
CacheEntrySerializer.Serialize(entry),
|
||||||
new DistributedCacheEntryOptions()
|
new DistributedCacheEntryOptions()
|
||||||
{
|
{
|
||||||
AbsoluteExpirationRelativeToNow = validFor
|
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;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Http.Headers;
|
using Microsoft.AspNetCore.Http.Headers;
|
||||||
using Microsoft.AspNetCore.ResponseCaching.Internal;
|
using Microsoft.AspNetCore.ResponseCaching.Internal;
|
||||||
using Microsoft.Extensions.ObjectPool;
|
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
|
|
@ -20,54 +17,37 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
{
|
{
|
||||||
internal class ResponseCachingContext
|
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 HttpContext _httpContext;
|
||||||
private readonly IResponseCache _cache;
|
private readonly IResponseCache _cache;
|
||||||
private readonly ResponseCachingOptions _options;
|
private readonly ResponseCachingOptions _options;
|
||||||
private readonly ObjectPool<StringBuilder> _builderPool;
|
private readonly ICacheabilityValidator _cacheabilityValidator;
|
||||||
private readonly IResponseCachingCacheabilityValidator _cacheabilityValidator;
|
private readonly IKeyProvider _keyProvider;
|
||||||
private readonly IResponseCachingCacheKeyModifier _cacheKeyModifier;
|
|
||||||
|
|
||||||
private string _cacheKey;
|
private ResponseCachingState _state;
|
||||||
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;
|
|
||||||
|
|
||||||
// Internal for testing
|
|
||||||
internal ResponseCachingContext(
|
internal ResponseCachingContext(
|
||||||
HttpContext httpContext,
|
HttpContext httpContext,
|
||||||
IResponseCache cache,
|
IResponseCache cache,
|
||||||
ResponseCachingOptions options,
|
ResponseCachingOptions options,
|
||||||
ObjectPool<StringBuilder> builderPool,
|
ICacheabilityValidator cacheabilityValidator,
|
||||||
IResponseCachingCacheabilityValidator cacheabilityValidator,
|
IKeyProvider keyProvider)
|
||||||
IResponseCachingCacheKeyModifier cacheKeyModifier)
|
|
||||||
{
|
{
|
||||||
_httpContext = httpContext;
|
_httpContext = httpContext;
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
_options = options;
|
_options = options;
|
||||||
_builderPool = builderPool;
|
|
||||||
_cacheabilityValidator = cacheabilityValidator;
|
_cacheabilityValidator = cacheabilityValidator;
|
||||||
_cacheKeyModifier = cacheKeyModifier;
|
_keyProvider = keyProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool CacheResponse
|
internal ResponseCachingState State
|
||||||
{
|
{
|
||||||
get
|
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 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()
|
internal async Task<bool> TryServeFromCacheAsync()
|
||||||
{
|
{
|
||||||
_cacheKey = CreateCacheKey();
|
State.BaseKey = _keyProvider.CreateBaseKey(_httpContext);
|
||||||
var cacheEntry = _cache.Get(_cacheKey);
|
var cacheEntry = _cache.Get(State.BaseKey);
|
||||||
var responseServed = false;
|
var responseServed = false;
|
||||||
|
|
||||||
if (cacheEntry is CachedVaryBy)
|
if (cacheEntry is CachedVaryRules)
|
||||||
{
|
{
|
||||||
// Request contains VaryBy rules, recompute key and try again
|
// Request contains vary rules, recompute key and try again
|
||||||
_cacheKey = CreateCacheKey(cacheEntry as CachedVaryBy);
|
var varyKey = _keyProvider.CreateVaryKey(_httpContext, ((CachedVaryRules)cacheEntry).VaryRules);
|
||||||
cacheEntry = _cache.Get(_cacheKey);
|
cacheEntry = _cache.Get(varyKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cacheEntry is CachedResponse)
|
if (cacheEntry is CachedResponse)
|
||||||
|
|
@ -429,17 +77,18 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
var cachedResponse = cacheEntry as CachedResponse;
|
var cachedResponse = cacheEntry as CachedResponse;
|
||||||
var cachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers);
|
var cachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers);
|
||||||
|
|
||||||
_responseTime = _options.SystemClock.UtcNow;
|
State.ResponseTime = _options.SystemClock.UtcNow;
|
||||||
var age = _responseTime - cachedResponse.Created;
|
var cachedEntryAge = State.ResponseTime - cachedResponse.Created;
|
||||||
age = age > TimeSpan.Zero ? age : TimeSpan.Zero;
|
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
|
// Check conditional request rules
|
||||||
if (ConditionalRequestSatisfied(cachedResponseHeaders))
|
if (ConditionalRequestSatisfied(cachedResponseHeaders))
|
||||||
{
|
{
|
||||||
_httpContext.Response.StatusCode = StatusCodes.Status304NotModified;
|
_httpContext.Response.StatusCode = StatusCodes.Status304NotModified;
|
||||||
responseServed = true;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -451,33 +100,20 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
response.Headers.Add(header);
|
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
|
// 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;
|
response.ContentLength = body.Length;
|
||||||
}
|
}
|
||||||
|
await response.Body.WriteAsync(body, 0, 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.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -487,7 +123,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!responseServed && RequestCacheControl.OnlyIfCached)
|
if (!responseServed && State.RequestCacheControl.OnlyIfCached)
|
||||||
{
|
{
|
||||||
_httpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
|
_httpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
|
||||||
responseServed = true;
|
responseServed = true;
|
||||||
|
|
@ -498,7 +134,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
|
|
||||||
internal bool ConditionalRequestSatisfied(ResponseHeaders cachedResponseHeaders)
|
internal bool ConditionalRequestSatisfied(ResponseHeaders cachedResponseHeaders)
|
||||||
{
|
{
|
||||||
var ifNoneMatchHeader = RequestHeaders.IfNoneMatch;
|
var ifNoneMatchHeader = State.RequestHeaders.IfNoneMatch;
|
||||||
|
|
||||||
if (ifNoneMatchHeader != null)
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -528,57 +164,56 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
|
|
||||||
internal void FinalizeCachingHeaders()
|
internal void FinalizeCachingHeaders()
|
||||||
{
|
{
|
||||||
if (CacheResponse)
|
if (_cacheabilityValidator.ResponseIsCacheable(_httpContext))
|
||||||
{
|
{
|
||||||
|
State.ShouldCacheResponse = true;
|
||||||
|
|
||||||
// Create the cache entry now
|
// Create the cache entry now
|
||||||
var response = _httpContext.Response;
|
var response = _httpContext.Response;
|
||||||
var varyHeaderValue = response.Headers[HeaderNames.Vary];
|
var varyHeaderValue = response.Headers[HeaderNames.Vary];
|
||||||
var varyParamsValue = _httpContext.GetResponseCachingFeature().VaryByParams;
|
var varyParamsValue = _httpContext.GetResponseCachingFeature()?.VaryParams ?? StringValues.Empty;
|
||||||
_cachedResponseValidFor = ResponseCacheControl.SharedMaxAge
|
State.CachedResponseValidFor = State.ResponseCacheControl.SharedMaxAge
|
||||||
?? ResponseCacheControl.MaxAge
|
?? State.ResponseCacheControl.MaxAge
|
||||||
?? (ResponseHeaders.Expires - _responseTime)
|
?? (State.ResponseHeaders.Expires - State.ResponseTime)
|
||||||
// TODO: Heuristics for expiration?
|
// TODO: Heuristics for expiration?
|
||||||
?? TimeSpan.FromSeconds(10);
|
?? TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
// Check if any VaryBy rules exist
|
// Check if any vary rules exist
|
||||||
if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue))
|
if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue))
|
||||||
{
|
{
|
||||||
if (varyParamsValue.Count > 1)
|
var cachedVaryRules = new CachedVaryRules
|
||||||
{
|
{
|
||||||
Array.Sort(varyParamsValue.ToArray(), StringComparer.OrdinalIgnoreCase);
|
VaryRules = new VaryRules()
|
||||||
}
|
{
|
||||||
|
// TODO: Vary Encoding
|
||||||
var cachedVaryBy = new CachedVaryBy
|
Headers = varyHeaderValue,
|
||||||
{
|
Params = varyParamsValue
|
||||||
// TODO: VaryBy Encoding
|
}
|
||||||
Headers = varyHeaderValue,
|
|
||||||
Params = varyParamsValue
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Overwrite?
|
// TODO: Overwrite?
|
||||||
_cache.Set(_cacheKey, cachedVaryBy, _cachedResponseValidFor);
|
_cache.Set(State.BaseKey, cachedVaryRules, State.CachedResponseValidFor);
|
||||||
_cacheKey = CreateCacheKey(cachedVaryBy);
|
State.VaryKey = _keyProvider.CreateVaryKey(_httpContext, cachedVaryRules.VaryRules);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure date header is set
|
// 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
|
// 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
|
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)
|
if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase))
|
||||||
&& !string.Equals(header.Key, HeaderNames.SetCookie, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
_cachedResponse.Headers.Add(header);
|
State.CachedResponse.Headers.Add(header);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -590,11 +225,11 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
|
|
||||||
internal void FinalizeCachingBody()
|
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)
|
if (!ResponseStarted)
|
||||||
{
|
{
|
||||||
ResponseStarted = true;
|
ResponseStarted = true;
|
||||||
_responseTime = _options.SystemClock.UtcNow;
|
State.ResponseTime = _options.SystemClock.UtcNow;
|
||||||
|
|
||||||
FinalizeCachingHeaders();
|
FinalizeCachingHeaders();
|
||||||
}
|
}
|
||||||
|
|
@ -640,11 +275,5 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
// TODO: Move this temporary interface with endpoint to HttpAbstractions
|
// TODO: Move this temporary interface with endpoint to HttpAbstractions
|
||||||
_httpContext.RemoveResponseCachingFeature();
|
_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
|
// TODO: Temporary interface for endpoints to specify options for response caching
|
||||||
public class ResponseCachingFeature
|
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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.ResponseCaching.Internal;
|
||||||
using Microsoft.Extensions.Internal;
|
using Microsoft.Extensions.Internal;
|
||||||
using Microsoft.Extensions.ObjectPool;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching
|
namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
|
|
@ -23,17 +22,15 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
private readonly IResponseCache _cache;
|
private readonly IResponseCache _cache;
|
||||||
private readonly ResponseCachingOptions _options;
|
private readonly ResponseCachingOptions _options;
|
||||||
private readonly ObjectPool<StringBuilder> _builderPool;
|
private readonly ICacheabilityValidator _cacheabilityValidator;
|
||||||
private readonly IResponseCachingCacheabilityValidator _cacheabilityValidator;
|
private readonly IKeyProvider _keyProvider;
|
||||||
private readonly IResponseCachingCacheKeyModifier _cacheKeyModifier;
|
|
||||||
|
|
||||||
public ResponseCachingMiddleware(
|
public ResponseCachingMiddleware(
|
||||||
RequestDelegate next,
|
RequestDelegate next,
|
||||||
IResponseCache cache,
|
IResponseCache cache,
|
||||||
IOptions<ResponseCachingOptions> options,
|
IOptions<ResponseCachingOptions> options,
|
||||||
ObjectPoolProvider poolProvider,
|
ICacheabilityValidator cacheabilityValidator,
|
||||||
IResponseCachingCacheabilityValidator cacheabilityValidator,
|
IKeyProvider keyProvider)
|
||||||
IResponseCachingCacheKeyModifier cacheKeyModifier)
|
|
||||||
{
|
{
|
||||||
if (next == null)
|
if (next == null)
|
||||||
{
|
{
|
||||||
|
|
@ -47,71 +44,74 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(options));
|
throw new ArgumentNullException(nameof(options));
|
||||||
}
|
}
|
||||||
if (poolProvider == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(poolProvider));
|
|
||||||
}
|
|
||||||
if (cacheabilityValidator == null)
|
if (cacheabilityValidator == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(cacheabilityValidator));
|
throw new ArgumentNullException(nameof(cacheabilityValidator));
|
||||||
}
|
}
|
||||||
if (cacheKeyModifier == null)
|
if (keyProvider == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(cacheKeyModifier));
|
throw new ArgumentNullException(nameof(keyProvider));
|
||||||
}
|
}
|
||||||
|
|
||||||
_next = next;
|
_next = next;
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
_builderPool = poolProvider.CreateStringBuilderPool();
|
|
||||||
_cacheabilityValidator = cacheabilityValidator;
|
_cacheabilityValidator = cacheabilityValidator;
|
||||||
_cacheKeyModifier = cacheKeyModifier;
|
_keyProvider = keyProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Invoke(HttpContext context)
|
public async Task Invoke(HttpContext context)
|
||||||
{
|
{
|
||||||
var cachingContext = new ResponseCachingContext(
|
context.AddResponseCachingState();
|
||||||
context,
|
|
||||||
_cache,
|
|
||||||
_options,
|
|
||||||
_builderPool,
|
|
||||||
_cacheabilityValidator,
|
|
||||||
_cacheKeyModifier);
|
|
||||||
|
|
||||||
// Should we attempt any caching logic?
|
try
|
||||||
if (cachingContext.RequestIsCacheable())
|
|
||||||
{
|
{
|
||||||
// Can this request be served from cache?
|
var cachingContext = new ResponseCachingContext(
|
||||||
if (await cachingContext.TryServeFromCacheAsync())
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
// Hook up to listen to the response stream
|
|
||||||
cachingContext.ShimResponseStream();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
// Subscribe to OnStarting event
|
// TODO: Invalidate resources for successful unsafe methods? Required by RFC
|
||||||
context.Response.OnStarting(OnStartingCallback, cachingContext);
|
|
||||||
|
|
||||||
await _next(context);
|
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
|
context.RemoveResponseCachingState();
|
||||||
await _next(context);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@
|
||||||
|
|
||||||
using Microsoft.Extensions.Primitives;
|
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 Headers { get; set; }
|
||||||
internal StringValues Params { 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;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
|
@ -12,6 +11,7 @@ using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.Http.Headers;
|
using Microsoft.AspNetCore.Http.Headers;
|
||||||
using Microsoft.AspNetCore.ResponseCaching.Internal;
|
using Microsoft.AspNetCore.ResponseCaching.Internal;
|
||||||
using Microsoft.Extensions.ObjectPool;
|
using Microsoft.Extensions.ObjectPool;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -19,708 +19,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
{
|
{
|
||||||
public class ResponseCachingContextTests
|
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]
|
[Fact]
|
||||||
public void ConditionalRequestSatisfied_NotConditionalRequest_Fails()
|
public void ConditionalRequestSatisfied_NotConditionalRequest_Fails()
|
||||||
|
|
@ -861,8 +159,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
return CreateTestContext(
|
return CreateTestContext(
|
||||||
httpContext,
|
httpContext,
|
||||||
new ResponseCachingOptions(),
|
new ResponseCachingOptions(),
|
||||||
new NoopCacheKeyModifier(),
|
new CacheabilityValidator());
|
||||||
new NoopCacheabilityValidator());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ResponseCachingContext CreateTestContext(HttpContext httpContext, ResponseCachingOptions options)
|
private static ResponseCachingContext CreateTestContext(HttpContext httpContext, ResponseCachingOptions options)
|
||||||
|
|
@ -870,41 +167,30 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
return CreateTestContext(
|
return CreateTestContext(
|
||||||
httpContext,
|
httpContext,
|
||||||
options,
|
options,
|
||||||
new NoopCacheKeyModifier(),
|
new CacheabilityValidator());
|
||||||
new NoopCacheabilityValidator());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ResponseCachingContext CreateTestContext(HttpContext httpContext, IResponseCachingCacheKeyModifier cacheKeyModifier)
|
private static ResponseCachingContext CreateTestContext(HttpContext httpContext, ICacheabilityValidator cacheabilityValidator)
|
||||||
{
|
{
|
||||||
return CreateTestContext(
|
return CreateTestContext(
|
||||||
httpContext,
|
httpContext,
|
||||||
new ResponseCachingOptions(),
|
new ResponseCachingOptions(),
|
||||||
cacheKeyModifier,
|
|
||||||
new NoopCacheabilityValidator());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ResponseCachingContext CreateTestContext(HttpContext httpContext, IResponseCachingCacheabilityValidator cacheabilityValidator)
|
|
||||||
{
|
|
||||||
return CreateTestContext(
|
|
||||||
httpContext,
|
|
||||||
new ResponseCachingOptions(),
|
|
||||||
new NoopCacheKeyModifier(),
|
|
||||||
cacheabilityValidator);
|
cacheabilityValidator);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ResponseCachingContext CreateTestContext(
|
private static ResponseCachingContext CreateTestContext(
|
||||||
HttpContext httpContext,
|
HttpContext httpContext,
|
||||||
ResponseCachingOptions options,
|
ResponseCachingOptions options,
|
||||||
IResponseCachingCacheKeyModifier cacheKeyModifier,
|
ICacheabilityValidator cacheabilityValidator)
|
||||||
IResponseCachingCacheabilityValidator cacheabilityValidator)
|
|
||||||
{
|
{
|
||||||
|
httpContext.AddResponseCachingState();
|
||||||
|
|
||||||
return new ResponseCachingContext(
|
return new ResponseCachingContext(
|
||||||
httpContext,
|
httpContext,
|
||||||
new TestResponseCache(),
|
new TestResponseCache(),
|
||||||
options,
|
options,
|
||||||
new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy()),
|
|
||||||
cacheabilityValidator,
|
cacheabilityValidator,
|
||||||
cacheKeyModifier);
|
new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestResponseCache : IResponseCache
|
private class TestResponseCache : IResponseCache
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_IfVaryByHeader_Matches()
|
public async void ServesCachedContent_IfVaryHeader_Matches()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||||
{
|
{
|
||||||
|
|
@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesFreshContent_IfVaryByHeader_Mismatches()
|
public async void ServesFreshContent_IfVaryHeader_Mismatches()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||||
{
|
{
|
||||||
|
|
@ -90,11 +90,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_IfVaryByParams_Matches()
|
public async void ServesCachedContent_IfVaryParams_Matches()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||||
{
|
{
|
||||||
context.GetResponseCachingFeature().VaryByParams = "param";
|
context.GetResponseCachingFeature().VaryParams = "param";
|
||||||
await DefaultRequestDelegate(context);
|
await DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -109,11 +109,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_IfVaryByParamsExplicit_Matches_ParamNameCaseInsensitive()
|
public async void ServesCachedContent_IfVaryParamsExplicit_Matches_ParamNameCaseInsensitive()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||||
{
|
{
|
||||||
context.GetResponseCachingFeature().VaryByParams = new[] { "ParamA", "paramb" };
|
context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "paramb" };
|
||||||
await DefaultRequestDelegate(context);
|
await DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -128,11 +128,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_IfVaryByParamsStar_Matches_ParamNameCaseInsensitive()
|
public async void ServesCachedContent_IfVaryParamsStar_Matches_ParamNameCaseInsensitive()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||||
{
|
{
|
||||||
context.GetResponseCachingFeature().VaryByParams = new[] { "*" };
|
context.GetResponseCachingFeature().VaryParams = new[] { "*" };
|
||||||
await DefaultRequestDelegate(context);
|
await DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -147,11 +147,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_IfVaryByParamsExplicit_Matches_OrderInsensitive()
|
public async void ServesCachedContent_IfVaryParamsExplicit_Matches_OrderInsensitive()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||||
{
|
{
|
||||||
context.GetResponseCachingFeature().VaryByParams = new[] { "ParamB", "ParamA" };
|
context.GetResponseCachingFeature().VaryParams = new[] { "ParamB", "ParamA" };
|
||||||
await DefaultRequestDelegate(context);
|
await DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -166,11 +166,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_IfVaryByParamsStar_Matches_OrderInsensitive()
|
public async void ServesCachedContent_IfVaryParamsStar_Matches_OrderInsensitive()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||||
{
|
{
|
||||||
context.GetResponseCachingFeature().VaryByParams = new[] { "*" };
|
context.GetResponseCachingFeature().VaryParams = new[] { "*" };
|
||||||
await DefaultRequestDelegate(context);
|
await DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -185,11 +185,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesFreshContent_IfVaryByParams_Mismatches()
|
public async void ServesFreshContent_IfVaryParams_Mismatches()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||||
{
|
{
|
||||||
context.GetResponseCachingFeature().VaryByParams = "param";
|
context.GetResponseCachingFeature().VaryParams = "param";
|
||||||
await DefaultRequestDelegate(context);
|
await DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -204,11 +204,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesFreshContent_IfVaryByParamsExplicit_Mismatch_ParamValueCaseSensitive()
|
public async void ServesFreshContent_IfVaryParamsExplicit_Mismatch_ParamValueCaseSensitive()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||||
{
|
{
|
||||||
context.GetResponseCachingFeature().VaryByParams = new[] { "ParamA", "ParamB" };
|
context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "ParamB" };
|
||||||
await DefaultRequestDelegate(context);
|
await DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -223,11 +223,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesFreshContent_IfVaryByParamsStar_Mismatch_ParamValueCaseSensitive()
|
public async void ServesFreshContent_IfVaryParamsStar_Mismatch_ParamValueCaseSensitive()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||||
{
|
{
|
||||||
context.GetResponseCachingFeature().VaryByParams = new[] { "*" };
|
context.GetResponseCachingFeature().VaryParams = new[] { "*" };
|
||||||
await DefaultRequestDelegate(context);
|
await DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -281,7 +281,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_WithoutSetCookie()
|
public async void ServesFreshContent_IfSetCookie_IsSpecified()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||||
{
|
{
|
||||||
|
|
@ -295,20 +295,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
var initialResponse = await client.GetAsync("");
|
var initialResponse = await client.GetAsync("");
|
||||||
var subsequentResponse = await client.GetAsync("");
|
var subsequentResponse = await client.GetAsync("");
|
||||||
|
|
||||||
initialResponse.EnsureSuccessStatusCode();
|
await AssertResponseNotCachedAsync(initialResponse, subsequentResponse);
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue