API review renames and updates

This commit is contained in:
John Luo 2016-09-12 17:36:47 -07:00
parent e236e64055
commit ccfa090e6e
24 changed files with 1602 additions and 1769 deletions

View File

@ -146,7 +146,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
param[index] = reader.ReadString(); param[index] = reader.ReadString();
} }
return new CachedVaryRules { VaryKeyPrefix = varyKeyPrefix, VaryRules = new VaryRules() { Headers = headers, Params = param } }; return new CachedVaryRules { VaryKeyPrefix = varyKeyPrefix, Headers = headers, Params = param };
} }
// See serialization format above // See serialization format above
@ -222,14 +222,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
{ {
writer.Write(varyRules.VaryKeyPrefix); writer.Write(varyRules.VaryKeyPrefix);
writer.Write(varyRules.VaryRules.Headers.Count); writer.Write(varyRules.Headers.Count);
foreach (var header in varyRules.VaryRules.Headers) foreach (var header in varyRules.Headers)
{ {
writer.Write(header); writer.Write(header);
} }
writer.Write(varyRules.VaryRules.Params.Count); writer.Write(varyRules.Params.Count);
foreach (var param in varyRules.VaryRules.Params) foreach (var param in varyRules.Params)
{ {
writer.Write(param); writer.Write(param);
} }

View File

@ -4,18 +4,18 @@
using System; using System;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.ResponseCaching.Internal namespace Microsoft.AspNetCore.ResponseCaching
{ {
internal class CachedResponse public class CachedResponse
{ {
internal string BodyKeyPrefix { get; set; } public string BodyKeyPrefix { get; internal set; }
internal DateTimeOffset Created { get; set; } public DateTimeOffset Created { get; internal set; }
internal int StatusCode { get; set; } public int StatusCode { get; internal set; }
internal IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); public IHeaderDictionary Headers { get; internal set; } = new HeaderDictionary();
internal byte[] Body { get; set; } public byte[] Body { get; internal set; }
} }
} }

View File

@ -1,10 +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.
namespace Microsoft.AspNetCore.ResponseCaching.Internal namespace Microsoft.AspNetCore.ResponseCaching
{ {
internal class CachedResponseBody public class CachedResponseBody
{ {
internal byte[] Body { get; set; } public byte[] Body { get; internal set; }
} }
} }

View File

@ -1,12 +1,16 @@
// 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.
namespace Microsoft.AspNetCore.ResponseCaching.Internal using Microsoft.Extensions.Primitives;
{
internal class CachedVaryRules
{
internal string VaryKeyPrefix { get; set; }
internal VaryRules VaryRules { get; set; } namespace Microsoft.AspNetCore.ResponseCaching
{
public class CachedVaryRules
{
public string VaryKeyPrefix { get; internal set; }
public StringValues Headers { get; internal set; }
public StringValues Params { get; internal set; }
} }
} }

View File

@ -6,14 +6,13 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.ResponseCaching namespace Microsoft.AspNetCore.ResponseCaching
{ {
public class KeyProvider : IKeyProvider public class CacheKeyProvider : ICacheKeyProvider
{ {
// Use the record separator for delimiting components of the cache key to avoid possible collisions // Use the record separator for delimiting components of the cache key to avoid possible collisions
private static readonly char KeyDelimiter = '\x1e'; private static readonly char KeyDelimiter = '\x1e';
@ -21,7 +20,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
private readonly ObjectPool<StringBuilder> _builderPool; private readonly ObjectPool<StringBuilder> _builderPool;
private readonly ResponseCachingOptions _options; private readonly ResponseCachingOptions _options;
public KeyProvider(ObjectPoolProvider poolProvider, IOptions<ResponseCachingOptions> options) public CacheKeyProvider(ObjectPoolProvider poolProvider, IOptions<ResponseCachingOptions> options)
{ {
if (poolProvider == null) if (poolProvider == null)
{ {
@ -36,26 +35,25 @@ namespace Microsoft.AspNetCore.ResponseCaching
_options = options.Value; _options = options.Value;
} }
public virtual IEnumerable<string> CreateLookupBaseKey(HttpContext httpContext) public virtual IEnumerable<string> CreateLookupBaseKeys(ResponseCachingContext context)
{ {
return new string[] { CreateStorageBaseKey(httpContext) }; return new string[] { CreateStorageBaseKey(context) };
} }
public virtual IEnumerable<string> CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules) public virtual IEnumerable<string> CreateLookupVaryKeys(ResponseCachingContext context)
{ {
return new string[] { CreateStorageVaryKey(httpContext, varyRules) }; return new string[] { CreateStorageVaryKey(context) };
} }
// GET<delimiter>/PATH // GET<delimiter>/PATH
// TODO: Method invariant retrieval? E.g. HEAD after GET to the same resource. public virtual string CreateStorageBaseKey(ResponseCachingContext context)
public virtual string CreateStorageBaseKey(HttpContext httpContext)
{ {
if (httpContext == null) if (context == null)
{ {
throw new ArgumentNullException(nameof(httpContext)); throw new ArgumentNullException(nameof(context));
} }
var request = httpContext.Request; var request = context.HttpContext.Request;
var builder = _builderPool.Get(); var builder = _builderPool.Get();
try try
@ -74,24 +72,31 @@ namespace Microsoft.AspNetCore.ResponseCaching
} }
// BaseKey<delimiter>H<delimiter>HeaderName=HeaderValue<delimiter>Q<delimiter>QueryName=QueryValue // BaseKey<delimiter>H<delimiter>HeaderName=HeaderValue<delimiter>Q<delimiter>QueryName=QueryValue
public virtual string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules) public virtual string CreateStorageVaryKey(ResponseCachingContext context)
{ {
if (httpContext == null) if (context == null)
{ {
throw new ArgumentNullException(nameof(httpContext)); throw new ArgumentNullException(nameof(context));
}
if (varyRules == null || (StringValues.IsNullOrEmpty(varyRules.Headers) && StringValues.IsNullOrEmpty(varyRules.Params)))
{
return httpContext.GetResponseCachingState().CachedVaryRules.VaryKeyPrefix;
} }
var request = httpContext.Request; var varyRules = context.CachedVaryRules;
if (varyRules == null)
{
throw new InvalidOperationException($"{nameof(CachedVaryRules)} must not be null on the {nameof(ResponseCachingContext)}");
}
if ((StringValues.IsNullOrEmpty(varyRules.Headers) && StringValues.IsNullOrEmpty(varyRules.Params)))
{
return varyRules.VaryKeyPrefix;
}
var request = context.HttpContext.Request;
var builder = _builderPool.Get(); var builder = _builderPool.Get();
try try
{ {
// Prepend with the Guid of the CachedVaryRules // Prepend with the Guid of the CachedVaryRules
builder.Append(httpContext.GetResponseCachingState().CachedVaryRules.VaryKeyPrefix); builder.Append(varyRules.VaryKeyPrefix);
// Vary by headers // Vary by headers
if (varyRules?.Headers.Count > 0) if (varyRules?.Headers.Count > 0)
@ -102,18 +107,11 @@ namespace Microsoft.AspNetCore.ResponseCaching
foreach (var header in varyRules.Headers) 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) builder.Append(KeyDelimiter)
.Append(header) .Append(header)
.Append("=") .Append("=")
.Append(value); // TODO: Perf - iterate the string values instead?
.Append(context.HttpContext.Request.Headers[header]);
} }
} }
@ -127,7 +125,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
if (varyRules.Params.Count == 1 && string.Equals(varyRules.Params[0], "*", StringComparison.Ordinal)) if (varyRules.Params.Count == 1 && string.Equals(varyRules.Params[0], "*", StringComparison.Ordinal))
{ {
// Vary by all available query params // Vary by all available query params
foreach (var query in httpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase)) foreach (var query in context.HttpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase))
{ {
builder.Append(KeyDelimiter) builder.Append(KeyDelimiter)
.Append(query.Key.ToUpperInvariant()) .Append(query.Key.ToUpperInvariant())
@ -139,18 +137,11 @@ namespace Microsoft.AspNetCore.ResponseCaching
{ {
foreach (var param in varyRules.Params) 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) builder.Append(KeyDelimiter)
.Append(param) .Append(param)
.Append("=") .Append("=")
.Append(value); // TODO: Perf - iterate the string values instead?
.Append(context.HttpContext.Request.Query[param]);
} }
} }
} }

View File

@ -13,13 +13,11 @@ namespace Microsoft.AspNetCore.ResponseCaching
{ {
private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue();
public virtual bool RequestIsCacheable(HttpContext httpContext) public virtual bool IsRequestCacheable(ResponseCachingContext context)
{ {
var state = httpContext.GetResponseCachingState();
// Verify the method // 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. // TODO: RFC lists POST as a cacheable method when explicit freshness information is provided, but this is not widely implemented. Will revisit.
var request = httpContext.Request; var request = context.HttpContext.Request;
if (!string.Equals("GET", request.Method, StringComparison.OrdinalIgnoreCase) && if (!string.Equals("GET", request.Method, StringComparison.OrdinalIgnoreCase) &&
!string.Equals("HEAD", request.Method, StringComparison.OrdinalIgnoreCase)) !string.Equals("HEAD", request.Method, StringComparison.OrdinalIgnoreCase))
{ {
@ -37,7 +35,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
// TODO: no-cache requests can be retrieved upon validation with origin // TODO: no-cache requests can be retrieved upon validation with origin
if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl])) if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl]))
{ {
if (state.RequestCacheControl.NoCache) if (context.RequestCacheControlHeaderValue.NoCache)
{ {
return false; return false;
} }
@ -59,31 +57,29 @@ namespace Microsoft.AspNetCore.ResponseCaching
return true; return true;
} }
public virtual bool ResponseIsCacheable(HttpContext httpContext) public virtual bool IsResponseCacheable(ResponseCachingContext context)
{ {
var state = httpContext.GetResponseCachingState();
// Only cache pages explicitly marked with public // Only cache pages explicitly marked with public
// TODO: Consider caching responses that are not marked as public but otherwise cacheable? // TODO: Consider caching responses that are not marked as public but otherwise cacheable?
if (!state.ResponseCacheControl.Public) if (!context.ResponseCacheControlHeaderValue.Public)
{ {
return false; return false;
} }
// Check no-store // Check no-store
if (state.RequestCacheControl.NoStore || state.ResponseCacheControl.NoStore) if (context.RequestCacheControlHeaderValue.NoStore || context.ResponseCacheControlHeaderValue.NoStore)
{ {
return false; return false;
} }
// Check no-cache // Check no-cache
// TODO: Handle no-cache with headers // TODO: Handle no-cache with headers
if (state.ResponseCacheControl.NoCache) if (context.ResponseCacheControlHeaderValue.NoCache)
{ {
return false; return false;
} }
var response = httpContext.Response; var response = context.HttpContext.Response;
// Do not cache responses with Set-Cookie headers // Do not cache responses with Set-Cookie headers
if (!StringValues.IsNullOrEmpty(response.Headers[HeaderNames.SetCookie])) if (!StringValues.IsNullOrEmpty(response.Headers[HeaderNames.SetCookie]))
@ -101,7 +97,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
// TODO: public MAY override the cacheability checks for private and status codes // TODO: public MAY override the cacheability checks for private and status codes
// Check private // Check private
if (state.ResponseCacheControl.Private) if (context.ResponseCacheControlHeaderValue.Private)
{ {
return false; return false;
} }
@ -115,35 +111,35 @@ namespace Microsoft.AspNetCore.ResponseCaching
// Check response freshness // Check response freshness
// TODO: apparent age vs corrected age value // TODO: apparent age vs corrected age value
if (state.ResponseHeaders.Date == null) if (context.TypedResponseHeaders.Date == null)
{ {
if (state.ResponseCacheControl.SharedMaxAge == null && if (context.ResponseCacheControlHeaderValue.SharedMaxAge == null &&
state.ResponseCacheControl.MaxAge == null && context.ResponseCacheControlHeaderValue.MaxAge == null &&
state.ResponseTime > state.ResponseHeaders.Expires) context.ResponseTime > context.TypedResponseHeaders.Expires)
{ {
return false; return false;
} }
} }
else else
{ {
var age = state.ResponseTime - state.ResponseHeaders.Date.Value; var age = context.ResponseTime - context.TypedResponseHeaders.Date.Value;
// Validate shared max age // Validate shared max age
if (age > state.ResponseCacheControl.SharedMaxAge) if (age > context.ResponseCacheControlHeaderValue.SharedMaxAge)
{ {
return false; return false;
} }
else if (state.ResponseCacheControl.SharedMaxAge == null) else if (context.ResponseCacheControlHeaderValue.SharedMaxAge == null)
{ {
// Validate max age // Validate max age
if (age > state.ResponseCacheControl.MaxAge) if (age > context.ResponseCacheControlHeaderValue.MaxAge)
{ {
return false; return false;
} }
else if (state.ResponseCacheControl.MaxAge == null) else if (context.ResponseCacheControlHeaderValue.MaxAge == null)
{ {
// Validate expiration // Validate expiration
if (state.ResponseTime > state.ResponseHeaders.Expires) if (context.ResponseTime > context.TypedResponseHeaders.Expires)
{ {
return false; return false;
} }
@ -154,16 +150,15 @@ namespace Microsoft.AspNetCore.ResponseCaching
return true; return true;
} }
public virtual bool CachedEntryIsFresh(HttpContext httpContext, ResponseHeaders cachedResponseHeaders) public virtual bool IsCachedEntryFresh(ResponseCachingContext context)
{ {
var state = httpContext.GetResponseCachingState(); var age = context.CachedEntryAge;
var age = state.CachedEntryAge; var cachedControlHeaders = context.CachedResponseHeaders.CacheControl ?? EmptyCacheControl;
var cachedControlHeaders = cachedResponseHeaders.CacheControl ?? EmptyCacheControl;
// Add min-fresh requirements // Add min-fresh requirements
if (state.RequestCacheControl.MinFresh != null) if (context.RequestCacheControlHeaderValue.MinFresh != null)
{ {
age += state.RequestCacheControl.MinFresh.Value; age += context.RequestCacheControlHeaderValue.MinFresh.Value;
} }
// Validate shared max age, this overrides any max age settings for shared caches // Validate shared max age, this overrides any max age settings for shared caches
@ -175,7 +170,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
else if (cachedControlHeaders.SharedMaxAge == null) else if (cachedControlHeaders.SharedMaxAge == null)
{ {
// Validate max age // Validate max age
if (age > cachedControlHeaders.MaxAge || age > state.RequestCacheControl.MaxAge) if (age > cachedControlHeaders.MaxAge || age > context.RequestCacheControlHeaderValue.MaxAge)
{ {
// Must revalidate // Must revalidate
if (cachedControlHeaders.MustRevalidate) if (cachedControlHeaders.MustRevalidate)
@ -184,7 +179,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
} }
// Request allows stale values // Request allows stale values
if (age < state.RequestCacheControl.MaxStaleLimit) if (age < context.RequestCacheControlHeaderValue.MaxStaleLimit)
{ {
// TODO: Add warning header indicating the response is stale // TODO: Add warning header indicating the response is stale
return true; return true;
@ -192,10 +187,10 @@ namespace Microsoft.AspNetCore.ResponseCaching
return false; return false;
} }
else if (cachedControlHeaders.MaxAge == null && state.RequestCacheControl.MaxAge == null) else if (cachedControlHeaders.MaxAge == null && context.RequestCacheControlHeaderValue.MaxAge == null)
{ {
// Validate expiration // Validate expiration
if (state.ResponseTime > cachedResponseHeaders.Expires) if (context.ResponseTime > context.CachedResponseHeaders.Expires)
{ {
return false; return false;
} }

View File

@ -2,18 +2,12 @@
// 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 ResponseCachingState GetResponseCachingState(this HttpContext httpContext)
{
return httpContext.Features.Get<ResponseCachingState>();
}
public static ResponseCachingFeature GetResponseCachingFeature(this HttpContext httpContext) public static ResponseCachingFeature GetResponseCachingFeature(this HttpContext httpContext)
{ {
return httpContext.Features.Get<ResponseCachingFeature>(); return httpContext.Features.Get<ResponseCachingFeature>();

View File

@ -40,7 +40,7 @@ namespace Microsoft.Extensions.DependencyInjection
private static IServiceCollection AddResponseCachingServices(this IServiceCollection services) private static IServiceCollection AddResponseCachingServices(this IServiceCollection services)
{ {
services.TryAdd(ServiceDescriptor.Singleton<IKeyProvider, KeyProvider>()); services.TryAdd(ServiceDescriptor.Singleton<ICacheKeyProvider, CacheKeyProvider>());
services.TryAdd(ServiceDescriptor.Singleton<ICacheabilityValidator, CacheabilityValidator>()); services.TryAdd(ServiceDescriptor.Singleton<ICacheabilityValidator, CacheabilityValidator>());
return services; return services;

View File

@ -0,0 +1,38 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
namespace Microsoft.AspNetCore.ResponseCaching
{
public interface ICacheKeyProvider
{
/// <summary>
/// Create a base key for storing items.
/// </summary>
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
/// <returns>The created base key.</returns>
string CreateStorageBaseKey(ResponseCachingContext context);
/// <summary>
/// Create one or more base keys for looking up items.
/// </summary>
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
/// <returns>An ordered <see cref="IEnumerable{T}"/> containing the base keys to try when looking up items.</returns>
IEnumerable<string> CreateLookupBaseKeys(ResponseCachingContext context);
/// <summary>
/// Create a vary key for storing items.
/// </summary>
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
/// <returns>The created vary key.</returns>
string CreateStorageVaryKey(ResponseCachingContext context);
/// <summary>
/// Create one or more vary keys for looking up items.
/// </summary>
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
/// <returns>An ordered <see cref="IEnumerable{T}"/> containing the vary keys to try when looking up items.</returns>
IEnumerable<string> CreateLookupVaryKeys(ResponseCachingContext context);
}
}

View File

@ -1,9 +1,6 @@
// 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;
using Microsoft.AspNetCore.Http.Headers;
namespace Microsoft.AspNetCore.ResponseCaching namespace Microsoft.AspNetCore.ResponseCaching
{ {
public interface ICacheabilityValidator public interface ICacheabilityValidator
@ -11,23 +8,22 @@ namespace Microsoft.AspNetCore.ResponseCaching
/// <summary> /// <summary>
/// Determine the cacheability of an HTTP request. /// Determine the cacheability of an HTTP request.
/// </summary> /// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param> /// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
/// <returns><c>true</c> if the request is cacheable; otherwise <c>false</c>.</returns> /// <returns><c>true</c> if the request is cacheable; otherwise <c>false</c>.</returns>
bool RequestIsCacheable(HttpContext httpContext); bool IsRequestCacheable(ResponseCachingContext context);
/// <summary> /// <summary>
/// Determine the cacheability of an HTTP response. /// Determine the cacheability of an HTTP response.
/// </summary> /// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param> /// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
/// <returns><c>true</c> if the response is cacheable; otherwise <c>false</c>.</returns> /// <returns><c>true</c> if the response is cacheable; otherwise <c>false</c>.</returns>
bool ResponseIsCacheable(HttpContext httpContext); bool IsResponseCacheable(ResponseCachingContext context);
/// <summary> /// <summary>
/// Determine the freshness of the cached entry. /// Determine the freshness of the cached entry.
/// </summary> /// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param> /// <param name="context">The <see cref="ResponseCachingContext"/>.</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> /// <returns><c>true</c> if the cached entry is fresh; otherwise <c>false</c>.</returns>
bool CachedEntryIsFresh(HttpContext httpContext, ResponseHeaders cachedResponseHeaders); bool IsCachedEntryFresh(ResponseCachingContext context);
} }
} }

View File

@ -1,41 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.ResponseCaching
{
public interface IKeyProvider
{
/// <summary>
/// Create a base key using the HTTP context for storing items.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
/// <returns>The created base key.</returns>
string CreateStorageBaseKey(HttpContext httpContext);
/// <summary>
/// Create one or more base keys using the HTTP context for looking up items.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
/// <returns>An ordered <see cref="IEnumerable{T}"/> containing the base keys to try when looking up items.</returns>
IEnumerable<string> CreateLookupBaseKey(HttpContext httpContext);
/// <summary>
/// Create a vary key using the HTTP context and vary rules for storing items.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
/// <param name="varyRules">The <see cref="VaryRules"/>.</param>
/// <returns>The created vary key.</returns>
string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules);
/// <summary>
/// Create one or more vary keys using the HTTP context and vary rules for looking up items.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
/// <param name="varyRules">The <see cref="VaryRules"/>.</param>
/// <returns>An ordered <see cref="IEnumerable{T}"/> containing the vary keys to try when looking up items.</returns>
IEnumerable<string> CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules);
}
}

View File

@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.ResponseCaching.Internal namespace Microsoft.AspNetCore.ResponseCaching.Internal
{ {
internal static class HttpContextInternalExtensions internal static class InternalHttpContextExtensions
{ {
internal static void AddResponseCachingFeature(this HttpContext httpContext) internal static void AddResponseCachingFeature(this HttpContext httpContext)
{ {
@ -21,24 +21,5 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
{ {
httpContext.Features.Set<ResponseCachingFeature>(null); httpContext.Features.Set<ResponseCachingFeature>(null);
} }
internal static void AddResponseCachingState(this HttpContext httpContext)
{
if (httpContext.GetResponseCachingState() != null)
{
throw new InvalidOperationException($"Another instance of {nameof(ResponseCachingState)} already exists. Only one instance of {nameof(ResponseCachingMiddleware)} can be configured for an application.");
}
httpContext.Features.Set(new ResponseCachingState(httpContext));
}
internal static void RemoveResponseCachingState(this HttpContext httpContext)
{
httpContext.Features.Set<ResponseCachingState>(null);
}
internal static ResponseCachingState GetResponseCachingState(this HttpContext httpContext)
{
return httpContext.Features.Get<ResponseCachingState>();
}
} }
} }

View File

@ -1,91 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
public class ResponseCachingState
{
private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue();
private readonly HttpContext _httpContext;
private RequestHeaders _requestHeaders;
private ResponseHeaders _responseHeaders;
private CacheControlHeaderValue _requestCacheControl;
private CacheControlHeaderValue _responseCacheControl;
internal ResponseCachingState(HttpContext httpContext)
{
_httpContext = httpContext;
}
public bool ShouldCacheResponse { get; internal set; }
public string StorageBaseKey { get; internal set; }
public string StorageVaryKey { get; internal set; }
public DateTimeOffset ResponseTime { get; internal set; }
public TimeSpan CachedEntryAge { get; internal set; }
public TimeSpan CachedResponseValidFor { get; internal set; }
internal CachedResponse CachedResponse { get; set; }
internal CachedVaryRules CachedVaryRules { get; set; }
public RequestHeaders RequestHeaders
{
get
{
if (_requestHeaders == null)
{
_requestHeaders = _httpContext.Request.GetTypedHeaders();
}
return _requestHeaders;
}
}
public ResponseHeaders ResponseHeaders
{
get
{
if (_responseHeaders == null)
{
_responseHeaders = _httpContext.Response.GetTypedHeaders();
}
return _responseHeaders;
}
}
public CacheControlHeaderValue RequestCacheControl
{
get
{
if (_requestCacheControl == null)
{
_requestCacheControl = RequestHeaders.CacheControl ?? EmptyCacheControl;
}
return _requestCacheControl;
}
}
public CacheControlHeaderValue ResponseCacheControl
{
get
{
if (_responseCacheControl == null)
{
_responseCacheControl = ResponseHeaders.CacheControl ?? EmptyCacheControl;
}
return _responseCacheControl;
}
}
}
}

View File

@ -3,356 +3,102 @@
using System; using System;
using System.IO; using System.IO;
using System.Globalization;
using System.Threading.Tasks;
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.Primitives;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.ResponseCaching namespace Microsoft.AspNetCore.ResponseCaching
{ {
internal class ResponseCachingContext public class ResponseCachingContext
{ {
private readonly HttpContext _httpContext; private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue();
private readonly IResponseCache _cache;
private readonly ResponseCachingOptions _options;
private readonly ICacheabilityValidator _cacheabilityValidator;
private readonly IKeyProvider _keyProvider;
private ResponseCachingState _state; private RequestHeaders _requestHeaders;
private ResponseHeaders _responseHeaders;
private CacheControlHeaderValue _requestCacheControl;
private CacheControlHeaderValue _responseCacheControl;
internal ResponseCachingContext( internal ResponseCachingContext(
HttpContext httpContext, HttpContext httpContext)
IResponseCache cache,
ResponseCachingOptions options,
ICacheabilityValidator cacheabilityValidator,
IKeyProvider keyProvider)
{ {
_httpContext = httpContext; HttpContext = httpContext;
_cache = cache;
_options = options;
_cacheabilityValidator = cacheabilityValidator;
_keyProvider = keyProvider;
} }
internal ResponseCachingState State public HttpContext HttpContext { get; }
{
get public bool ShouldCacheResponse { get; internal set; }
{
if (_state == null) public string StorageBaseKey { get; internal set; }
{
_state = _httpContext.GetResponseCachingState(); public string StorageVaryKey { get; internal set; }
}
return _state; public DateTimeOffset ResponseTime { get; internal set; }
}
} public TimeSpan CachedEntryAge { get; internal set; }
public TimeSpan CachedResponseValidFor { get; internal set; }
public CachedResponse CachedResponse { get; internal set; }
public CachedVaryRules CachedVaryRules { get; internal set; }
internal bool ResponseStarted { get; set; } internal bool ResponseStarted { get; set; }
private Stream OriginalResponseStream { get; set; } internal Stream OriginalResponseStream { get; set; }
private ResponseCacheStream ResponseCacheStream { get; set; } internal ResponseCacheStream ResponseCacheStream { get; set; }
private IHttpSendFileFeature OriginalSendFileFeature { get; set; } internal IHttpSendFileFeature OriginalSendFileFeature { get; set; }
internal async Task<bool> TryServeCachedResponseAsync(CachedResponse cachedResponse) internal ResponseHeaders CachedResponseHeaders { get; set; }
internal RequestHeaders TypedRequestHeaders
{ {
State.CachedResponse = cachedResponse; get
var cachedResponseHeaders = new ResponseHeaders(State.CachedResponse.Headers);
State.ResponseTime = _options.SystemClock.UtcNow;
var cachedEntryAge = State.ResponseTime - State.CachedResponse.Created;
State.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero;
if (_cacheabilityValidator.CachedEntryIsFresh(_httpContext, cachedResponseHeaders))
{ {
// Check conditional request rules if (_requestHeaders == null)
if (ConditionalRequestSatisfied(cachedResponseHeaders))
{ {
_httpContext.Response.StatusCode = StatusCodes.Status304NotModified; _requestHeaders = HttpContext.Request.GetTypedHeaders();
} }
else return _requestHeaders;
{
var response = _httpContext.Response;
// Copy the cached status code and response headers
response.StatusCode = State.CachedResponse.StatusCode;
foreach (var header in State.CachedResponse.Headers)
{
response.Headers.Add(header);
}
response.Headers[HeaderNames.Age] = State.CachedEntryAge.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture);
var body = State.CachedResponse.Body ??
((CachedResponseBody)_cache.Get(State.CachedResponse.BodyKeyPrefix))?.Body;
// If the body is not found, something went wrong.
if (body == null)
{
return false;
}
// Copy the cached response body
if (body.Length > 0)
{
// Add a content-length if required
if (response.ContentLength == null && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding]))
{
response.ContentLength = body.Length;
}
await response.Body.WriteAsync(body, 0, body.Length);
}
}
return true;
}
else
{
// TODO: Validate with endpoint instead
}
return false;
}
internal async Task<bool> TryServeFromCacheAsync()
{
foreach (var baseKey in _keyProvider.CreateLookupBaseKey(_httpContext))
{
var cacheEntry = _cache.Get(baseKey);
if (cacheEntry is CachedVaryRules)
{
// Request contains vary rules, recompute key(s) and try again
State.CachedVaryRules = cacheEntry as CachedVaryRules;
foreach (var varyKey in _keyProvider.CreateLookupVaryKey(_httpContext, State.CachedVaryRules.VaryRules))
{
cacheEntry = _cache.Get(varyKey);
if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(cacheEntry as CachedResponse))
{
return true;
}
}
}
if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(cacheEntry as CachedResponse))
{
return true;
}
}
if (State.RequestCacheControl.OnlyIfCached)
{
_httpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
return true;
}
return false;
}
internal bool ConditionalRequestSatisfied(ResponseHeaders cachedResponseHeaders)
{
var ifNoneMatchHeader = State.RequestHeaders.IfNoneMatch;
if (ifNoneMatchHeader != null)
{
if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any))
{
return true;
}
if (cachedResponseHeaders.ETag != null)
{
foreach (var tag in ifNoneMatchHeader)
{
if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: true))
{
return true;
}
}
}
}
else if ((cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= State.RequestHeaders.IfUnmodifiedSince)
{
return true;
}
return false;
}
internal void FinalizeCachingHeaders()
{
if (_cacheabilityValidator.ResponseIsCacheable(_httpContext))
{
State.ShouldCacheResponse = true;
State.StorageBaseKey = _keyProvider.CreateStorageBaseKey(_httpContext);
// Create the cache entry now
var response = _httpContext.Response;
var varyHeaderValue = response.Headers[HeaderNames.Vary];
var varyParamsValue = _httpContext.GetResponseCachingFeature()?.VaryParams ?? StringValues.Empty;
State.CachedResponseValidFor = State.ResponseCacheControl.SharedMaxAge
?? State.ResponseCacheControl.MaxAge
?? (State.ResponseHeaders.Expires - State.ResponseTime)
// TODO: Heuristics for expiration?
?? TimeSpan.FromSeconds(10);
// Check if any vary rules exist
if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue))
{
// Normalize order and casing of vary by rules
var normalizedVaryHeaderValue = GetNormalizedStringValues(varyHeaderValue);
var normalizedVaryParamsValue = GetNormalizedStringValues(varyParamsValue);
// Update vary rules if they are different
if (State.CachedVaryRules == null ||
!StringValues.Equals(State.CachedVaryRules.VaryRules.Params, normalizedVaryParamsValue) ||
!StringValues.Equals(State.CachedVaryRules.VaryRules.Headers, normalizedVaryHeaderValue))
{
var cachedVaryRules = new CachedVaryRules
{
VaryKeyPrefix = FastGuid.NewGuid().IdString,
VaryRules = new VaryRules()
{
// TODO: Vary Encoding
Headers = normalizedVaryHeaderValue,
Params = normalizedVaryParamsValue
}
};
State.CachedVaryRules = cachedVaryRules;
_cache.Set(State.StorageBaseKey, cachedVaryRules, State.CachedResponseValidFor);
}
State.StorageVaryKey = _keyProvider.CreateStorageVaryKey(_httpContext, State.CachedVaryRules.VaryRules);
}
// Ensure date header is set
if (State.ResponseHeaders.Date == null)
{
State.ResponseHeaders.Date = State.ResponseTime;
}
// Store the response on the state
State.CachedResponse = new CachedResponse
{
BodyKeyPrefix = FastGuid.NewGuid().IdString,
Created = State.ResponseHeaders.Date.Value,
StatusCode = _httpContext.Response.StatusCode
};
foreach (var header in State.ResponseHeaders.Headers)
{
if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase))
{
State.CachedResponse.Headers.Add(header);
}
}
}
else
{
ResponseCacheStream.DisableBuffering();
} }
} }
internal void FinalizeCachingBody() internal ResponseHeaders TypedResponseHeaders
{ {
if (State.ShouldCacheResponse && get
ResponseCacheStream.BufferingEnabled &&
(State.ResponseHeaders.ContentLength == null ||
State.ResponseHeaders.ContentLength == ResponseCacheStream.BufferedStream.Length))
{ {
if (ResponseCacheStream.BufferedStream.Length >= _options.MinimumSplitBodySize) if (_responseHeaders == null)
{ {
// Store response and response body separately _responseHeaders = HttpContext.Response.GetTypedHeaders();
_cache.Set(State.StorageVaryKey ?? State.StorageBaseKey, State.CachedResponse, State.CachedResponseValidFor);
var cachedResponseBody = new CachedResponseBody()
{
Body = ResponseCacheStream.BufferedStream.ToArray()
};
_cache.Set(State.CachedResponse.BodyKeyPrefix, cachedResponseBody, State.CachedResponseValidFor);
} }
else return _responseHeaders;
}
}
internal CacheControlHeaderValue RequestCacheControlHeaderValue
{
get
{
if (_requestCacheControl == null)
{ {
// Store response and response body together _requestCacheControl = TypedRequestHeaders.CacheControl ?? EmptyCacheControl;
State.CachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray();
_cache.Set(State.StorageVaryKey ?? State.StorageBaseKey, State.CachedResponse, State.CachedResponseValidFor);
} }
return _requestCacheControl;
} }
} }
internal void OnResponseStarting() internal CacheControlHeaderValue ResponseCacheControlHeaderValue
{ {
if (!ResponseStarted) get
{ {
ResponseStarted = true; if (_responseCacheControl == null)
State.ResponseTime = _options.SystemClock.UtcNow;
FinalizeCachingHeaders();
}
}
internal void ShimResponseStream()
{
// TODO: Consider caching large responses on disk and serving them from there.
// Shim response stream
OriginalResponseStream = _httpContext.Response.Body;
ResponseCacheStream = new ResponseCacheStream(OriginalResponseStream, _options.MaximumCachedBodySize);
_httpContext.Response.Body = ResponseCacheStream;
// Shim IHttpSendFileFeature
OriginalSendFileFeature = _httpContext.Features.Get<IHttpSendFileFeature>();
if (OriginalSendFileFeature != null)
{
_httpContext.Features.Set<IHttpSendFileFeature>(new SendFileFeatureWrapper(OriginalSendFileFeature, ResponseCacheStream));
}
// TODO: Move this temporary interface with endpoint to HttpAbstractions
_httpContext.AddResponseCachingFeature();
}
internal void UnshimResponseStream()
{
// Unshim response stream
_httpContext.Response.Body = OriginalResponseStream;
// Unshim IHttpSendFileFeature
_httpContext.Features.Set(OriginalSendFileFeature);
// TODO: Move this temporary interface with endpoint to HttpAbstractions
_httpContext.RemoveResponseCachingFeature();
}
// Normalize order and casing
internal static StringValues GetNormalizedStringValues(StringValues stringVales)
{
if (stringVales.Count == 1)
{
return new StringValues(stringVales.ToString().ToUpperInvariant());
}
else
{
var originalArray = stringVales.ToArray();
var newArray = new string[originalArray.Length];
for (int i = 0; i < originalArray.Length; i++)
{ {
newArray[i] = originalArray[i].ToUpperInvariant(); _responseCacheControl = TypedResponseHeaders.CacheControl ?? EmptyCacheControl;
} }
return _responseCacheControl;
// Since the casing has already been normalized, use Ordinal comparison
Array.Sort(newArray, StringComparer.Ordinal);
return new StringValues(newArray);
} }
} }
} }

View File

@ -2,35 +2,37 @@
// 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.Globalization;
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.Headers;
using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.ResponseCaching namespace Microsoft.AspNetCore.ResponseCaching
{ {
public class ResponseCachingMiddleware public class ResponseCachingMiddleware
{ {
private static readonly Func<object, Task> OnStartingCallback = state => private static readonly TimeSpan DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10);
{
((ResponseCachingContext)state).OnResponseStarting();
return TaskCache.CompletedTask;
};
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 ICacheabilityValidator _cacheabilityValidator; private readonly ICacheabilityValidator _cacheabilityValidator;
private readonly IKeyProvider _keyProvider; private readonly ICacheKeyProvider _cacheKeyProvider;
private readonly Func<object, Task> _onStartingCallback;
public ResponseCachingMiddleware( public ResponseCachingMiddleware(
RequestDelegate next, RequestDelegate next,
IResponseCache cache, IResponseCache cache,
IOptions<ResponseCachingOptions> options, IOptions<ResponseCachingOptions> options,
ICacheabilityValidator cacheabilityValidator, ICacheabilityValidator cacheabilityValidator,
IKeyProvider keyProvider) ICacheKeyProvider cacheKeyProvider)
{ {
if (next == null) if (next == null)
{ {
@ -48,70 +50,352 @@ namespace Microsoft.AspNetCore.ResponseCaching
{ {
throw new ArgumentNullException(nameof(cacheabilityValidator)); throw new ArgumentNullException(nameof(cacheabilityValidator));
} }
if (keyProvider == null) if (cacheKeyProvider == null)
{ {
throw new ArgumentNullException(nameof(keyProvider)); throw new ArgumentNullException(nameof(cacheKeyProvider));
} }
_next = next; _next = next;
_cache = cache; _cache = cache;
_options = options.Value; _options = options.Value;
_cacheabilityValidator = cacheabilityValidator; _cacheabilityValidator = cacheabilityValidator;
_keyProvider = keyProvider; _cacheKeyProvider = cacheKeyProvider;
_onStartingCallback = state =>
{
OnResponseStarting((ResponseCachingContext)state);
return TaskCache.CompletedTask;
};
} }
public async Task Invoke(HttpContext context) public async Task Invoke(HttpContext httpContext)
{ {
context.AddResponseCachingState(); var context = new ResponseCachingContext(httpContext);
try // Should we attempt any caching logic?
if (_cacheabilityValidator.IsRequestCacheable(context))
{ {
var cachingContext = new ResponseCachingContext( // Can this request be served from cache?
context, if (await TryServeFromCacheAsync(context))
_cache,
_options,
_cacheabilityValidator,
_keyProvider);
// Should we attempt any caching logic?
if (_cacheabilityValidator.RequestIsCacheable(context))
{ {
// Can this request be served from cache? return;
if (await cachingContext.TryServeFromCacheAsync()) }
{
return;
}
// Hook up to listen to the response stream // Hook up to listen to the response stream
cachingContext.ShimResponseStream(); ShimResponseStream(context);
try try
{ {
// Subscribe to OnStarting event // Subscribe to OnStarting event
context.Response.OnStarting(OnStartingCallback, cachingContext); httpContext.Response.OnStarting(_onStartingCallback, context);
await _next(context); await _next(httpContext);
// If there was no response body, check the response headers now. We can cache things like redirects. // If there was no response body, check the response headers now. We can cache things like redirects.
cachingContext.OnResponseStarting(); OnResponseStarting(context);
// Finalize the cache entry // Finalize the cache entry
cachingContext.FinalizeCachingBody(); FinalizeCachingBody(context);
} }
finally finally
{ {
cachingContext.UnshimResponseStream(); UnshimResponseStream(context);
} }
}
else
{
// TODO: Invalidate resources for successful unsafe methods? Required by RFC
await _next(httpContext);
}
}
internal async Task<bool> TryServeCachedResponseAsync(ResponseCachingContext context, CachedResponse cachedResponse)
{
context.CachedResponse = cachedResponse;
context.CachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers);
context.ResponseTime = _options.SystemClock.UtcNow;
var cachedEntryAge = context.ResponseTime - context.CachedResponse.Created;
context.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero;
if (_cacheabilityValidator.IsCachedEntryFresh(context))
{
// Check conditional request rules
if (ConditionalRequestSatisfied(context))
{
context.HttpContext.Response.StatusCode = StatusCodes.Status304NotModified;
} }
else else
{ {
// TODO: Invalidate resources for successful unsafe methods? Required by RFC var response = context.HttpContext.Response;
await _next(context); // Copy the cached status code and response headers
response.StatusCode = context.CachedResponse.StatusCode;
foreach (var header in context.CachedResponse.Headers)
{
response.Headers.Add(header);
}
response.Headers[HeaderNames.Age] = context.CachedEntryAge.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture);
var body = context.CachedResponse.Body ??
((CachedResponseBody)_cache.Get(context.CachedResponse.BodyKeyPrefix))?.Body;
// If the body is not found, something went wrong.
if (body == null)
{
return false;
}
// Copy the cached response body
if (body.Length > 0)
{
// Add a content-length if required
if (response.ContentLength == null && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding]))
{
response.ContentLength = body.Length;
}
await response.Body.WriteAsync(body, 0, body.Length);
}
}
return true;
}
else
{
// TODO: Validate with endpoint instead
}
return false;
}
internal async Task<bool> TryServeFromCacheAsync(ResponseCachingContext context)
{
foreach (var baseKey in _cacheKeyProvider.CreateLookupBaseKeys(context))
{
var cacheEntry = _cache.Get(baseKey);
if (cacheEntry is CachedVaryRules)
{
// Request contains vary rules, recompute key(s) and try again
context.CachedVaryRules = (CachedVaryRules)cacheEntry;
foreach (var varyKey in _cacheKeyProvider.CreateLookupVaryKeys(context))
{
cacheEntry = _cache.Get(varyKey);
if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(context, (CachedResponse)cacheEntry))
{
return true;
}
}
}
if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(context, (CachedResponse)cacheEntry))
{
return true;
} }
} }
finally
if (context.RequestCacheControlHeaderValue.OnlyIfCached)
{ {
context.RemoveResponseCachingState(); context.HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
return true;
}
return false;
}
internal void FinalizeCachingHeaders(ResponseCachingContext context)
{
if (_cacheabilityValidator.IsResponseCacheable(context))
{
context.ShouldCacheResponse = true;
context.StorageBaseKey = _cacheKeyProvider.CreateStorageBaseKey(context);
// Create the cache entry now
var response = context.HttpContext.Response;
var varyHeaderValue = response.Headers[HeaderNames.Vary];
var varyParamsValue = context.HttpContext.GetResponseCachingFeature()?.VaryParams ?? StringValues.Empty;
context.CachedResponseValidFor = context.ResponseCacheControlHeaderValue.SharedMaxAge ??
context.ResponseCacheControlHeaderValue.MaxAge ??
(context.TypedResponseHeaders.Expires - context.ResponseTime) ??
DefaultExpirationTimeSpan;
// Check if any vary rules exist
if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue))
{
// Normalize order and casing of vary by rules
var normalizedVaryHeaderValue = GetNormalizedStringValues(varyHeaderValue);
var normalizedVaryParamsValue = GetNormalizedStringValues(varyParamsValue);
// Update vary rules if they are different
if (context.CachedVaryRules == null ||
!StringValues.Equals(context.CachedVaryRules.Params, normalizedVaryParamsValue) ||
!StringValues.Equals(context.CachedVaryRules.Headers, normalizedVaryHeaderValue))
{
context.CachedVaryRules = new CachedVaryRules
{
VaryKeyPrefix = FastGuid.NewGuid().IdString,
Headers = normalizedVaryHeaderValue,
Params = normalizedVaryParamsValue
};
_cache.Set(context.StorageBaseKey, context.CachedVaryRules, context.CachedResponseValidFor);
}
context.StorageVaryKey = _cacheKeyProvider.CreateStorageVaryKey(context);
}
// Ensure date header is set
if (context.TypedResponseHeaders.Date == null)
{
context.TypedResponseHeaders.Date = context.ResponseTime;
}
// Store the response on the state
context.CachedResponse = new CachedResponse
{
BodyKeyPrefix = FastGuid.NewGuid().IdString,
Created = context.TypedResponseHeaders.Date.Value,
StatusCode = context.HttpContext.Response.StatusCode
};
foreach (var header in context.TypedResponseHeaders.Headers)
{
if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase))
{
context.CachedResponse.Headers.Add(header);
}
}
}
else
{
context.ResponseCacheStream.DisableBuffering();
}
}
internal void FinalizeCachingBody(ResponseCachingContext context)
{
if (context.ShouldCacheResponse &&
context.ResponseCacheStream.BufferingEnabled &&
(context.TypedResponseHeaders.ContentLength == null ||
context.TypedResponseHeaders.ContentLength == context.ResponseCacheStream.BufferedStream.Length))
{
if (context.ResponseCacheStream.BufferedStream.Length >= _options.MinimumSplitBodySize)
{
// Store response and response body separately
_cache.Set(context.StorageVaryKey ?? context.StorageBaseKey, context.CachedResponse, context.CachedResponseValidFor);
var cachedResponseBody = new CachedResponseBody()
{
Body = context.ResponseCacheStream.BufferedStream.ToArray()
};
_cache.Set(context.CachedResponse.BodyKeyPrefix, cachedResponseBody, context.CachedResponseValidFor);
}
else
{
// Store response and response body together
context.CachedResponse.Body = context.ResponseCacheStream.BufferedStream.ToArray();
_cache.Set(context.StorageVaryKey ?? context.StorageBaseKey, context.CachedResponse, context.CachedResponseValidFor);
}
}
}
internal void OnResponseStarting(ResponseCachingContext context)
{
if (!context.ResponseStarted)
{
context.ResponseStarted = true;
context.ResponseTime = _options.SystemClock.UtcNow;
FinalizeCachingHeaders(context);
}
}
internal void ShimResponseStream(ResponseCachingContext context)
{
// TODO: Consider caching large responses on disk and serving them from there.
// Shim response stream
context.OriginalResponseStream = context.HttpContext.Response.Body;
context.ResponseCacheStream = new ResponseCacheStream(context.OriginalResponseStream, _options.MaximumCachedBodySize);
context.HttpContext.Response.Body = context.ResponseCacheStream;
// Shim IHttpSendFileFeature
context.OriginalSendFileFeature = context.HttpContext.Features.Get<IHttpSendFileFeature>();
if (context.OriginalSendFileFeature != null)
{
context.HttpContext.Features.Set<IHttpSendFileFeature>(new SendFileFeatureWrapper(context.OriginalSendFileFeature, context.ResponseCacheStream));
}
// TODO: Move this temporary interface with endpoint to HttpAbstractions
context.HttpContext.AddResponseCachingFeature();
}
internal static void UnshimResponseStream(ResponseCachingContext context)
{
// Unshim response stream
context.HttpContext.Response.Body = context.OriginalResponseStream;
// Unshim IHttpSendFileFeature
context.HttpContext.Features.Set(context.OriginalSendFileFeature);
// TODO: Move this temporary interface with endpoint to HttpAbstractions
context.HttpContext.RemoveResponseCachingFeature();
}
internal static bool ConditionalRequestSatisfied(ResponseCachingContext context)
{
var cachedResponseHeaders = context.CachedResponseHeaders;
var ifNoneMatchHeader = context.TypedRequestHeaders.IfNoneMatch;
if (ifNoneMatchHeader != null)
{
if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any))
{
return true;
}
if (cachedResponseHeaders.ETag != null)
{
foreach (var tag in ifNoneMatchHeader)
{
if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: true))
{
return true;
}
}
}
}
else if ((cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= context.TypedRequestHeaders.IfUnmodifiedSince)
{
return true;
}
return false;
}
// Normalize order and casing
internal static StringValues GetNormalizedStringValues(StringValues stringValues)
{
if (stringValues.Count == 1)
{
return new StringValues(stringValues.ToString().ToUpperInvariant());
}
else
{
var originalArray = stringValues.ToArray();
var newArray = new string[originalArray.Length];
for (int i = 0; i < originalArray.Length; i++)
{
newArray[i] = originalArray[i].ToUpperInvariant();
}
// Since the casing has already been normalized, use Ordinal comparison
Array.Sort(newArray, StringComparer.Ordinal);
return new StringValues(newArray);
} }
} }
} }

View File

@ -1,13 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.ResponseCaching
{
public class VaryRules
{
internal StringValues Headers { get; set; }
internal StringValues Params { get; set; }
}
}

View File

@ -81,8 +81,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{ {
var cachedVaryRule = new CachedVaryRules() var cachedVaryRule = new CachedVaryRules()
{ {
VaryKeyPrefix = FastGuid.NewGuid().IdString, VaryKeyPrefix = FastGuid.NewGuid().IdString
VaryRules = new VaryRules()
}; };
AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
@ -95,10 +94,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var cachedVaryRule = new CachedVaryRules() var cachedVaryRule = new CachedVaryRules()
{ {
VaryKeyPrefix = FastGuid.NewGuid().IdString, VaryKeyPrefix = FastGuid.NewGuid().IdString,
VaryRules = new VaryRules() Headers = headers
{
Headers = headers
}
}; };
AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
@ -111,10 +107,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var cachedVaryRule = new CachedVaryRules() var cachedVaryRule = new CachedVaryRules()
{ {
VaryKeyPrefix = FastGuid.NewGuid().IdString, VaryKeyPrefix = FastGuid.NewGuid().IdString,
VaryRules = new VaryRules() Params = param
{
Params = param
}
}; };
AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
@ -128,11 +121,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var cachedVaryRule = new CachedVaryRules() var cachedVaryRule = new CachedVaryRules()
{ {
VaryKeyPrefix = FastGuid.NewGuid().IdString, VaryKeyPrefix = FastGuid.NewGuid().IdString,
VaryRules = new VaryRules() Headers = headers,
{ Params = param
Headers = headers,
Params = param
}
}; };
AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
@ -145,10 +135,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var cachedVaryRule = new CachedVaryRules() var cachedVaryRule = new CachedVaryRules()
{ {
VaryKeyPrefix = FastGuid.NewGuid().IdString, VaryKeyPrefix = FastGuid.NewGuid().IdString,
VaryRules = new VaryRules() Headers = headers
{
Headers = headers
}
}; };
var serializedEntry = CacheEntrySerializer.Serialize(cachedVaryRule); var serializedEntry = CacheEntrySerializer.Serialize(cachedVaryRule);
Array.Reverse(serializedEntry); Array.Reverse(serializedEntry);
@ -188,8 +175,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Assert.NotNull(actual); Assert.NotNull(actual);
Assert.NotNull(expected); Assert.NotNull(expected);
Assert.Equal(expected.VaryKeyPrefix, actual.VaryKeyPrefix); Assert.Equal(expected.VaryKeyPrefix, actual.VaryKeyPrefix);
Assert.Equal(expected.VaryRules.Headers, actual.VaryRules.Headers); Assert.Equal(expected.Headers, actual.Headers);
Assert.Equal(expected.VaryRules.Params, actual.VaryRules.Params); Assert.Equal(expected.Params, actual.Params);
} }
} }
} }

View File

@ -4,7 +4,6 @@
using System; using System;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.Http.Headers;
using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Xunit; using Xunit;
@ -17,10 +16,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[InlineData("HEAD")] [InlineData("HEAD")]
public void RequestIsCacheable_CacheableMethods_Allowed(string method) public void RequestIsCacheable_CacheableMethods_Allowed(string method)
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Request.Method = method; context.HttpContext.Request.Method = method;
Assert.True(new CacheabilityValidator().RequestIsCacheable(httpContext)); Assert.True(new CacheabilityValidator().IsRequestCacheable(context));
} }
[Theory] [Theory]
@ -34,182 +33,182 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[InlineData(null)] [InlineData(null)]
public void RequestIsCacheable_UncacheableMethods_NotAllowed(string method) public void RequestIsCacheable_UncacheableMethods_NotAllowed(string method)
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Request.Method = method; context.HttpContext.Request.Method = method;
Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext)); Assert.False(new CacheabilityValidator().IsRequestCacheable(context));
} }
[Fact] [Fact]
public void RequestIsCacheable_AuthorizationHeaders_NotAllowed() public void RequestIsCacheable_AuthorizationHeaders_NotAllowed()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Request.Method = "GET"; context.HttpContext.Request.Method = "GET";
httpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW"; context.HttpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW";
Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext)); Assert.False(new CacheabilityValidator().IsRequestCacheable(context));
} }
[Fact] [Fact]
public void RequestIsCacheable_NoCache_NotAllowed() public void RequestIsCacheable_NoCache_NotAllowed()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Request.Method = "GET"; context.HttpContext.Request.Method = "GET";
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
{ {
NoCache = true NoCache = true
}; };
Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext)); Assert.False(new CacheabilityValidator().IsRequestCacheable(context));
} }
[Fact] [Fact]
public void RequestIsCacheable_NoStore_Allowed() public void RequestIsCacheable_NoStore_Allowed()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Request.Method = "GET"; context.HttpContext.Request.Method = "GET";
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
{ {
NoStore = true NoStore = true
}; };
Assert.True(new CacheabilityValidator().RequestIsCacheable(httpContext)); Assert.True(new CacheabilityValidator().IsRequestCacheable(context));
} }
[Fact] [Fact]
public void RequestIsCacheable_LegacyDirectives_NotAllowed() public void RequestIsCacheable_LegacyDirectives_NotAllowed()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Request.Method = "GET"; context.HttpContext.Request.Method = "GET";
httpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache";
Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext)); Assert.False(new CacheabilityValidator().IsRequestCacheable(context));
} }
[Fact] [Fact]
public void RequestIsCacheable_LegacyDirectives_OverridenByCacheControl() public void RequestIsCacheable_LegacyDirectives_OverridenByCacheControl()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Request.Method = "GET"; context.HttpContext.Request.Method = "GET";
httpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache";
httpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10"; context.HttpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10";
Assert.True(new CacheabilityValidator().RequestIsCacheable(httpContext)); Assert.True(new CacheabilityValidator().IsRequestCacheable(context));
} }
[Fact] [Fact]
public void ResponseIsCacheable_NoPublic_NotAllowed() public void ResponseIsCacheable_NoPublic_NotAllowed()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
} }
[Fact] [Fact]
public void ResponseIsCacheable_Public_Allowed() public void ResponseIsCacheable_Public_Allowed()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
Public = true Public = true
}; };
Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); Assert.True(new CacheabilityValidator().IsResponseCacheable(context));
} }
[Fact] [Fact]
public void ResponseIsCacheable_NoCache_NotAllowed() public void ResponseIsCacheable_NoCache_NotAllowed()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
Public = true, Public = true,
NoCache = true NoCache = true
}; };
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
} }
[Fact] [Fact]
public void ResponseIsCacheable_RequestNoStore_NotAllowed() public void ResponseIsCacheable_RequestNoStore_NotAllowed()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
{ {
NoStore = true NoStore = true
}; };
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
Public = true Public = true
}; };
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
} }
[Fact] [Fact]
public void ResponseIsCacheable_ResponseNoStore_NotAllowed() public void ResponseIsCacheable_ResponseNoStore_NotAllowed()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
Public = true, Public = true,
NoStore = true NoStore = true
}; };
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
} }
[Fact] [Fact]
public void ResponseIsCacheable_SetCookieHeader_NotAllowed() public void ResponseIsCacheable_SetCookieHeader_NotAllowed()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
Public = true Public = true
}; };
httpContext.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; context.HttpContext.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue";
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
} }
[Fact] [Fact]
public void ResponseIsCacheable_VaryHeaderByStar_NotAllowed() public void ResponseIsCacheable_VaryHeaderByStar_NotAllowed()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
Public = true Public = true
}; };
httpContext.Response.Headers[HeaderNames.Vary] = "*"; context.HttpContext.Response.Headers[HeaderNames.Vary] = "*";
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
} }
[Fact] [Fact]
public void ResponseIsCacheable_Private_NotAllowed() public void ResponseIsCacheable_Private_NotAllowed()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
Public = true, Public = true,
Private = true Private = true
}; };
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
} }
[Theory] [Theory]
[InlineData(StatusCodes.Status200OK)] [InlineData(StatusCodes.Status200OK)]
public void ResponseIsCacheable_SuccessStatusCodes_Allowed(int statusCode) public void ResponseIsCacheable_SuccessStatusCodes_Allowed(int statusCode)
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Response.StatusCode = statusCode; context.HttpContext.Response.StatusCode = statusCode;
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
Public = true Public = true
}; };
Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); Assert.True(new CacheabilityValidator().IsResponseCacheable(context));
} }
[Theory] [Theory]
@ -263,146 +262,141 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[InlineData(StatusCodes.Status507InsufficientStorage)] [InlineData(StatusCodes.Status507InsufficientStorage)]
public void ResponseIsCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode) public void ResponseIsCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode)
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Response.StatusCode = statusCode; context.HttpContext.Response.StatusCode = statusCode;
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
Public = true Public = true
}; };
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
} }
[Fact] [Fact]
public void ResponseIsCacheable_NoExpiryRequirements_IsAllowed() public void ResponseIsCacheable_NoExpiryRequirements_IsAllowed()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Response.StatusCode = StatusCodes.Status200OK; context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
var headers = httpContext.Response.GetTypedHeaders(); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
headers.CacheControl = new CacheControlHeaderValue()
{ {
Public = true Public = true
}; };
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
headers.Date = utcNow; context.TypedResponseHeaders.Date = utcNow;
httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; context.ResponseTime = DateTimeOffset.MaxValue;
Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); Assert.True(new CacheabilityValidator().IsResponseCacheable(context));
} }
[Fact] [Fact]
public void ResponseIsCacheable_PastExpiry_NotAllowed() public void ResponseIsCacheable_PastExpiry_NotAllowed()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Response.StatusCode = StatusCodes.Status200OK; context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
var headers = httpContext.Response.GetTypedHeaders(); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
headers.CacheControl = new CacheControlHeaderValue()
{ {
Public = true Public = true
}; };
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
headers.Expires = utcNow; context.TypedResponseHeaders.Expires = utcNow;
headers.Date = utcNow; context.TypedResponseHeaders.Date = utcNow;
httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; context.ResponseTime = DateTimeOffset.MaxValue;
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
} }
[Fact] [Fact]
public void ResponseIsCacheable_MaxAgeOverridesExpiry_ToAllowed() public void ResponseIsCacheable_MaxAgeOverridesExpiry_ToAllowed()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Response.StatusCode = StatusCodes.Status200OK; context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
var headers = httpContext.Response.GetTypedHeaders(); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
headers.CacheControl = new CacheControlHeaderValue()
{ {
Public = true, Public = true,
MaxAge = TimeSpan.FromSeconds(10) MaxAge = TimeSpan.FromSeconds(10)
}; };
headers.Expires = utcNow; context.TypedResponseHeaders.Expires = utcNow;
headers.Date = utcNow; context.TypedResponseHeaders.Date = utcNow;
httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(9); context.ResponseTime = utcNow + TimeSpan.FromSeconds(9);
Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); Assert.True(new CacheabilityValidator().IsResponseCacheable(context));
} }
[Fact] [Fact]
public void ResponseIsCacheable_MaxAgeOverridesExpiry_ToNotAllowed() public void ResponseIsCacheable_MaxAgeOverridesExpiry_ToNotAllowed()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Response.StatusCode = StatusCodes.Status200OK; context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
var headers = httpContext.Response.GetTypedHeaders(); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
headers.CacheControl = new CacheControlHeaderValue()
{ {
Public = true, Public = true,
MaxAge = TimeSpan.FromSeconds(10) MaxAge = TimeSpan.FromSeconds(10)
}; };
headers.Expires = utcNow; context.TypedResponseHeaders.Expires = utcNow;
headers.Date = utcNow; context.TypedResponseHeaders.Date = utcNow;
httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(11); context.ResponseTime = utcNow + TimeSpan.FromSeconds(11);
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
} }
[Fact] [Fact]
public void ResponseIsCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed() public void ResponseIsCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Response.StatusCode = StatusCodes.Status200OK; context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
var headers = httpContext.Response.GetTypedHeaders(); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
headers.CacheControl = new CacheControlHeaderValue()
{ {
Public = true, Public = true,
MaxAge = TimeSpan.FromSeconds(10), MaxAge = TimeSpan.FromSeconds(10),
SharedMaxAge = TimeSpan.FromSeconds(15) SharedMaxAge = TimeSpan.FromSeconds(15)
}; };
headers.Date = utcNow; context.TypedResponseHeaders.Date = utcNow;
httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(11); context.ResponseTime = utcNow + TimeSpan.FromSeconds(11);
Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); Assert.True(new CacheabilityValidator().IsResponseCacheable(context));
} }
[Fact] [Fact]
public void ResponseIsCacheable_SharedMaxAgeOverridesMaxAge_ToNotFresh() public void ResponseIsCacheable_SharedMaxAgeOverridesMaxAge_ToNotFresh()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Response.StatusCode = StatusCodes.Status200OK; context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
var headers = httpContext.Response.GetTypedHeaders(); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
headers.CacheControl = new CacheControlHeaderValue()
{ {
Public = true, Public = true,
MaxAge = TimeSpan.FromSeconds(10), MaxAge = TimeSpan.FromSeconds(10),
SharedMaxAge = TimeSpan.FromSeconds(5) SharedMaxAge = TimeSpan.FromSeconds(5)
}; };
headers.Date = utcNow; context.TypedResponseHeaders.Date = utcNow;
httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(6); context.ResponseTime = utcNow + TimeSpan.FromSeconds(6);
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
} }
[Fact] [Fact]
public void EntryIsFresh_NoCachedCacheControl_FallsbackToEmptyCacheControl() public void EntryIsFresh_NoCachedCacheControl_FallsbackToEmptyCacheControl()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; context.ResponseTime = DateTimeOffset.MaxValue;
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, new ResponseHeaders(new HeaderDictionary()))); Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context));
} }
[Fact] [Fact]
public void EntryIsFresh_NoExpiryRequirements_IsFresh() public void EntryIsFresh_NoExpiryRequirements_IsFresh()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; context.ResponseTime = DateTimeOffset.MaxValue;
var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
CacheControl = new CacheControlHeaderValue() CacheControl = new CacheControlHeaderValue()
{ {
@ -410,15 +404,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
}; };
Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context));
} }
[Fact] [Fact]
public void EntryIsFresh_PastExpiry_IsNotFresh() public void EntryIsFresh_PastExpiry_IsNotFresh()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; context.ResponseTime = DateTimeOffset.MaxValue;
var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
CacheControl = new CacheControlHeaderValue() CacheControl = new CacheControlHeaderValue()
{ {
@ -427,18 +421,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Expires = DateTimeOffset.UtcNow Expires = DateTimeOffset.UtcNow
}; };
Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context));
} }
[Fact] [Fact]
public void EntryIsFresh_MaxAgeOverridesExpiry_ToFresh() public void EntryIsFresh_MaxAgeOverridesExpiry_ToFresh()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
var state = httpContext.GetResponseCachingState(); context.CachedEntryAge = TimeSpan.FromSeconds(9);
state.CachedEntryAge = TimeSpan.FromSeconds(9); context.ResponseTime = utcNow + context.CachedEntryAge;
state.ResponseTime = utcNow + state.CachedEntryAge; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
CacheControl = new CacheControlHeaderValue() CacheControl = new CacheControlHeaderValue()
{ {
@ -448,18 +441,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Expires = utcNow Expires = utcNow
}; };
Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context));
} }
[Fact] [Fact]
public void EntryIsFresh_MaxAgeOverridesExpiry_ToNotFresh() public void EntryIsFresh_MaxAgeOverridesExpiry_ToNotFresh()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
var state = httpContext.GetResponseCachingState(); context.CachedEntryAge = TimeSpan.FromSeconds(11);
state.CachedEntryAge = TimeSpan.FromSeconds(11); context.ResponseTime = utcNow + context.CachedEntryAge;
state.ResponseTime = utcNow + state.CachedEntryAge; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
CacheControl = new CacheControlHeaderValue() CacheControl = new CacheControlHeaderValue()
{ {
@ -469,18 +461,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Expires = utcNow Expires = utcNow
}; };
Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context));
} }
[Fact] [Fact]
public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToFresh() public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToFresh()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
var state = httpContext.GetResponseCachingState(); context.CachedEntryAge = TimeSpan.FromSeconds(11);
state.CachedEntryAge = TimeSpan.FromSeconds(11); context.ResponseTime = utcNow + context.CachedEntryAge;
state.ResponseTime = utcNow + state.CachedEntryAge; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
CacheControl = new CacheControlHeaderValue() CacheControl = new CacheControlHeaderValue()
{ {
@ -491,18 +482,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Expires = utcNow Expires = utcNow
}; };
Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context));
} }
[Fact] [Fact]
public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh() public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
var state = httpContext.GetResponseCachingState(); context.CachedEntryAge = TimeSpan.FromSeconds(6);
state.CachedEntryAge = TimeSpan.FromSeconds(6); context.ResponseTime = utcNow + context.CachedEntryAge;
state.ResponseTime = utcNow + state.CachedEntryAge; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
CacheControl = new CacheControlHeaderValue() CacheControl = new CacheControlHeaderValue()
{ {
@ -513,18 +503,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Expires = utcNow Expires = utcNow
}; };
Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context));
} }
[Fact] [Fact]
public void EntryIsFresh_MinFreshReducesFreshness_ToNotFresh() public void EntryIsFresh_MinFreshReducesFreshness_ToNotFresh()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
{ {
MinFresh = TimeSpan.FromSeconds(3) MinFresh = TimeSpan.FromSeconds(3)
}; };
var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
CacheControl = new CacheControlHeaderValue() CacheControl = new CacheControlHeaderValue()
{ {
@ -532,64 +522,64 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
SharedMaxAge = TimeSpan.FromSeconds(5) SharedMaxAge = TimeSpan.FromSeconds(5)
} }
}; };
httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(3); context.CachedEntryAge = TimeSpan.FromSeconds(3);
Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context));
} }
[Fact] [Fact]
public void EntryIsFresh_RequestMaxAgeRestrictAge_ToNotFresh() public void EntryIsFresh_RequestMaxAgeRestrictAge_ToNotFresh()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
{ {
MaxAge = TimeSpan.FromSeconds(5) MaxAge = TimeSpan.FromSeconds(5)
}; };
var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
CacheControl = new CacheControlHeaderValue() CacheControl = new CacheControlHeaderValue()
{ {
MaxAge = TimeSpan.FromSeconds(10), MaxAge = TimeSpan.FromSeconds(10),
} }
}; };
httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(6); context.CachedEntryAge = TimeSpan.FromSeconds(6);
Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context));
} }
[Fact] [Fact]
public void EntryIsFresh_MaxStaleOverridesFreshness_ToFresh() public void EntryIsFresh_MaxStaleOverridesFreshness_ToFresh()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
{ {
MaxAge = TimeSpan.FromSeconds(5), MaxAge = TimeSpan.FromSeconds(5),
MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit
MaxStaleLimit = TimeSpan.FromSeconds(10) MaxStaleLimit = TimeSpan.FromSeconds(10)
}; };
var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
CacheControl = new CacheControlHeaderValue() CacheControl = new CacheControlHeaderValue()
{ {
MaxAge = TimeSpan.FromSeconds(5), MaxAge = TimeSpan.FromSeconds(5),
} }
}; };
httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(6); context.CachedEntryAge = TimeSpan.FromSeconds(6);
Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context));
} }
[Fact] [Fact]
public void EntryIsFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh() public void EntryIsFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
{ {
MaxAge = TimeSpan.FromSeconds(5), MaxAge = TimeSpan.FromSeconds(5),
MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit
MaxStaleLimit = TimeSpan.FromSeconds(10) MaxStaleLimit = TimeSpan.FromSeconds(10)
}; };
var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
CacheControl = new CacheControlHeaderValue() CacheControl = new CacheControlHeaderValue()
{ {
@ -597,21 +587,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
MustRevalidate = true MustRevalidate = true
} }
}; };
httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(6); context.CachedEntryAge = TimeSpan.FromSeconds(6);
Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context));
} }
[Fact] [Fact]
public void EntryIsFresh_IgnoresRequestVerificationWhenSpecified() public void EntryIsFresh_IgnoresRequestVerificationWhenSpecified()
{ {
var httpContext = CreateDefaultContext(); var context = TestUtils.CreateTestContext();
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
{ {
MinFresh = TimeSpan.FromSeconds(1), MinFresh = TimeSpan.FromSeconds(1),
MaxAge = TimeSpan.FromSeconds(3) MaxAge = TimeSpan.FromSeconds(3)
}; };
var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
CacheControl = new CacheControlHeaderValue() CacheControl = new CacheControlHeaderValue()
{ {
@ -619,16 +609,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
SharedMaxAge = TimeSpan.FromSeconds(5) SharedMaxAge = TimeSpan.FromSeconds(5)
} }
}; };
httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(3); context.CachedEntryAge = TimeSpan.FromSeconds(3);
Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context));
}
private static HttpContext CreateDefaultContext()
{
var context = new DefaultHttpContext();
context.AddResponseCachingState();
return context;
} }
} }
} }

View File

@ -21,17 +21,5 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
// Should throw // Should throw
Assert.ThrowsAny<InvalidOperationException>(() => httpContext.AddResponseCachingFeature()); Assert.ThrowsAny<InvalidOperationException>(() => httpContext.AddResponseCachingFeature());
} }
[Fact]
public void AddingSecondResponseCachingState_Throws()
{
var httpContext = new DefaultHttpContext();
// Should not throw
httpContext.AddResponseCachingState();
// Should throw
Assert.ThrowsAny<InvalidOperationException>(() => httpContext.AddResponseCachingState());
}
} }
} }

View File

@ -1,11 +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 System;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.ResponseCaching.Tests namespace Microsoft.AspNetCore.ResponseCaching.Tests
@ -13,156 +12,155 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
public class DefaultKeyProviderTests public class DefaultKeyProviderTests
{ {
private static readonly char KeyDelimiter = '\x1e'; private static readonly char KeyDelimiter = '\x1e';
private static readonly CachedVaryRules TestVaryRules = new CachedVaryRules()
{
VaryKeyPrefix = FastGuid.NewGuid().IdString
};
[Fact] [Fact]
public void DefaultKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodAndPath() public void DefaultKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodAndPath()
{ {
var httpContext = CreateDefaultContext(); var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
httpContext.Request.Method = "head"; var context = TestUtils.CreateTestContext();
httpContext.Request.Path = "/path/subpath"; context.HttpContext.Request.Method = "head";
httpContext.Request.Scheme = "https"; context.HttpContext.Request.Path = "/path/subpath";
httpContext.Request.Host = new HostString("example.com", 80); context.HttpContext.Request.Scheme = "https";
httpContext.Request.PathBase = "/pathBase"; context.HttpContext.Request.Host = new HostString("example.com", 80);
httpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b"); context.HttpContext.Request.PathBase = "/pathBase";
var keyProvider = CreateTestKeyProvider(); context.HttpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b");
Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", keyProvider.CreateStorageBaseKey(httpContext)); Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", cacheKeyProvider.CreateStorageBaseKey(context));
} }
[Fact] [Fact]
public void DefaultKeyProvider_CreateStorageBaseKey_CaseInsensitivePath_NormalizesPath() public void DefaultKeyProvider_CreateStorageBaseKey_CaseInsensitivePath_NormalizesPath()
{ {
var httpContext = CreateDefaultContext(); var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions()
httpContext.Request.Method = "GET";
httpContext.Request.Path = "/Path";
var keyProvider = CreateTestKeyProvider(new ResponseCachingOptions()
{ {
CaseSensitivePaths = false CaseSensitivePaths = false
}); });
var context = TestUtils.CreateTestContext();
context.HttpContext.Request.Method = "GET";
context.HttpContext.Request.Path = "/Path";
Assert.Equal($"GET{KeyDelimiter}/PATH", keyProvider.CreateStorageBaseKey(httpContext)); Assert.Equal($"GET{KeyDelimiter}/PATH", cacheKeyProvider.CreateStorageBaseKey(context));
} }
[Fact] [Fact]
public void DefaultKeyProvider_CreateStorageBaseKey_CaseSensitivePath_PreservesPathCase() public void DefaultKeyProvider_CreateStorageBaseKey_CaseSensitivePath_PreservesPathCase()
{ {
var httpContext = CreateDefaultContext(); var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions()
httpContext.Request.Method = "GET";
httpContext.Request.Path = "/Path";
var keyProvider = CreateTestKeyProvider(new ResponseCachingOptions()
{ {
CaseSensitivePaths = true CaseSensitivePaths = true
}); });
var context = TestUtils.CreateTestContext();
context.HttpContext.Request.Method = "GET";
context.HttpContext.Request.Path = "/Path";
Assert.Equal($"GET{KeyDelimiter}/Path", keyProvider.CreateStorageBaseKey(httpContext)); Assert.Equal($"GET{KeyDelimiter}/Path", cacheKeyProvider.CreateStorageBaseKey(context));
} }
[Fact] [Fact]
public void DefaultKeyProvider_CreateStorageVaryKey_ReturnsCachedVaryGuid_IfVaryRulesIsNullOrEmpty() public void DefaultKeyProvider_CreateStorageVaryKey_Throws_IfVaryRulesIsNull()
{ {
var httpContext = CreateDefaultContext(); var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
var keyProvider = CreateTestKeyProvider(); var context = TestUtils.CreateTestContext();
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateStorageVaryKey(httpContext, null)); Assert.Throws<InvalidOperationException>(() => cacheKeyProvider.CreateStorageVaryKey(context));
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateStorageVaryKey(httpContext, new VaryRules())); }
[Fact]
public void DefaultKeyProvider_CreateStorageVaryKey_ReturnsCachedVaryGuid_IfVaryRulesIsEmpty()
{
var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
var context = TestUtils.CreateTestContext();
context.CachedVaryRules = new CachedVaryRules()
{
VaryKeyPrefix = FastGuid.NewGuid().IdString
};
Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}", cacheKeyProvider.CreateStorageVaryKey(context));
} }
[Fact] [Fact]
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly() public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly()
{ {
var httpContext = CreateDefaultContext(); var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
httpContext.Request.Headers["HeaderA"] = "ValueA"; var context = TestUtils.CreateTestContext();
httpContext.Request.Headers["HeaderB"] = "ValueB"; context.HttpContext.Request.Headers["HeaderA"] = "ValueA";
var keyProvider = CreateTestKeyProvider(); context.HttpContext.Request.Headers["HeaderB"] = "ValueB";
context.CachedVaryRules = new CachedVaryRules()
{
Headers = new string[] { "HeaderA", "HeaderC" }
};
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null", Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=",
keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() cacheKeyProvider.CreateStorageVaryKey(context));
{
Headers = new string[] { "HeaderA", "HeaderC" }
}));
} }
[Fact] [Fact]
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedParamsOnly() public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedParamsOnly()
{ {
var httpContext = CreateDefaultContext(); var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); var context = TestUtils.CreateTestContext();
var keyProvider = CreateTestKeyProvider(); context.HttpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
context.CachedVaryRules = new CachedVaryRules()
{
VaryKeyPrefix = FastGuid.NewGuid().IdString,
Params = new string[] { "ParamA", "ParamC" }
};
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=",
keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() cacheKeyProvider.CreateStorageVaryKey(context));
{
Params = new string[] { "ParamA", "ParamC" }
}));
} }
[Fact] [Fact]
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesParams_ParamNameCaseInsensitive_UseParamCasing() public void DefaultKeyProvider_CreateStorageVaryKey_IncludesParams_ParamNameCaseInsensitive_UseParamCasing()
{ {
var httpContext = CreateDefaultContext(); var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
httpContext.Request.QueryString = new QueryString("?parama=ValueA&paramB=ValueB"); var context = TestUtils.CreateTestContext();
var keyProvider = CreateTestKeyProvider(); context.HttpContext.Request.QueryString = new QueryString("?parama=ValueA&paramB=ValueB");
context.CachedVaryRules = new CachedVaryRules()
{
VaryKeyPrefix = FastGuid.NewGuid().IdString,
Params = new string[] { "ParamA", "ParamC" }
};
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=",
keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() cacheKeyProvider.CreateStorageVaryKey(context));
{
Params = new string[] { "ParamA", "ParamC" }
}));
} }
[Fact] [Fact]
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesAllQueryParamsGivenAsterisk() public void DefaultKeyProvider_CreateStorageVaryKey_IncludesAllQueryParamsGivenAsterisk()
{ {
var httpContext = CreateDefaultContext(); var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); var context = TestUtils.CreateTestContext();
var keyProvider = CreateTestKeyProvider(); context.HttpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
context.CachedVaryRules = new CachedVaryRules()
{
VaryKeyPrefix = FastGuid.NewGuid().IdString,
Params = new string[] { "*" }
};
// To support case insensitivity, all param keys are converted to upper case. // To support case insensitivity, all param keys are converted to upper case.
// Explicit params uses the casing specified in the setting. // Explicit params uses the casing specified in the setting.
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB", Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB",
keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() cacheKeyProvider.CreateStorageVaryKey(context));
{
Params = new string[] { "*" }
}));
} }
[Fact] [Fact]
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndParams() public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndParams()
{ {
var httpContext = CreateDefaultContext(); var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
httpContext.Request.Headers["HeaderA"] = "ValueA"; var context = TestUtils.CreateTestContext();
httpContext.Request.Headers["HeaderB"] = "ValueB"; context.HttpContext.Request.Headers["HeaderA"] = "ValueA";
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); context.HttpContext.Request.Headers["HeaderB"] = "ValueB";
var keyProvider = CreateTestKeyProvider(); context.HttpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
context.CachedVaryRules = new CachedVaryRules()
{
VaryKeyPrefix = FastGuid.NewGuid().IdString,
Headers = new string[] { "HeaderA", "HeaderC" },
Params = new string[] { "ParamA", "ParamC" }
};
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=",
keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() cacheKeyProvider.CreateStorageVaryKey(context));
{
Headers = new string[] { "HeaderA", "HeaderC" },
Params = new string[] { "ParamA", "ParamC" }
}));
}
private static HttpContext CreateDefaultContext()
{
var context = new DefaultHttpContext();
context.AddResponseCachingState();
context.GetResponseCachingState().CachedVaryRules = TestVaryRules;
return context;
}
private static IKeyProvider CreateTestKeyProvider()
{
return CreateTestKeyProvider(new ResponseCachingOptions());
}
private static IKeyProvider CreateTestKeyProvider(ResponseCachingOptions options)
{
return new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options));
} }
} }
} }

View File

@ -1,742 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Xunit;
namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
public class ResponseCachingContextTests
{
[Fact]
public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504()
{
var cache = new TestResponseCache();
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider());
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
OnlyIfCached = true
};
Assert.True(await context.TryServeFromCacheAsync());
Assert.Equal(StatusCodes.Status504GatewayTimeout, httpContext.Response.StatusCode);
}
[Fact]
public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails()
{
var cache = new TestResponseCache();
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }));
Assert.False(await context.TryServeFromCacheAsync());
Assert.Equal(2, cache.GetCount);
}
[Fact]
public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds()
{
var cache = new TestResponseCache();
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }));
cache.Set(
"BaseKey2",
new CachedResponse()
{
Body = new byte[0]
},
TimeSpan.Zero);
Assert.True(await context.TryServeFromCacheAsync());
Assert.Equal(2, cache.GetCount);
}
[Fact]
public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseNotFound_Fails()
{
var cache = new TestResponseCache();
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }));
cache.Set(
"BaseKey2",
new CachedVaryRules(),
TimeSpan.Zero);
Assert.False(await context.TryServeFromCacheAsync());
Assert.Equal(2, cache.GetCount);
}
[Fact]
public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseFound_Succeeds()
{
var cache = new TestResponseCache();
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }, new[] { "VaryKey", "VaryKey2" }));
cache.Set(
"BaseKey2",
new CachedVaryRules(),
TimeSpan.Zero);
cache.Set(
"BaseKey2VaryKey2",
new CachedResponse()
{
Body = new byte[0]
},
TimeSpan.Zero);
Assert.True(await context.TryServeFromCacheAsync());
Assert.Equal(6, cache.GetCount);
}
[Fact]
public void ConditionalRequestSatisfied_NotConditionalRequest_Fails()
{
var context = CreateTestContext(new DefaultHttpContext());
var cachedHeaders = new ResponseHeaders(new HeaderDictionary());
Assert.False(context.ConditionalRequestSatisfied(cachedHeaders));
}
[Fact]
public void ConditionalRequestSatisfied_IfUnmodifiedSince_FallsbackToDateHeader()
{
var utcNow = DateTimeOffset.UtcNow;
var cachedHeaders = new ResponseHeaders(new HeaderDictionary());
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext);
httpContext.Request.GetTypedHeaders().IfUnmodifiedSince = utcNow;
// Verify modifications in the past succeeds
cachedHeaders.Date = utcNow - TimeSpan.FromSeconds(10);
Assert.True(context.ConditionalRequestSatisfied(cachedHeaders));
// Verify modifications at present succeeds
cachedHeaders.Date = utcNow;
Assert.True(context.ConditionalRequestSatisfied(cachedHeaders));
// Verify modifications in the future fails
cachedHeaders.Date = utcNow + TimeSpan.FromSeconds(10);
Assert.False(context.ConditionalRequestSatisfied(cachedHeaders));
}
[Fact]
public void ConditionalRequestSatisfied_IfUnmodifiedSince_LastModifiedOverridesDateHeader()
{
var utcNow = DateTimeOffset.UtcNow;
var cachedHeaders = new ResponseHeaders(new HeaderDictionary());
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext);
httpContext.Request.GetTypedHeaders().IfUnmodifiedSince = utcNow;
// Verify modifications in the past succeeds
cachedHeaders.Date = utcNow + TimeSpan.FromSeconds(10);
cachedHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10);
Assert.True(context.ConditionalRequestSatisfied(cachedHeaders));
// Verify modifications at present
cachedHeaders.Date = utcNow + TimeSpan.FromSeconds(10);
cachedHeaders.LastModified = utcNow;
Assert.True(context.ConditionalRequestSatisfied(cachedHeaders));
// Verify modifications in the future fails
cachedHeaders.Date = utcNow - TimeSpan.FromSeconds(10);
cachedHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10);
Assert.False(context.ConditionalRequestSatisfied(cachedHeaders));
}
[Fact]
public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToPass()
{
var utcNow = DateTimeOffset.UtcNow;
var cachedHeaders = new ResponseHeaders(new HeaderDictionary());
var httpContext = new DefaultHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
var context = CreateTestContext(httpContext);
// This would fail the IfUnmodifiedSince checks
requestHeaders.IfUnmodifiedSince = utcNow;
cachedHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10);
requestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { EntityTagHeaderValue.Any });
Assert.True(context.ConditionalRequestSatisfied(cachedHeaders));
}
[Fact]
public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToFail()
{
var utcNow = DateTimeOffset.UtcNow;
var cachedHeaders = new ResponseHeaders(new HeaderDictionary());
var httpContext = new DefaultHttpContext();
var requestHeaders = httpContext.Request.GetTypedHeaders();
var context = CreateTestContext(httpContext);
// This would pass the IfUnmodifiedSince checks
requestHeaders.IfUnmodifiedSince = utcNow;
cachedHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10);
requestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
Assert.False(context.ConditionalRequestSatisfied(cachedHeaders));
}
[Fact]
public void ConditionalRequestSatisfied_IfNoneMatch_AnyWithoutETagInResponse_Passes()
{
var cachedHeaders = new ResponseHeaders(new HeaderDictionary());
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext);
httpContext.Request.GetTypedHeaders().IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
Assert.False(context.ConditionalRequestSatisfied(cachedHeaders));
}
[Fact]
public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithMatch_Passes()
{
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
{
ETag = new EntityTagHeaderValue("\"E1\"")
};
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext);
httpContext.Request.GetTypedHeaders().IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
Assert.True(context.ConditionalRequestSatisfied(cachedHeaders));
}
[Fact]
public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithoutMatch_Fails()
{
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
{
ETag = new EntityTagHeaderValue("\"E2\"")
};
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext);
httpContext.Request.GetTypedHeaders().IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
Assert.False(context.ConditionalRequestSatisfied(cachedHeaders));
}
[Fact]
public void FinalizeCachingHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable()
{
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext, cacheabilityValidator: new CacheabilityValidator());
var state = httpContext.GetResponseCachingState();
Assert.False(state.ShouldCacheResponse);
context.ShimResponseStream();
context.FinalizeCachingHeaders();
Assert.False(state.ShouldCacheResponse);
}
[Fact]
public void FinalizeCachingHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable()
{
var httpContext = new DefaultHttpContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true
};
var context = CreateTestContext(httpContext, cacheabilityValidator: new CacheabilityValidator());
var state = httpContext.GetResponseCachingState();
Assert.False(state.ShouldCacheResponse);
context.FinalizeCachingHeaders();
Assert.True(state.ShouldCacheResponse);
}
[Fact]
public void FinalizeCachingHeaders_DefaultResponseValidity_Is10Seconds()
{
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext);
context.FinalizeCachingHeaders();
Assert.Equal(TimeSpan.FromSeconds(10), httpContext.GetResponseCachingState().CachedResponseValidFor);
}
[Fact]
public void FinalizeCachingHeaders_ResponseValidity_UseExpiryIfAvailable()
{
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext);
var state = httpContext.GetResponseCachingState();
var utcNow = DateTimeOffset.MinValue;
state.ResponseTime = utcNow;
state.ResponseHeaders.Expires = utcNow + TimeSpan.FromSeconds(11);
context.FinalizeCachingHeaders();
Assert.Equal(TimeSpan.FromSeconds(11), state.CachedResponseValidFor);
}
[Fact]
public void FinalizeCachingHeaders_ResponseValidity_UseMaxAgeIfAvailable()
{
var httpContext = new DefaultHttpContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(12)
};
var context = CreateTestContext(httpContext);
var state = httpContext.GetResponseCachingState();
state.ResponseTime = DateTimeOffset.UtcNow;
state.ResponseHeaders.Expires = state.ResponseTime + TimeSpan.FromSeconds(11);
context.FinalizeCachingHeaders();
Assert.Equal(TimeSpan.FromSeconds(12), state.CachedResponseValidFor);
}
[Fact]
public void FinalizeCachingHeaders_ResponseValidity_UseSharedMaxAgeIfAvailable()
{
var httpContext = new DefaultHttpContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(12),
SharedMaxAge = TimeSpan.FromSeconds(13)
};
var context = CreateTestContext(httpContext);
var state = httpContext.GetResponseCachingState();
state.ResponseTime = DateTimeOffset.UtcNow;
state.ResponseHeaders.Expires = state.ResponseTime + TimeSpan.FromSeconds(11);
context.FinalizeCachingHeaders();
Assert.Equal(TimeSpan.FromSeconds(13), state.CachedResponseValidFor);
}
[Fact]
public void FinalizeCachingHeaders_UpdateCachedVaryRules_IfNotEquivalentToPrevious()
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
var context = CreateTestContext(httpContext, cache);
var state = httpContext.GetResponseCachingState();
httpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" });
httpContext.AddResponseCachingFeature();
httpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMAA" });
var cachedVaryRules = new CachedVaryRules()
{
VaryRules = new VaryRules()
{
Headers = new StringValues(new[] { "HeaderA", "HeaderB" }),
Params = new StringValues(new[] { "ParamA", "ParamB" })
}
};
state.CachedVaryRules = cachedVaryRules;
context.FinalizeCachingHeaders();
Assert.Equal(1, cache.SetCount);
Assert.NotSame(cachedVaryRules, state.CachedVaryRules);
}
[Fact]
public void FinalizeCachingHeaders_DoNotUpdateCachedVaryRules_IfEquivalentToPrevious()
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
var context = CreateTestContext(httpContext, cache);
var state = httpContext.GetResponseCachingState();
httpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB" });
httpContext.AddResponseCachingFeature();
httpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMA" });
var cachedVaryRules = new CachedVaryRules()
{
VaryKeyPrefix = FastGuid.NewGuid().IdString,
VaryRules = new VaryRules()
{
Headers = new StringValues(new[] { "HEADERA", "HEADERB" }),
Params = new StringValues(new[] { "PARAMA", "PARAMB" })
}
};
state.CachedVaryRules = cachedVaryRules;
context.FinalizeCachingHeaders();
Assert.Equal(0, cache.SetCount);
Assert.Same(cachedVaryRules, state.CachedVaryRules);
}
[Fact]
public void FinalizeCachingHeaders_DoNotAddDate_IfSpecified()
{
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext);
var state = httpContext.GetResponseCachingState();
var utcNow = DateTimeOffset.MinValue;
state.ResponseTime = utcNow;
Assert.Null(state.ResponseHeaders.Date);
context.FinalizeCachingHeaders();
Assert.Equal(utcNow, state.ResponseHeaders.Date);
}
[Fact]
public void FinalizeCachingHeaders_AddsDate_IfNoneSpecified()
{
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext);
var state = httpContext.GetResponseCachingState();
var utcNow = DateTimeOffset.MinValue;
state.ResponseHeaders.Date = utcNow;
state.ResponseTime = utcNow + TimeSpan.FromSeconds(10);
Assert.Equal(utcNow, state.ResponseHeaders.Date);
context.FinalizeCachingHeaders();
Assert.Equal(utcNow, state.ResponseHeaders.Date);
}
[Fact]
public void FinalizeCachingHeaders_StoresCachedResponse_InState()
{
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext);
var state = httpContext.GetResponseCachingState();
Assert.Null(state.CachedResponse);
context.FinalizeCachingHeaders();
Assert.NotNull(state.CachedResponse);
}
[Fact]
public async Task FinalizeCachingBody_StoreResponseBodySeparately_IfLargerThanLimit()
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
var context = CreateTestContext(httpContext, cache);
context.ShimResponseStream();
await httpContext.Response.WriteAsync(new string('0', 70 * 1024));
var state = httpContext.GetResponseCachingState();
state.ShouldCacheResponse = true;
state.CachedResponse = new CachedResponse()
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
state.StorageBaseKey = "BaseKey";
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
context.FinalizeCachingBody();
Assert.Equal(2, cache.SetCount);
}
[Fact]
public async Task FinalizeCachingBody_StoreResponseBodyInCachedResponse_IfSmallerThanLimit()
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
var context = CreateTestContext(httpContext, cache);
context.ShimResponseStream();
await httpContext.Response.WriteAsync(new string('0', 70 * 1024 - 1));
var state = httpContext.GetResponseCachingState();
state.ShouldCacheResponse = true;
state.CachedResponse = new CachedResponse()
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
state.StorageBaseKey = "BaseKey";
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
context.FinalizeCachingBody();
Assert.Equal(1, cache.SetCount);
}
[Fact]
public async Task FinalizeCachingBody_StoreResponseBodySeparately_LimitIsConfigurable()
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions()
{
MinimumSplitBodySize = 2048
});
context.ShimResponseStream();
await httpContext.Response.WriteAsync(new string('0', 1024));
var state = httpContext.GetResponseCachingState();
state.ShouldCacheResponse = true;
state.CachedResponse = new CachedResponse()
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
state.StorageBaseKey = "BaseKey";
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
context.FinalizeCachingBody();
Assert.Equal(1, cache.SetCount);
}
[Fact]
public async Task FinalizeCachingBody_Cache_IfContentLengthMatches()
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
var context = CreateTestContext(httpContext, cache);
context.ShimResponseStream();
httpContext.Response.ContentLength = 10;
await httpContext.Response.WriteAsync(new string('0', 10));
var state = httpContext.GetResponseCachingState();
state.ShouldCacheResponse = true;
state.CachedResponse = new CachedResponse()
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
state.StorageBaseKey = "BaseKey";
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
context.FinalizeCachingBody();
Assert.Equal(1, cache.SetCount);
}
[Fact]
public async Task FinalizeCachingBody_DoNotCache_IfContentLengthMismatches()
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
var context = CreateTestContext(httpContext, cache);
context.ShimResponseStream();
httpContext.Response.ContentLength = 9;
await httpContext.Response.WriteAsync(new string('0', 10));
var state = httpContext.GetResponseCachingState();
state.ShouldCacheResponse = true;
state.CachedResponse = new CachedResponse()
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
state.StorageBaseKey = "BaseKey";
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
context.FinalizeCachingBody();
Assert.Equal(0, cache.SetCount);
}
[Fact]
public async Task FinalizeCachingBody_Cache_IfContentLengthAbsent()
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
var context = CreateTestContext(httpContext, cache);
context.ShimResponseStream();
await httpContext.Response.WriteAsync(new string('0', 10));
var state = httpContext.GetResponseCachingState();
state.ShouldCacheResponse = true;
state.CachedResponse = new CachedResponse()
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
state.StorageBaseKey = "BaseKey";
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
context.FinalizeCachingBody();
Assert.Equal(1, cache.SetCount);
}
[Fact]
public void NormalizeStringValues_NormalizesCasingToUpper()
{
var uppercaseStrings = new StringValues(new[] { "STRINGA", "STRINGB" });
var lowercaseStrings = new StringValues(new[] { "stringA", "stringB" });
var normalizedStrings = ResponseCachingContext.GetNormalizedStringValues(lowercaseStrings);
Assert.Equal(uppercaseStrings, normalizedStrings);
}
[Fact]
public void NormalizeStringValues_NormalizesOrder()
{
var orderedStrings = new StringValues(new[] { "STRINGA", "STRINGB" });
var reverseOrderStrings = new StringValues(new[] { "STRINGB", "STRINGA" });
var normalizedStrings = ResponseCachingContext.GetNormalizedStringValues(reverseOrderStrings);
Assert.Equal(orderedStrings, normalizedStrings);
}
private static ResponseCachingContext CreateTestContext(
HttpContext httpContext,
IResponseCache responseCache = null,
ResponseCachingOptions options = null,
IKeyProvider keyProvider = null,
ICacheabilityValidator cacheabilityValidator = null)
{
if (responseCache == null)
{
responseCache = new TestResponseCache();
}
if (options == null)
{
options = new ResponseCachingOptions();
}
if (keyProvider == null)
{
keyProvider = new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options));
}
if (cacheabilityValidator == null)
{
cacheabilityValidator = new TestCacheabilityValidator();
}
httpContext.AddResponseCachingState();
return new ResponseCachingContext(
httpContext,
responseCache,
options,
cacheabilityValidator,
keyProvider);
}
private class TestCacheabilityValidator : ICacheabilityValidator
{
public bool CachedEntryIsFresh(HttpContext httpContext, ResponseHeaders cachedResponseHeaders) => true;
public bool RequestIsCacheable(HttpContext httpContext) => true;
public bool ResponseIsCacheable(HttpContext httpContext) => true;
}
private class TestKeyProvider : IKeyProvider
{
private readonly StringValues _baseKey;
private readonly StringValues _varyKey;
public TestKeyProvider(StringValues? lookupBaseKey = null, StringValues? lookupVaryKey = null)
{
if (lookupBaseKey.HasValue)
{
_baseKey = lookupBaseKey.Value;
}
if (lookupVaryKey.HasValue)
{
_varyKey = lookupVaryKey.Value;
}
}
public IEnumerable<string> CreateLookupBaseKey(HttpContext httpContext) => _baseKey;
public IEnumerable<string> CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules)
{
foreach (var baseKey in _baseKey)
{
foreach (var varyKey in _varyKey)
{
yield return baseKey + varyKey;
}
}
}
public string CreateBodyKey(HttpContext httpContext)
{
throw new NotImplementedException();
}
public string CreateStorageBaseKey(HttpContext httpContext)
{
throw new NotImplementedException();
}
public string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules)
{
throw new NotImplementedException();
}
}
private class TestResponseCache : IResponseCache
{
private readonly IDictionary<string, object> _storage = new Dictionary<string, object>();
public int GetCount { get; private set; }
public int SetCount { get; private set; }
public object Get(string key)
{
GetCount++;
try
{
return _storage[key];
}
catch
{
return null;
}
}
public void Remove(string key)
{
}
public void Set(string key, object entry, TimeSpan validFor)
{
SetCount++;
_storage[key] = entry;
}
}
private class TestHttpSendFileFeature : IHttpSendFileFeature
{
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
{
return TaskCache.CompletedTask;
}
}
}
}

View File

@ -0,0 +1,578 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Xunit;
namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
public class ResponseCachingMiddlewareTests
{
[Fact]
public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504()
{
var cache = new TestResponseCache();
var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider());
var context = TestUtils.CreateTestContext();
context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
{
OnlyIfCached = true
};
Assert.True(await middleware.TryServeFromCacheAsync(context));
Assert.Equal(StatusCodes.Status504GatewayTimeout, context.HttpContext.Response.StatusCode);
}
[Fact]
public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails()
{
var cache = new TestResponseCache();
var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }));
var context = TestUtils.CreateTestContext();
Assert.False(await middleware.TryServeFromCacheAsync(context));
Assert.Equal(2, cache.GetCount);
}
[Fact]
public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds()
{
var cache = new TestResponseCache();
var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }));
var context = TestUtils.CreateTestContext();
cache.Set(
"BaseKey2",
new CachedResponse()
{
Body = new byte[0]
},
TimeSpan.Zero);
Assert.True(await middleware.TryServeFromCacheAsync(context));
Assert.Equal(2, cache.GetCount);
}
[Fact]
public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseNotFound_Fails()
{
var cache = new TestResponseCache();
var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }));
var context = TestUtils.CreateTestContext();
cache.Set(
"BaseKey2",
new CachedVaryRules(),
TimeSpan.Zero);
Assert.False(await middleware.TryServeFromCacheAsync(context));
Assert.Equal(2, cache.GetCount);
}
[Fact]
public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseFound_Succeeds()
{
var cache = new TestResponseCache();
var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }, new[] { "VaryKey", "VaryKey2" }));
var context = TestUtils.CreateTestContext();
cache.Set(
"BaseKey2",
new CachedVaryRules(),
TimeSpan.Zero);
cache.Set(
"BaseKey2VaryKey2",
new CachedResponse()
{
Body = new byte[0]
},
TimeSpan.Zero);
Assert.True(await middleware.TryServeFromCacheAsync(context));
Assert.Equal(6, cache.GetCount);
}
[Fact]
public void ConditionalRequestSatisfied_NotConditionalRequest_Fails()
{
var context = TestUtils.CreateTestContext();
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
}
[Fact]
public void ConditionalRequestSatisfied_IfUnmodifiedSince_FallsbackToDateHeader()
{
var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext();
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
context.TypedRequestHeaders.IfUnmodifiedSince = utcNow;
// Verify modifications in the past succeeds
context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10);
Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
// Verify modifications at present succeeds
context.CachedResponseHeaders.Date = utcNow;
Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
// Verify modifications in the future fails
context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10);
Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
}
[Fact]
public void ConditionalRequestSatisfied_IfUnmodifiedSince_LastModifiedOverridesDateHeader()
{
var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext();
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
context.TypedRequestHeaders.IfUnmodifiedSince = utcNow;
// Verify modifications in the past succeeds
context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10);
context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10);
Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
// Verify modifications at present
context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10);
context.CachedResponseHeaders.LastModified = utcNow;
Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
// Verify modifications in the future fails
context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10);
context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10);
Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
}
[Fact]
public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToPass()
{
var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext();
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
// This would fail the IfUnmodifiedSince checks
context.TypedRequestHeaders.IfUnmodifiedSince = utcNow;
context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10);
context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { EntityTagHeaderValue.Any });
Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
}
[Fact]
public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToFail()
{
var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext();
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
// This would pass the IfUnmodifiedSince checks
context.TypedRequestHeaders.IfUnmodifiedSince = utcNow;
context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10);
context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
}
[Fact]
public void ConditionalRequestSatisfied_IfNoneMatch_AnyWithoutETagInResponse_Passes()
{
var context = TestUtils.CreateTestContext();
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
}
[Fact]
public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithMatch_Passes()
{
var context = TestUtils.CreateTestContext();
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
{
ETag = new EntityTagHeaderValue("\"E1\"")
};
context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
}
[Fact]
public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithoutMatch_Fails()
{
var context = TestUtils.CreateTestContext();
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
{
ETag = new EntityTagHeaderValue("\"E2\"")
};
context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
}
[Fact]
public void FinalizeCachingHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable()
{
var middleware = TestUtils.CreateTestMiddleware(cacheabilityValidator: new CacheabilityValidator());
var context = TestUtils.CreateTestContext();
Assert.False(context.ShouldCacheResponse);
middleware.ShimResponseStream(context);
middleware.FinalizeCachingHeaders(context);
Assert.False(context.ShouldCacheResponse);
}
[Fact]
public void FinalizeCachingHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable()
{
var middleware = TestUtils.CreateTestMiddleware(cacheabilityValidator: new CacheabilityValidator());
var context = TestUtils.CreateTestContext();
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{
Public = true
};
Assert.False(context.ShouldCacheResponse);
middleware.FinalizeCachingHeaders(context);
Assert.True(context.ShouldCacheResponse);
}
[Fact]
public void FinalizeCachingHeaders_DefaultResponseValidity_Is10Seconds()
{
var middleware = TestUtils.CreateTestMiddleware();
var context = TestUtils.CreateTestContext();
middleware.FinalizeCachingHeaders(context);
Assert.Equal(TimeSpan.FromSeconds(10), context.CachedResponseValidFor);
}
[Fact]
public void FinalizeCachingHeaders_ResponseValidity_UseExpiryIfAvailable()
{
var utcNow = DateTimeOffset.MinValue;
var middleware = TestUtils.CreateTestMiddleware();
var context = TestUtils.CreateTestContext();
context.ResponseTime = utcNow;
context.TypedResponseHeaders.Expires = utcNow + TimeSpan.FromSeconds(11);
middleware.FinalizeCachingHeaders(context);
Assert.Equal(TimeSpan.FromSeconds(11), context.CachedResponseValidFor);
}
[Fact]
public void FinalizeCachingHeaders_ResponseValidity_UseMaxAgeIfAvailable()
{
var middleware = TestUtils.CreateTestMiddleware();
var context = TestUtils.CreateTestContext();
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(12)
};
context.ResponseTime = DateTimeOffset.UtcNow;
context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11);
middleware.FinalizeCachingHeaders(context);
Assert.Equal(TimeSpan.FromSeconds(12), context.CachedResponseValidFor);
}
[Fact]
public void FinalizeCachingHeaders_ResponseValidity_UseSharedMaxAgeIfAvailable()
{
var middleware = TestUtils.CreateTestMiddleware();
var context = TestUtils.CreateTestContext();
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(12),
SharedMaxAge = TimeSpan.FromSeconds(13)
};
context.ResponseTime = DateTimeOffset.UtcNow;
context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11);
middleware.FinalizeCachingHeaders(context);
Assert.Equal(TimeSpan.FromSeconds(13), context.CachedResponseValidFor);
}
[Fact]
public void FinalizeCachingHeaders_UpdateCachedVaryRules_IfNotEquivalentToPrevious()
{
var cache = new TestResponseCache();
var middleware = TestUtils.CreateTestMiddleware(cache);
var context = TestUtils.CreateTestContext();
context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" });
context.HttpContext.AddResponseCachingFeature();
context.HttpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMAA" });
var cachedVaryRules = new CachedVaryRules()
{
Headers = new StringValues(new[] { "HeaderA", "HeaderB" }),
Params = new StringValues(new[] { "ParamA", "ParamB" })
};
context.CachedVaryRules = cachedVaryRules;
middleware.FinalizeCachingHeaders(context);
Assert.Equal(1, cache.SetCount);
Assert.NotSame(cachedVaryRules, context.CachedVaryRules);
}
[Fact]
public void FinalizeCachingHeaders_DoNotUpdateCachedVaryRules_IfEquivalentToPrevious()
{
var cache = new TestResponseCache();
var middleware = TestUtils.CreateTestMiddleware(cache);
var context = TestUtils.CreateTestContext();
context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB" });
context.HttpContext.AddResponseCachingFeature();
context.HttpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMA" });
var cachedVaryRules = new CachedVaryRules()
{
VaryKeyPrefix = FastGuid.NewGuid().IdString,
Headers = new StringValues(new[] { "HEADERA", "HEADERB" }),
Params = new StringValues(new[] { "PARAMA", "PARAMB" })
};
context.CachedVaryRules = cachedVaryRules;
middleware.FinalizeCachingHeaders(context);
Assert.Equal(0, cache.SetCount);
Assert.Same(cachedVaryRules, context.CachedVaryRules);
}
[Fact]
public void FinalizeCachingHeaders_DoNotAddDate_IfSpecified()
{
var utcNow = DateTimeOffset.MinValue;
var middleware = TestUtils.CreateTestMiddleware();
var context = TestUtils.CreateTestContext();
context.ResponseTime = utcNow;
Assert.Null(context.TypedResponseHeaders.Date);
middleware.FinalizeCachingHeaders(context);
Assert.Equal(utcNow, context.TypedResponseHeaders.Date);
}
[Fact]
public void FinalizeCachingHeaders_AddsDate_IfNoneSpecified()
{
var utcNow = DateTimeOffset.MinValue;
var middleware = TestUtils.CreateTestMiddleware();
var context = TestUtils.CreateTestContext();
context.TypedResponseHeaders.Date = utcNow;
context.ResponseTime = utcNow + TimeSpan.FromSeconds(10);
Assert.Equal(utcNow, context.TypedResponseHeaders.Date);
middleware.FinalizeCachingHeaders(context);
Assert.Equal(utcNow, context.TypedResponseHeaders.Date);
}
[Fact]
public void FinalizeCachingHeaders_StoresCachedResponse_InState()
{
var middleware = TestUtils.CreateTestMiddleware();
var context = TestUtils.CreateTestContext();
Assert.Null(context.CachedResponse);
middleware.FinalizeCachingHeaders(context);
Assert.NotNull(context.CachedResponse);
}
[Fact]
public async Task FinalizeCachingBody_StoreResponseBodySeparately_IfLargerThanLimit()
{
var cache = new TestResponseCache();
var middleware = TestUtils.CreateTestMiddleware(cache);
var context = TestUtils.CreateTestContext();
middleware.ShimResponseStream(context);
await context.HttpContext.Response.WriteAsync(new string('0', 70 * 1024));
context.ShouldCacheResponse = true;
context.CachedResponse = new CachedResponse()
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
context.StorageBaseKey = "BaseKey";
context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
middleware.FinalizeCachingBody(context);
Assert.Equal(2, cache.SetCount);
}
[Fact]
public async Task FinalizeCachingBody_StoreResponseBodyInCachedResponse_IfSmallerThanLimit()
{
var cache = new TestResponseCache();
var middleware = TestUtils.CreateTestMiddleware(cache);
var context = TestUtils.CreateTestContext();
middleware.ShimResponseStream(context);
await context.HttpContext.Response.WriteAsync(new string('0', 70 * 1024 - 1));
context.ShouldCacheResponse = true;
context.CachedResponse = new CachedResponse()
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
context.StorageBaseKey = "BaseKey";
context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
middleware.FinalizeCachingBody(context);
Assert.Equal(1, cache.SetCount);
}
[Fact]
public async Task FinalizeCachingBody_StoreResponseBodySeparately_LimitIsConfigurable()
{
var cache = new TestResponseCache();
var middleware = TestUtils.CreateTestMiddleware(cache, new ResponseCachingOptions()
{
MinimumSplitBodySize = 2048
});
var context = TestUtils.CreateTestContext();
middleware.ShimResponseStream(context);
await context.HttpContext.Response.WriteAsync(new string('0', 1024));
context.ShouldCacheResponse = true;
context.CachedResponse = new CachedResponse()
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
context.StorageBaseKey = "BaseKey";
context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
middleware.FinalizeCachingBody(context);
Assert.Equal(1, cache.SetCount);
}
[Fact]
public async Task FinalizeCachingBody_Cache_IfContentLengthMatches()
{
var cache = new TestResponseCache();
var middleware = TestUtils.CreateTestMiddleware(cache);
var context = TestUtils.CreateTestContext();
middleware.ShimResponseStream(context);
context.HttpContext.Response.ContentLength = 10;
await context.HttpContext.Response.WriteAsync(new string('0', 10));
context.ShouldCacheResponse = true;
context.CachedResponse = new CachedResponse()
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
context.StorageBaseKey = "BaseKey";
context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
middleware.FinalizeCachingBody(context);
Assert.Equal(1, cache.SetCount);
}
[Fact]
public async Task FinalizeCachingBody_DoNotCache_IfContentLengthMismatches()
{
var cache = new TestResponseCache();
var middleware = TestUtils.CreateTestMiddleware(cache);
var context = TestUtils.CreateTestContext();
middleware.ShimResponseStream(context);
context.HttpContext.Response.ContentLength = 9;
await context.HttpContext.Response.WriteAsync(new string('0', 10));
context.ShouldCacheResponse = true;
context.CachedResponse = new CachedResponse()
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
context.StorageBaseKey = "BaseKey";
context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
middleware.FinalizeCachingBody(context);
Assert.Equal(0, cache.SetCount);
}
[Fact]
public async Task FinalizeCachingBody_Cache_IfContentLengthAbsent()
{
var cache = new TestResponseCache();
var middleware = TestUtils.CreateTestMiddleware(cache);
var context = TestUtils.CreateTestContext();
middleware.ShimResponseStream(context);
await context.HttpContext.Response.WriteAsync(new string('0', 10));
context.ShouldCacheResponse = true;
context.CachedResponse = new CachedResponse()
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
context.StorageBaseKey = "BaseKey";
context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
middleware.FinalizeCachingBody(context);
Assert.Equal(1, cache.SetCount);
}
[Fact]
public void NormalizeStringValues_NormalizesCasingToUpper()
{
var uppercaseStrings = new StringValues(new[] { "STRINGA", "STRINGB" });
var lowercaseStrings = new StringValues(new[] { "stringA", "stringB" });
var normalizedStrings = ResponseCachingMiddleware.GetNormalizedStringValues(lowercaseStrings);
Assert.Equal(uppercaseStrings, normalizedStrings);
}
[Fact]
public void NormalizeStringValues_NormalizesOrder()
{
var orderedStrings = new StringValues(new[] { "STRINGA", "STRINGB" });
var reverseOrderStrings = new StringValues(new[] { "STRINGB", "STRINGA" });
var normalizedStrings = ResponseCachingMiddleware.GetNormalizedStringValues(reverseOrderStrings);
Assert.Equal(orderedStrings, normalizedStrings);
}
}
}

View File

@ -6,11 +6,9 @@ using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Xunit; using Xunit;
@ -21,7 +19,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesCachedContent_IfAvailable() public async void ServesCachedContent_IfAvailable()
{ {
var builder = CreateBuilderWithResponseCaching(); var builder = TestUtils.CreateBuilderWithResponseCaching();
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
{ {
@ -36,7 +34,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesFreshContent_IfNotAvailable() public async void ServesFreshContent_IfNotAvailable()
{ {
var builder = CreateBuilderWithResponseCaching(); var builder = TestUtils.CreateBuilderWithResponseCaching();
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
{ {
@ -51,10 +49,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesCachedContent_IfVaryHeader_Matches() public async void ServesCachedContent_IfVaryHeader_Matches()
{ {
var builder = CreateBuilderWithResponseCaching(async (context) => var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
{ {
context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; context.Response.Headers[HeaderNames.Vary] = HeaderNames.From;
await DefaultRequestDelegate(context); await TestUtils.DefaultRequestDelegate(context);
}); });
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
@ -71,10 +69,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesFreshContent_IfVaryHeader_Mismatches() public async void ServesFreshContent_IfVaryHeader_Mismatches()
{ {
var builder = CreateBuilderWithResponseCaching(async (context) => var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
{ {
context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; context.Response.Headers[HeaderNames.Vary] = HeaderNames.From;
await DefaultRequestDelegate(context); await TestUtils.DefaultRequestDelegate(context);
}); });
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
@ -92,10 +90,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesCachedContent_IfVaryParams_Matches() public async void ServesCachedContent_IfVaryParams_Matches()
{ {
var builder = CreateBuilderWithResponseCaching(async (context) => var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
{ {
context.GetResponseCachingFeature().VaryParams = "param"; context.GetResponseCachingFeature().VaryParams = "param";
await DefaultRequestDelegate(context); await TestUtils.DefaultRequestDelegate(context);
}); });
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
@ -111,10 +109,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesCachedContent_IfVaryParamsExplicit_Matches_ParamNameCaseInsensitive() public async void ServesCachedContent_IfVaryParamsExplicit_Matches_ParamNameCaseInsensitive()
{ {
var builder = CreateBuilderWithResponseCaching(async (context) => var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
{ {
context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "paramb" }; context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "paramb" };
await DefaultRequestDelegate(context); await TestUtils.DefaultRequestDelegate(context);
}); });
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
@ -130,10 +128,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesCachedContent_IfVaryParamsStar_Matches_ParamNameCaseInsensitive() public async void ServesCachedContent_IfVaryParamsStar_Matches_ParamNameCaseInsensitive()
{ {
var builder = CreateBuilderWithResponseCaching(async (context) => var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
{ {
context.GetResponseCachingFeature().VaryParams = new[] { "*" }; context.GetResponseCachingFeature().VaryParams = new[] { "*" };
await DefaultRequestDelegate(context); await TestUtils.DefaultRequestDelegate(context);
}); });
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
@ -149,10 +147,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesCachedContent_IfVaryParamsExplicit_Matches_OrderInsensitive() public async void ServesCachedContent_IfVaryParamsExplicit_Matches_OrderInsensitive()
{ {
var builder = CreateBuilderWithResponseCaching(async (context) => var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
{ {
context.GetResponseCachingFeature().VaryParams = new[] { "ParamB", "ParamA" }; context.GetResponseCachingFeature().VaryParams = new[] { "ParamB", "ParamA" };
await DefaultRequestDelegate(context); await TestUtils.DefaultRequestDelegate(context);
}); });
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
@ -168,10 +166,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesCachedContent_IfVaryParamsStar_Matches_OrderInsensitive() public async void ServesCachedContent_IfVaryParamsStar_Matches_OrderInsensitive()
{ {
var builder = CreateBuilderWithResponseCaching(async (context) => var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
{ {
context.GetResponseCachingFeature().VaryParams = new[] { "*" }; context.GetResponseCachingFeature().VaryParams = new[] { "*" };
await DefaultRequestDelegate(context); await TestUtils.DefaultRequestDelegate(context);
}); });
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
@ -187,10 +185,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesFreshContent_IfVaryParams_Mismatches() public async void ServesFreshContent_IfVaryParams_Mismatches()
{ {
var builder = CreateBuilderWithResponseCaching(async (context) => var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
{ {
context.GetResponseCachingFeature().VaryParams = "param"; context.GetResponseCachingFeature().VaryParams = "param";
await DefaultRequestDelegate(context); await TestUtils.DefaultRequestDelegate(context);
}); });
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
@ -206,10 +204,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesFreshContent_IfVaryParamsExplicit_Mismatch_ParamValueCaseSensitive() public async void ServesFreshContent_IfVaryParamsExplicit_Mismatch_ParamValueCaseSensitive()
{ {
var builder = CreateBuilderWithResponseCaching(async (context) => var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
{ {
context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "ParamB" }; context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "ParamB" };
await DefaultRequestDelegate(context); await TestUtils.DefaultRequestDelegate(context);
}); });
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
@ -225,10 +223,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesFreshContent_IfVaryParamsStar_Mismatch_ParamValueCaseSensitive() public async void ServesFreshContent_IfVaryParamsStar_Mismatch_ParamValueCaseSensitive()
{ {
var builder = CreateBuilderWithResponseCaching(async (context) => var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
{ {
context.GetResponseCachingFeature().VaryParams = new[] { "*" }; context.GetResponseCachingFeature().VaryParams = new[] { "*" };
await DefaultRequestDelegate(context); await TestUtils.DefaultRequestDelegate(context);
}); });
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
@ -244,7 +242,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesFreshContent_IfRequestRequirements_NotMet() public async void ServesFreshContent_IfRequestRequirements_NotMet()
{ {
var builder = CreateBuilderWithResponseCaching(); var builder = TestUtils.CreateBuilderWithResponseCaching();
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
{ {
@ -263,7 +261,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void Serves504_IfOnlyIfCachedHeader_IsSpecified() public async void Serves504_IfOnlyIfCachedHeader_IsSpecified()
{ {
var builder = CreateBuilderWithResponseCaching(); var builder = TestUtils.CreateBuilderWithResponseCaching();
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
{ {
@ -283,10 +281,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesFreshContent_IfSetCookie_IsSpecified() public async void ServesFreshContent_IfSetCookie_IsSpecified()
{ {
var builder = CreateBuilderWithResponseCaching(async (context) => var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
{ {
var headers = context.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; var headers = context.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue";
await DefaultRequestDelegate(context); await TestUtils.DefaultRequestDelegate(context);
}); });
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
@ -302,7 +300,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesCachedContent_IfIHttpSendFileFeature_NotUsed() public async void ServesCachedContent_IfIHttpSendFileFeature_NotUsed()
{ {
var builder = CreateBuilderWithResponseCaching(app => var builder = TestUtils.CreateBuilderWithResponseCaching(app =>
{ {
app.Use(async (context, next) => app.Use(async (context, next) =>
{ {
@ -324,7 +322,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesFreshContent_IfIHttpSendFileFeature_Used() public async void ServesFreshContent_IfIHttpSendFileFeature_Used()
{ {
var builder = CreateBuilderWithResponseCaching( var builder = TestUtils.CreateBuilderWithResponseCaching(
app => app =>
{ {
app.Use(async (context, next) => app.Use(async (context, next) =>
@ -333,10 +331,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
await next.Invoke(); await next.Invoke();
}); });
}, },
async (context) => requestDelegate: async (context) =>
{ {
await context.Features.Get<IHttpSendFileFeature>().SendFileAsync("dummy", 0, 0, CancellationToken.None); await context.Features.Get<IHttpSendFileFeature>().SendFileAsync("dummy", 0, 0, CancellationToken.None);
await DefaultRequestDelegate(context); await TestUtils.DefaultRequestDelegate(context);
}); });
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
@ -352,7 +350,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesCachedContent_IfSubsequentRequest_ContainsNoStore() public async void ServesCachedContent_IfSubsequentRequest_ContainsNoStore()
{ {
var builder = CreateBuilderWithResponseCaching(); var builder = TestUtils.CreateBuilderWithResponseCaching();
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
{ {
@ -371,7 +369,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesFreshContent_IfInitialRequestContains_NoStore() public async void ServesFreshContent_IfInitialRequestContains_NoStore()
{ {
var builder = CreateBuilderWithResponseCaching(); var builder = TestUtils.CreateBuilderWithResponseCaching();
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
{ {
@ -390,7 +388,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void Serves304_IfIfModifiedSince_Satisfied() public async void Serves304_IfIfModifiedSince_Satisfied()
{ {
var builder = CreateBuilderWithResponseCaching(); var builder = TestUtils.CreateBuilderWithResponseCaching();
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
{ {
@ -407,7 +405,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesCachedContent_IfIfModifiedSince_NotSatisfied() public async void ServesCachedContent_IfIfModifiedSince_NotSatisfied()
{ {
var builder = CreateBuilderWithResponseCaching(); var builder = TestUtils.CreateBuilderWithResponseCaching();
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
{ {
@ -423,10 +421,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void Serves304_IfIfNoneMatch_Satisfied() public async void Serves304_IfIfNoneMatch_Satisfied()
{ {
var builder = CreateBuilderWithResponseCaching(async (context) => var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
{ {
var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\"");
await DefaultRequestDelegate(context); await TestUtils.DefaultRequestDelegate(context);
}); });
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
@ -444,10 +442,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesCachedContent_IfIfNoneMatch_NotSatisfied() public async void ServesCachedContent_IfIfNoneMatch_NotSatisfied()
{ {
var builder = CreateBuilderWithResponseCaching(async (context) => var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
{ {
var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\"");
await DefaultRequestDelegate(context); await TestUtils.DefaultRequestDelegate(context);
}); });
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
@ -464,7 +462,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesCachedContent_IfBodySize_IsCacheable() public async void ServesCachedContent_IfBodySize_IsCacheable()
{ {
var builder = CreateBuilderWithResponseCaching(new ResponseCachingOptions() var builder = TestUtils.CreateBuilderWithResponseCaching(options: new ResponseCachingOptions()
{ {
MaximumCachedBodySize = 100 MaximumCachedBodySize = 100
}); });
@ -482,7 +480,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesFreshContent_IfBodySize_IsNotCacheable() public async void ServesFreshContent_IfBodySize_IsNotCacheable()
{ {
var builder = CreateBuilderWithResponseCaching(new ResponseCachingOptions() var builder = TestUtils.CreateBuilderWithResponseCaching(options: new ResponseCachingOptions()
{ {
MaximumCachedBodySize = 1 MaximumCachedBodySize = 1
}); });
@ -500,10 +498,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact] [Fact]
public async void ServesCachedContent_WithoutReplacingCachedVaryBy_OnCacheMiss() public async void ServesCachedContent_WithoutReplacingCachedVaryBy_OnCacheMiss()
{ {
var builder = CreateBuilderWithResponseCaching(async (context) => var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
{ {
context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; context.Response.Headers[HeaderNames.Vary] = HeaderNames.From;
await DefaultRequestDelegate(context); await TestUtils.DefaultRequestDelegate(context);
}); });
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
@ -541,57 +539,5 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age));
Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
} }
private static RequestDelegate DefaultRequestDelegate = async (context) =>
{
var uniqueId = Guid.NewGuid().ToString();
var headers = context.Response.GetTypedHeaders();
headers.CacheControl = new CacheControlHeaderValue()
{
Public = true,
MaxAge = TimeSpan.FromSeconds(10)
};
headers.Date = DateTimeOffset.UtcNow;
headers.Headers["X-Value"] = uniqueId;
await context.Response.WriteAsync(uniqueId);
};
private static IWebHostBuilder CreateBuilderWithResponseCaching() =>
CreateBuilderWithResponseCaching(app => { }, new ResponseCachingOptions(), DefaultRequestDelegate);
private static IWebHostBuilder CreateBuilderWithResponseCaching(ResponseCachingOptions options) =>
CreateBuilderWithResponseCaching(app => { }, options, DefaultRequestDelegate);
private static IWebHostBuilder CreateBuilderWithResponseCaching(RequestDelegate requestDelegate) =>
CreateBuilderWithResponseCaching(app => { }, new ResponseCachingOptions(), requestDelegate);
private static IWebHostBuilder CreateBuilderWithResponseCaching(Action<IApplicationBuilder> configureDelegate) =>
CreateBuilderWithResponseCaching(configureDelegate, new ResponseCachingOptions(), DefaultRequestDelegate);
private static IWebHostBuilder CreateBuilderWithResponseCaching(Action<IApplicationBuilder> configureDelegate, RequestDelegate requestDelegate) =>
CreateBuilderWithResponseCaching(configureDelegate, new ResponseCachingOptions(), requestDelegate);
private static IWebHostBuilder CreateBuilderWithResponseCaching(Action<IApplicationBuilder> configureDelegate, ResponseCachingOptions options, RequestDelegate requestDelegate)
{
return new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddDistributedResponseCache();
})
.Configure(app =>
{
configureDelegate(app);
app.UseResponseCaching(options);
app.Run(requestDelegate);
});
}
private class DummySendFileFeature : IHttpSendFileFeature
{
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
{
return Task.FromResult(0);
}
}
} }
} }

View File

@ -0,0 +1,211 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
internal class TestUtils
{
internal static RequestDelegate DefaultRequestDelegate = async (context) =>
{
var uniqueId = Guid.NewGuid().ToString();
var headers = context.Response.GetTypedHeaders();
headers.CacheControl = new CacheControlHeaderValue()
{
Public = true,
MaxAge = TimeSpan.FromSeconds(10)
};
headers.Date = DateTimeOffset.UtcNow;
headers.Headers["X-Value"] = uniqueId;
await context.Response.WriteAsync(uniqueId);
};
internal static ICacheKeyProvider CreateTestKeyProvider()
{
return CreateTestKeyProvider(new ResponseCachingOptions());
}
internal static ICacheKeyProvider CreateTestKeyProvider(ResponseCachingOptions options)
{
return new CacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options));
}
internal static IWebHostBuilder CreateBuilderWithResponseCaching(
Action<IApplicationBuilder> configureDelegate = null,
ResponseCachingOptions options = null,
RequestDelegate requestDelegate = null)
{
if (configureDelegate == null)
{
configureDelegate = app => { };
}
if (options == null)
{
options = new ResponseCachingOptions();
}
if (requestDelegate == null)
{
requestDelegate = DefaultRequestDelegate;
}
return new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddDistributedResponseCache();
})
.Configure(app =>
{
configureDelegate(app);
app.UseResponseCaching(options);
app.Run(requestDelegate);
});
}
internal static ResponseCachingMiddleware CreateTestMiddleware(
IResponseCache responseCache = null,
ResponseCachingOptions options = null,
ICacheKeyProvider cacheKeyProvider = null,
ICacheabilityValidator cacheabilityValidator = null)
{
if (responseCache == null)
{
responseCache = new TestResponseCache();
}
if (options == null)
{
options = new ResponseCachingOptions();
}
if (cacheKeyProvider == null)
{
cacheKeyProvider = new CacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options));
}
if (cacheabilityValidator == null)
{
cacheabilityValidator = new TestCacheabilityValidator();
}
return new ResponseCachingMiddleware(
httpContext => TaskCache.CompletedTask,
responseCache,
Options.Create(options),
cacheabilityValidator,
cacheKeyProvider);
}
internal static ResponseCachingContext CreateTestContext()
{
return new ResponseCachingContext(new DefaultHttpContext());
}
}
internal class DummySendFileFeature : IHttpSendFileFeature
{
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
{
return TaskCache.CompletedTask;
}
}
internal class TestCacheabilityValidator : ICacheabilityValidator
{
public bool IsCachedEntryFresh(ResponseCachingContext context) => true;
public bool IsRequestCacheable(ResponseCachingContext context) => true;
public bool IsResponseCacheable(ResponseCachingContext context) => true;
}
internal class TestKeyProvider : ICacheKeyProvider
{
private readonly StringValues _baseKey;
private readonly StringValues _varyKey;
public TestKeyProvider(StringValues? lookupBaseKey = null, StringValues? lookupVaryKey = null)
{
if (lookupBaseKey.HasValue)
{
_baseKey = lookupBaseKey.Value;
}
if (lookupVaryKey.HasValue)
{
_varyKey = lookupVaryKey.Value;
}
}
public IEnumerable<string> CreateLookupBaseKeys(ResponseCachingContext context) => _baseKey;
public IEnumerable<string> CreateLookupVaryKeys(ResponseCachingContext context)
{
foreach (var baseKey in _baseKey)
{
foreach (var varyKey in _varyKey)
{
yield return baseKey + varyKey;
}
}
}
public string CreateStorageBaseKey(ResponseCachingContext context)
{
throw new NotImplementedException();
}
public string CreateStorageVaryKey(ResponseCachingContext context)
{
throw new NotImplementedException();
}
}
internal class TestResponseCache : IResponseCache
{
private readonly IDictionary<string, object> _storage = new Dictionary<string, object>();
public int GetCount { get; private set; }
public int SetCount { get; private set; }
public object Get(string key)
{
GetCount++;
try
{
return _storage[key];
}
catch
{
return null;
}
}
public void Remove(string key)
{
}
public void Set(string key, object entry, TimeSpan validFor)
{
SetCount++;
_storage[key] = entry;
}
}
internal class TestHttpSendFileFeature : IHttpSendFileFeature
{
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
{
return TaskCache.CompletedTask;
}
}
}