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();
}
return new CachedVaryRules { VaryKeyPrefix = varyKeyPrefix, VaryRules = new VaryRules() { Headers = headers, Params = param } };
return new CachedVaryRules { VaryKeyPrefix = varyKeyPrefix, Headers = headers, Params = param };
}
// See serialization format above
@ -222,14 +222,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
writer.Write(varyRules.VaryKeyPrefix);
writer.Write(varyRules.VaryRules.Headers.Count);
foreach (var header in varyRules.VaryRules.Headers)
writer.Write(varyRules.Headers.Count);
foreach (var header in varyRules.Headers)
{
writer.Write(header);
}
writer.Write(varyRules.VaryRules.Params.Count);
foreach (var param in varyRules.VaryRules.Params)
writer.Write(varyRules.Params.Count);
foreach (var param in varyRules.Params)
{
writer.Write(param);
}

View File

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

View File

@ -1,10 +1,10 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNetCore.ResponseCaching.Internal
namespace Microsoft.AspNetCore.ResponseCaching
{
internal class CachedResponseBody
public class CachedResponseBody
{
internal byte[] Body { get; set; }
public byte[] Body { get; internal set; }
}
}

View File

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

View File

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

View File

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

View File

@ -2,18 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.ResponseCaching.Internal;
namespace Microsoft.AspNetCore.ResponseCaching
{
// TODO: Temporary interface for endpoints to specify options for response caching
public static class ResponseCachingHttpContextExtensions
{
public static ResponseCachingState GetResponseCachingState(this HttpContext httpContext)
{
return httpContext.Features.Get<ResponseCachingState>();
}
public static ResponseCachingFeature GetResponseCachingFeature(this HttpContext httpContext)
{
return httpContext.Features.Get<ResponseCachingFeature>();

View File

@ -40,7 +40,7 @@ namespace Microsoft.Extensions.DependencyInjection
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>());
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.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Headers;
namespace Microsoft.AspNetCore.ResponseCaching
{
public interface ICacheabilityValidator
@ -11,23 +8,22 @@ namespace Microsoft.AspNetCore.ResponseCaching
/// <summary>
/// Determine the cacheability of an HTTP request.
/// </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>
bool RequestIsCacheable(HttpContext httpContext);
bool IsRequestCacheable(ResponseCachingContext context);
/// <summary>
/// Determine the cacheability of an HTTP response.
/// </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>
bool ResponseIsCacheable(HttpContext httpContext);
bool IsResponseCacheable(ResponseCachingContext context);
/// <summary>
/// Determine the freshness of the cached entry.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
/// <param name="cachedResponseHeaders">The <see cref="ResponseHeaders"/> of the cached entry.</param>
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
/// <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
{
internal static class HttpContextInternalExtensions
internal static class InternalHttpContextExtensions
{
internal static void AddResponseCachingFeature(this HttpContext httpContext)
{
@ -21,24 +21,5 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
httpContext.Features.Set<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.IO;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.ResponseCaching
{
internal class ResponseCachingContext
public class ResponseCachingContext
{
private readonly HttpContext _httpContext;
private readonly IResponseCache _cache;
private readonly ResponseCachingOptions _options;
private readonly ICacheabilityValidator _cacheabilityValidator;
private readonly IKeyProvider _keyProvider;
private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue();
private ResponseCachingState _state;
private RequestHeaders _requestHeaders;
private ResponseHeaders _responseHeaders;
private CacheControlHeaderValue _requestCacheControl;
private CacheControlHeaderValue _responseCacheControl;
internal ResponseCachingContext(
HttpContext httpContext,
IResponseCache cache,
ResponseCachingOptions options,
ICacheabilityValidator cacheabilityValidator,
IKeyProvider keyProvider)
HttpContext httpContext)
{
_httpContext = httpContext;
_cache = cache;
_options = options;
_cacheabilityValidator = cacheabilityValidator;
_keyProvider = keyProvider;
HttpContext = httpContext;
}
internal ResponseCachingState State
{
get
{
if (_state == null)
{
_state = _httpContext.GetResponseCachingState();
}
return _state;
}
}
public HttpContext HttpContext { get; }
public bool ShouldCacheResponse { get; internal set; }
public string StorageBaseKey { get; internal set; }
public string StorageVaryKey { get; internal set; }
public DateTimeOffset ResponseTime { get; internal set; }
public TimeSpan CachedEntryAge { get; internal set; }
public TimeSpan CachedResponseValidFor { get; internal set; }
public CachedResponse CachedResponse { get; internal set; }
public CachedVaryRules CachedVaryRules { get; internal set; }
internal bool ResponseStarted { get; set; }
private Stream OriginalResponseStream { get; set; }
internal Stream OriginalResponseStream { get; set; }
private ResponseCacheStream ResponseCacheStream { get; set; }
internal ResponseCacheStream ResponseCacheStream { get; set; }
private IHttpSendFileFeature OriginalSendFileFeature { get; set; }
internal IHttpSendFileFeature OriginalSendFileFeature { get; set; }
internal async Task<bool> TryServeCachedResponseAsync(CachedResponse cachedResponse)
internal ResponseHeaders CachedResponseHeaders { get; set; }
internal RequestHeaders TypedRequestHeaders
{
State.CachedResponse = cachedResponse;
var cachedResponseHeaders = new ResponseHeaders(State.CachedResponse.Headers);
State.ResponseTime = _options.SystemClock.UtcNow;
var cachedEntryAge = State.ResponseTime - State.CachedResponse.Created;
State.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero;
if (_cacheabilityValidator.CachedEntryIsFresh(_httpContext, cachedResponseHeaders))
get
{
// Check conditional request rules
if (ConditionalRequestSatisfied(cachedResponseHeaders))
if (_requestHeaders == null)
{
_httpContext.Response.StatusCode = StatusCodes.Status304NotModified;
_requestHeaders = HttpContext.Request.GetTypedHeaders();
}
else
{
var response = _httpContext.Response;
// Copy the cached status code and response headers
response.StatusCode = State.CachedResponse.StatusCode;
foreach (var header in State.CachedResponse.Headers)
{
response.Headers.Add(header);
}
response.Headers[HeaderNames.Age] = State.CachedEntryAge.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture);
var body = State.CachedResponse.Body ??
((CachedResponseBody)_cache.Get(State.CachedResponse.BodyKeyPrefix))?.Body;
// If the body is not found, something went wrong.
if (body == null)
{
return false;
}
// Copy the cached response body
if (body.Length > 0)
{
// Add a content-length if required
if (response.ContentLength == null && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding]))
{
response.ContentLength = body.Length;
}
await response.Body.WriteAsync(body, 0, body.Length);
}
}
return true;
}
else
{
// TODO: Validate with endpoint instead
}
return false;
}
internal async Task<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();
return _requestHeaders;
}
}
internal void FinalizeCachingBody()
internal ResponseHeaders TypedResponseHeaders
{
if (State.ShouldCacheResponse &&
ResponseCacheStream.BufferingEnabled &&
(State.ResponseHeaders.ContentLength == null ||
State.ResponseHeaders.ContentLength == ResponseCacheStream.BufferedStream.Length))
get
{
if (ResponseCacheStream.BufferedStream.Length >= _options.MinimumSplitBodySize)
if (_responseHeaders == null)
{
// Store response and response body separately
_cache.Set(State.StorageVaryKey ?? State.StorageBaseKey, State.CachedResponse, State.CachedResponseValidFor);
var cachedResponseBody = new CachedResponseBody()
{
Body = ResponseCacheStream.BufferedStream.ToArray()
};
_cache.Set(State.CachedResponse.BodyKeyPrefix, cachedResponseBody, State.CachedResponseValidFor);
_responseHeaders = HttpContext.Response.GetTypedHeaders();
}
else
return _responseHeaders;
}
}
internal CacheControlHeaderValue RequestCacheControlHeaderValue
{
get
{
if (_requestCacheControl == null)
{
// Store response and response body together
State.CachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray();
_cache.Set(State.StorageVaryKey ?? State.StorageBaseKey, State.CachedResponse, State.CachedResponseValidFor);
_requestCacheControl = TypedRequestHeaders.CacheControl ?? EmptyCacheControl;
}
return _requestCacheControl;
}
}
internal void OnResponseStarting()
internal CacheControlHeaderValue ResponseCacheControlHeaderValue
{
if (!ResponseStarted)
get
{
ResponseStarted = true;
State.ResponseTime = _options.SystemClock.UtcNow;
FinalizeCachingHeaders();
}
}
internal void ShimResponseStream()
{
// TODO: Consider caching large responses on disk and serving them from there.
// Shim response stream
OriginalResponseStream = _httpContext.Response.Body;
ResponseCacheStream = new ResponseCacheStream(OriginalResponseStream, _options.MaximumCachedBodySize);
_httpContext.Response.Body = ResponseCacheStream;
// Shim IHttpSendFileFeature
OriginalSendFileFeature = _httpContext.Features.Get<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++)
if (_responseCacheControl == null)
{
newArray[i] = originalArray[i].ToUpperInvariant();
_responseCacheControl = TypedResponseHeaders.CacheControl ?? EmptyCacheControl;
}
// Since the casing has already been normalized, use Ordinal comparison
Array.Sort(newArray, StringComparer.Ordinal);
return new StringValues(newArray);
return _responseCacheControl;
}
}
}

View File

@ -2,35 +2,37 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.ResponseCaching
{
public class ResponseCachingMiddleware
{
private static readonly Func<object, Task> OnStartingCallback = state =>
{
((ResponseCachingContext)state).OnResponseStarting();
return TaskCache.CompletedTask;
};
private static readonly TimeSpan DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10);
private readonly RequestDelegate _next;
private readonly IResponseCache _cache;
private readonly ResponseCachingOptions _options;
private readonly ICacheabilityValidator _cacheabilityValidator;
private readonly IKeyProvider _keyProvider;
private readonly ICacheKeyProvider _cacheKeyProvider;
private readonly Func<object, Task> _onStartingCallback;
public ResponseCachingMiddleware(
RequestDelegate next,
IResponseCache cache,
IOptions<ResponseCachingOptions> options,
ICacheabilityValidator cacheabilityValidator,
IKeyProvider keyProvider)
ICacheKeyProvider cacheKeyProvider)
{
if (next == null)
{
@ -48,70 +50,352 @@ namespace Microsoft.AspNetCore.ResponseCaching
{
throw new ArgumentNullException(nameof(cacheabilityValidator));
}
if (keyProvider == null)
if (cacheKeyProvider == null)
{
throw new ArgumentNullException(nameof(keyProvider));
throw new ArgumentNullException(nameof(cacheKeyProvider));
}
_next = next;
_cache = cache;
_options = options.Value;
_cacheabilityValidator = cacheabilityValidator;
_keyProvider = keyProvider;
_cacheKeyProvider = cacheKeyProvider;
_onStartingCallback = state =>
{
OnResponseStarting((ResponseCachingContext)state);
return TaskCache.CompletedTask;
};
}
public async Task Invoke(HttpContext context)
public async Task Invoke(HttpContext httpContext)
{
context.AddResponseCachingState();
var context = new ResponseCachingContext(httpContext);
try
// Should we attempt any caching logic?
if (_cacheabilityValidator.IsRequestCacheable(context))
{
var cachingContext = new ResponseCachingContext(
context,
_cache,
_options,
_cacheabilityValidator,
_keyProvider);
// Should we attempt any caching logic?
if (_cacheabilityValidator.RequestIsCacheable(context))
// Can this request be served from cache?
if (await TryServeFromCacheAsync(context))
{
// Can this request be served from cache?
if (await cachingContext.TryServeFromCacheAsync())
{
return;
}
return;
}
// Hook up to listen to the response stream
cachingContext.ShimResponseStream();
// Hook up to listen to the response stream
ShimResponseStream(context);
try
{
// Subscribe to OnStarting event
context.Response.OnStarting(OnStartingCallback, cachingContext);
try
{
// Subscribe to OnStarting event
httpContext.Response.OnStarting(_onStartingCallback, context);
await _next(context);
await _next(httpContext);
// If there was no response body, check the response headers now. We can cache things like redirects.
cachingContext.OnResponseStarting();
// If there was no response body, check the response headers now. We can cache things like redirects.
OnResponseStarting(context);
// Finalize the cache entry
cachingContext.FinalizeCachingBody();
}
finally
{
cachingContext.UnshimResponseStream();
}
// Finalize the cache entry
FinalizeCachingBody(context);
}
finally
{
UnshimResponseStream(context);
}
}
else
{
// TODO: Invalidate resources for successful unsafe methods? Required by RFC
await _next(httpContext);
}
}
internal async Task<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
{
// TODO: Invalidate resources for successful unsafe methods? Required by RFC
await _next(context);
var response = context.HttpContext.Response;
// Copy the cached status code and response headers
response.StatusCode = context.CachedResponse.StatusCode;
foreach (var header in context.CachedResponse.Headers)
{
response.Headers.Add(header);
}
response.Headers[HeaderNames.Age] = context.CachedEntryAge.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture);
var body = context.CachedResponse.Body ??
((CachedResponseBody)_cache.Get(context.CachedResponse.BodyKeyPrefix))?.Body;
// If the body is not found, something went wrong.
if (body == null)
{
return false;
}
// Copy the cached response body
if (body.Length > 0)
{
// Add a content-length if required
if (response.ContentLength == null && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding]))
{
response.ContentLength = body.Length;
}
await response.Body.WriteAsync(body, 0, body.Length);
}
}
return true;
}
else
{
// TODO: Validate with endpoint instead
}
return false;
}
internal async Task<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()
{
VaryKeyPrefix = FastGuid.NewGuid().IdString,
VaryRules = new VaryRules()
VaryKeyPrefix = FastGuid.NewGuid().IdString
};
AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
@ -95,10 +94,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var cachedVaryRule = new CachedVaryRules()
{
VaryKeyPrefix = FastGuid.NewGuid().IdString,
VaryRules = new VaryRules()
{
Headers = headers
}
Headers = headers
};
AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
@ -111,10 +107,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var cachedVaryRule = new CachedVaryRules()
{
VaryKeyPrefix = FastGuid.NewGuid().IdString,
VaryRules = new VaryRules()
{
Params = param
}
Params = param
};
AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
@ -128,11 +121,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var cachedVaryRule = new CachedVaryRules()
{
VaryKeyPrefix = FastGuid.NewGuid().IdString,
VaryRules = new VaryRules()
{
Headers = headers,
Params = param
}
Headers = headers,
Params = param
};
AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
@ -145,10 +135,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var cachedVaryRule = new CachedVaryRules()
{
VaryKeyPrefix = FastGuid.NewGuid().IdString,
VaryRules = new VaryRules()
{
Headers = headers
}
Headers = headers
};
var serializedEntry = CacheEntrySerializer.Serialize(cachedVaryRule);
Array.Reverse(serializedEntry);
@ -188,8 +175,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Assert.NotNull(actual);
Assert.NotNull(expected);
Assert.Equal(expected.VaryKeyPrefix, actual.VaryKeyPrefix);
Assert.Equal(expected.VaryRules.Headers, actual.VaryRules.Headers);
Assert.Equal(expected.VaryRules.Params, actual.VaryRules.Params);
Assert.Equal(expected.Headers, actual.Headers);
Assert.Equal(expected.Params, actual.Params);
}
}
}

View File

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

View File

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

View File

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

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