API review renames and updates
This commit is contained in:
parent
e236e64055
commit
ccfa090e6e
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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¶mB=ValueB");
|
||||
var keyProvider = CreateTestKeyProvider();
|
||||
var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
|
||||
var context = TestUtils.CreateTestContext();
|
||||
context.HttpContext.Request.QueryString = new QueryString("?parama=ValueA¶mB=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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue