Restructure response caching middleware flow

- Always add IresponseCachingFeatu8re before calling the next middleware #81
- Use If-Modified-Since instead of the incorrect If-Unmodified-Since header #83
- Handle proxy-revalidate in the same way as must-revalidate #83
- Handle max-stale with no specified limit #83
- Bypass cache lookup for no-cache but store the response #83
- Bypass response capturing and buffering when no-store is specified #83
- Replace IsRequestCacheable cache policy with three new independent policy checks to reflect these changes
- Modify middleware flow to accommodate cache policy updates
This commit is contained in:
John Luo 2017-01-10 14:48:39 -08:00
parent a5717aa583
commit 3bf5f6a1ce
9 changed files with 488 additions and 186 deletions

View File

@ -6,21 +6,35 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
public interface IResponseCachingPolicyProvider public interface IResponseCachingPolicyProvider
{ {
/// <summary> /// <summary>
/// Determine wehther the response cache middleware should be executed for the incoming HTTP request. /// Determine whether the response caching logic should be attempted for the incoming HTTP request.
/// </summary> /// </summary>
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param> /// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
/// <returns><c>true</c> if the request is cacheable; otherwise <c>false</c>.</returns> /// <returns><c>true</c> if response caching logic should be attempted; otherwise <c>false</c>.</returns>
bool IsRequestCacheable(ResponseCachingContext context); bool AttemptResponseCaching(ResponseCachingContext context);
/// <summary> /// <summary>
/// Determine whether the response received by the middleware be cached for future requests. /// Determine whether a cache lookup is allowed for the incoming HTTP request.
/// </summary>
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
/// <returns><c>true</c> if cache lookup for this request is allowed; otherwise <c>false</c>.</returns>
bool AllowCacheLookup(ResponseCachingContext context);
/// <summary>
/// Determine whether storage of the response is allowed for the incoming HTTP request.
/// </summary>
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
/// <returns><c>true</c> if storage of the response for this request is allowed; otherwise <c>false</c>.</returns>
bool AllowCacheStorage(ResponseCachingContext context);
/// <summary>
/// Determine whether the response received by the middleware can be cached for future requests.
/// </summary> /// </summary>
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param> /// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
/// <returns><c>true</c> if the response is cacheable; otherwise <c>false</c>.</returns> /// <returns><c>true</c> if the response is cacheable; otherwise <c>false</c>.</returns>
bool IsResponseCacheable(ResponseCachingContext context); bool IsResponseCacheable(ResponseCachingContext context);
/// <summary> /// <summary>
/// Determine whether the response retrieved from the response cache is fresh and be served. /// Determine whether the response retrieved from the response cache is fresh and can be served.
/// </summary> /// </summary>
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param> /// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
/// <returns><c>true</c> if the cached entry is fresh; otherwise <c>false</c>.</returns> /// <returns><c>true</c> if the cached entry is fresh; otherwise <c>false</c>.</returns>

View File

@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
private static Action<ILogger, int, Exception> _logResponseWithUnsuccessfulStatusCodeNotCacheable; private static Action<ILogger, int, Exception> _logResponseWithUnsuccessfulStatusCodeNotCacheable;
private static Action<ILogger, Exception> _logNotModifiedIfNoneMatchStar; private static Action<ILogger, Exception> _logNotModifiedIfNoneMatchStar;
private static Action<ILogger, EntityTagHeaderValue, Exception> _logNotModifiedIfNoneMatchMatched; private static Action<ILogger, EntityTagHeaderValue, Exception> _logNotModifiedIfNoneMatchMatched;
private static Action<ILogger, DateTimeOffset, DateTimeOffset, Exception> _logNotModifiedIfUnmodifiedSinceSatisfied; private static Action<ILogger, DateTimeOffset, DateTimeOffset, Exception> _logNotModifiedIfModifiedSinceSatisfied;
private static Action<ILogger, Exception> _logNotModifiedServed; private static Action<ILogger, Exception> _logNotModifiedServed;
private static Action<ILogger, Exception> _logCachedResponseServed; private static Action<ILogger, Exception> _logCachedResponseServed;
private static Action<ILogger, Exception> _logGatewayTimeoutServed; private static Action<ILogger, Exception> _logGatewayTimeoutServed;
@ -40,6 +40,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
private static Action<ILogger, Exception> _logResponseCached; private static Action<ILogger, Exception> _logResponseCached;
private static Action<ILogger, Exception> _logResponseNotCached; private static Action<ILogger, Exception> _logResponseNotCached;
private static Action<ILogger, Exception> _logResponseContentLengthMismatchNotCached; private static Action<ILogger, Exception> _logResponseContentLengthMismatchNotCached;
private static Action<ILogger, TimeSpan, TimeSpan, Exception> _logExpirationInfiniteMaxStaleSatisfied;
static LoggerExtensions() static LoggerExtensions()
{ {
@ -70,7 +71,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
_logExpirationMustRevalidate = LoggerMessage.Define<TimeSpan, TimeSpan>( _logExpirationMustRevalidate = LoggerMessage.Define<TimeSpan, TimeSpan>(
logLevel: LogLevel.Debug, logLevel: LogLevel.Debug,
eventId: 7, eventId: 7,
formatString: "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. It must be revalidated because the 'must-revalidate' cache directive is specified."); formatString: "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. It must be revalidated because the 'must-revalidate' or 'proxy-revalidate' cache directive is specified.");
_logExpirationMaxStaleSatisfied = LoggerMessage.Define<TimeSpan, TimeSpan, TimeSpan>( _logExpirationMaxStaleSatisfied = LoggerMessage.Define<TimeSpan, TimeSpan, TimeSpan>(
logLevel: LogLevel.Debug, logLevel: LogLevel.Debug,
eventId: 8, eventId: 8,
@ -119,10 +120,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
logLevel: LogLevel.Debug, logLevel: LogLevel.Debug,
eventId: 19, eventId: 19,
formatString: $"The ETag {{ETag}} in the '{HeaderNames.IfNoneMatch}' header matched the ETag of a cached entry."); formatString: $"The ETag {{ETag}} in the '{HeaderNames.IfNoneMatch}' header matched the ETag of a cached entry.");
_logNotModifiedIfUnmodifiedSinceSatisfied = LoggerMessage.Define<DateTimeOffset, DateTimeOffset>( _logNotModifiedIfModifiedSinceSatisfied = LoggerMessage.Define<DateTimeOffset, DateTimeOffset>(
logLevel: LogLevel.Debug, logLevel: LogLevel.Debug,
eventId: 20, eventId: 20,
formatString: $"The last modified date of {{LastModified}} is before the date {{IfUnmodifiedSince}} specified in the '{HeaderNames.IfUnmodifiedSince}' header."); formatString: $"The last modified date of {{LastModified}} is before the date {{IfModifiedSince}} specified in the '{HeaderNames.IfModifiedSince}' header.");
_logNotModifiedServed = LoggerMessage.Define( _logNotModifiedServed = LoggerMessage.Define(
logLevel: LogLevel.Information, logLevel: LogLevel.Information,
eventId: 21, eventId: 21,
@ -155,6 +156,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
logLevel: LogLevel.Warning, logLevel: LogLevel.Warning,
eventId: 28, eventId: 28,
formatString: $"The response could not be cached for this request because the '{HeaderNames.ContentLength}' did not match the body length."); formatString: $"The response could not be cached for this request because the '{HeaderNames.ContentLength}' did not match the body length.");
_logExpirationInfiniteMaxStaleSatisfied = LoggerMessage.Define<TimeSpan, TimeSpan>(
logLevel: LogLevel.Debug,
eventId: 29,
formatString: "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. However, the 'max-stale' cache directive was specified without an assigned value and a stale response of any age is accepted.");
} }
internal static void LogRequestMethodNotCacheable(this ILogger logger, string method) internal static void LogRequestMethodNotCacheable(this ILogger logger, string method)
@ -252,9 +257,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
_logNotModifiedIfNoneMatchMatched(logger, etag, null); _logNotModifiedIfNoneMatchMatched(logger, etag, null);
} }
internal static void LogNotModifiedIfUnmodifiedSinceSatisfied(this ILogger logger, DateTimeOffset lastModified, DateTimeOffset ifUnmodifiedSince) internal static void LogNotModifiedIfModifiedSinceSatisfied(this ILogger logger, DateTimeOffset lastModified, DateTimeOffset ifModifiedSince)
{ {
_logNotModifiedIfUnmodifiedSinceSatisfied(logger, lastModified, ifUnmodifiedSince, null); _logNotModifiedIfModifiedSinceSatisfied(logger, lastModified, ifModifiedSince, null);
} }
internal static void LogNotModifiedServed(this ILogger logger) internal static void LogNotModifiedServed(this ILogger logger)
@ -296,5 +301,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
{ {
_logResponseContentLengthMismatchNotCached(logger, null); _logResponseContentLengthMismatchNotCached(logger, null);
} }
internal static void LogExpirationInfiniteMaxStaleSatisfied(this ILogger logger, TimeSpan age, TimeSpan maxAge)
{
_logExpirationInfiniteMaxStaleSatisfied(logger, age, maxAge, null);
}
} }
} }

View File

@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
internal ILogger Logger { get; } internal ILogger Logger { get; }
internal bool ShouldCacheResponse { get; set; } internal bool ShouldCacheResponse { get; set; }
internal string BaseKey { get; set; } internal string BaseKey { get; set; }

View File

@ -10,10 +10,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
{ {
public class ResponseCachingPolicyProvider : IResponseCachingPolicyProvider public class ResponseCachingPolicyProvider : IResponseCachingPolicyProvider
{ {
public virtual bool IsRequestCacheable(ResponseCachingContext context) public virtual bool AttemptResponseCaching(ResponseCachingContext context)
{ {
// Verify the method
var request = context.HttpContext.Request; var request = context.HttpContext.Request;
// Verify the method
if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsHead(request.Method)) if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsHead(request.Method))
{ {
context.Logger.LogRequestMethodNotCacheable(request.Method); context.Logger.LogRequestMethodNotCacheable(request.Method);
@ -27,6 +28,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
return false; return false;
} }
return true;
}
public virtual bool AllowCacheLookup(ResponseCachingContext context)
{
var request = context.HttpContext.Request;
// Verify request cache-control parameters // Verify request cache-control parameters
if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl])) if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl]))
{ {
@ -50,6 +58,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
return true; return true;
} }
public virtual bool AllowCacheStorage(ResponseCachingContext context)
{
// Check request no-store
return !HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.NoStoreString);
}
public virtual bool IsResponseCacheable(ResponseCachingContext context) public virtual bool IsResponseCacheable(ResponseCachingContext context)
{ {
var responseCacheControlHeader = context.HttpContext.Response.Headers[HeaderNames.CacheControl]; var responseCacheControlHeader = context.HttpContext.Response.Headers[HeaderNames.CacheControl];
@ -61,9 +75,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
return false; return false;
} }
// Check no-store // Check response no-store
if (HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.NoStoreString) if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoStoreString))
|| HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoStoreString))
{ {
context.Logger.LogResponseWithNoStoreNotCacheable(); context.Logger.LogResponseWithNoStoreNotCacheable();
return false; return false;
@ -187,17 +200,26 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
// Validate max age // Validate max age
if (age >= lowestMaxAge) if (age >= lowestMaxAge)
{ {
// Must revalidate // Must revalidate or proxy revalidate
if (HeaderUtilities.ContainsCacheDirective(cachedCacheControlHeaders, CacheControlHeaderValue.MustRevalidateString)) if (HeaderUtilities.ContainsCacheDirective(cachedCacheControlHeaders, CacheControlHeaderValue.MustRevalidateString)
|| HeaderUtilities.ContainsCacheDirective(cachedCacheControlHeaders, CacheControlHeaderValue.ProxyRevalidateString))
{ {
context.Logger.LogExpirationMustRevalidate(age, lowestMaxAge.Value); context.Logger.LogExpirationMustRevalidate(age, lowestMaxAge.Value);
return false; return false;
} }
TimeSpan? requestMaxStale; TimeSpan? requestMaxStale;
var maxStaleExist = HeaderUtilities.ContainsCacheDirective(requestCacheControlHeaders, CacheControlHeaderValue.MaxStaleString);
HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxStaleString, out requestMaxStale); HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxStaleString, out requestMaxStale);
// Request allows stale values // Request allows stale values with no age limit
if (maxStaleExist && !requestMaxStale.HasValue)
{
context.Logger.LogExpirationInfiniteMaxStaleSatisfied(age, lowestMaxAge.Value);
return true;
}
// Request allows stale values with age limit
if (requestMaxStale.HasValue && age - lowestMaxAge < requestMaxStale) if (requestMaxStale.HasValue && age - lowestMaxAge < requestMaxStale)
{ {
context.Logger.LogExpirationMaxStaleSatisfied(age, lowestMaxAge.Value, requestMaxStale.Value); context.Logger.LogExpirationMaxStaleSatisfied(age, lowestMaxAge.Value, requestMaxStale.Value);

View File

@ -74,39 +74,53 @@ namespace Microsoft.AspNetCore.ResponseCaching
var context = new ResponseCachingContext(httpContext, _logger); var context = new ResponseCachingContext(httpContext, _logger);
// Should we attempt any caching logic? // Should we attempt any caching logic?
if (_policyProvider.IsRequestCacheable(context)) if (_policyProvider.AttemptResponseCaching(context))
{ {
// Can this request be served from cache? // Can this request be served from cache?
if (await TryServeFromCacheAsync(context)) if (_policyProvider.AllowCacheLookup(context) && await TryServeFromCacheAsync(context))
{ {
return; return;
} }
// Hook up to listen to the response stream // Should we store the response to this request?
ShimResponseStream(context); if (_policyProvider.AllowCacheStorage(context))
try
{ {
// Subscribe to OnStarting event // Hook up to listen to the response stream
httpContext.Response.OnStarting(_onStartingCallback, context); ShimResponseStream(context);
await _next(httpContext); try
{
// Subscribe to OnStarting event
httpContext.Response.OnStarting(_onStartingCallback, context);
// If there was no response body, check the response headers now. We can cache things like redirects. await _next(httpContext);
await OnResponseStartingAsync(context);
// Finalize the cache entry // If there was no response body, check the response headers now. We can cache things like redirects.
await FinalizeCacheBodyAsync(context); await OnResponseStartingAsync(context);
}
finally // Finalize the cache entry
{ await FinalizeCacheBodyAsync(context);
UnshimResponseStream(context); }
finally
{
UnshimResponseStream(context);
}
return;
} }
} }
else
// Response should not be captured but add IResponseCachingFeature which may be required when the response is generated
AddResponseCachingFeature(httpContext);
try
{ {
await _next(httpContext); await _next(httpContext);
} }
finally
{
RemoveResponseCachingFeature(httpContext);
}
} }
internal async Task<bool> TryServeCachedResponseAsync(ResponseCachingContext context, IResponseCacheEntry cacheEntry) internal async Task<bool> TryServeCachedResponseAsync(ResponseCachingContext context, IResponseCacheEntry cacheEntry)
@ -220,6 +234,12 @@ namespace Microsoft.AspNetCore.ResponseCaching
(context.ResponseExpires - context.ResponseTime.Value) ?? (context.ResponseExpires - context.ResponseTime.Value) ??
DefaultExpirationTimeSpan; DefaultExpirationTimeSpan;
// Generate a base key if none exist
if (string.IsNullOrEmpty(context.BaseKey))
{
context.BaseKey = _keyProvider.CreateBaseKey(context);
}
// Check if any vary rules exist // Check if any vary rules exist
if (!StringValues.IsNullOrEmpty(varyHeaders) || !StringValues.IsNullOrEmpty(varyQueryKeys)) if (!StringValues.IsNullOrEmpty(varyHeaders) || !StringValues.IsNullOrEmpty(varyQueryKeys))
{ {
@ -279,9 +299,9 @@ namespace Microsoft.AspNetCore.ResponseCaching
internal async Task FinalizeCacheBodyAsync(ResponseCachingContext context) internal async Task FinalizeCacheBodyAsync(ResponseCachingContext context)
{ {
var contentLength = context.HttpContext.Response.ContentLength;
if (context.ShouldCacheResponse && context.ResponseCachingStream.BufferingEnabled) if (context.ShouldCacheResponse && context.ResponseCachingStream.BufferingEnabled)
{ {
var contentLength = context.HttpContext.Response.ContentLength;
var bufferStream = context.ResponseCachingStream.GetBufferStream(); var bufferStream = context.ResponseCachingStream.GetBufferStream();
if (!contentLength.HasValue || contentLength == bufferStream.Length) if (!contentLength.HasValue || contentLength == bufferStream.Length)
{ {
@ -322,6 +342,15 @@ namespace Microsoft.AspNetCore.ResponseCaching
} }
} }
internal static void AddResponseCachingFeature(HttpContext context)
{
if (context.Features.Get<IResponseCachingFeature>() != null)
{
throw new InvalidOperationException($"Another instance of {nameof(ResponseCachingFeature)} already exists. Only one instance of {nameof(ResponseCachingMiddleware)} can be configured for an application.");
}
context.Features.Set<IResponseCachingFeature>(new ResponseCachingFeature());
}
internal void ShimResponseStream(ResponseCachingContext context) internal void ShimResponseStream(ResponseCachingContext context)
{ {
// Shim response stream // Shim response stream
@ -337,13 +366,12 @@ namespace Microsoft.AspNetCore.ResponseCaching
} }
// Add IResponseCachingFeature // Add IResponseCachingFeature
if (context.HttpContext.Features.Get<IResponseCachingFeature>() != null) AddResponseCachingFeature(context.HttpContext);
{
throw new InvalidOperationException($"Another instance of {nameof(ResponseCachingFeature)} already exists. Only one instance of {nameof(ResponseCachingMiddleware)} can be configured for an application.");
}
context.HttpContext.Features.Set<IResponseCachingFeature>(new ResponseCachingFeature());
} }
internal static void RemoveResponseCachingFeature(HttpContext context) =>
context.Features.Set<IResponseCachingFeature>(null);
internal static void UnshimResponseStream(ResponseCachingContext context) internal static void UnshimResponseStream(ResponseCachingContext context)
{ {
// Unshim response stream // Unshim response stream
@ -353,7 +381,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
context.HttpContext.Features.Set(context.OriginalSendFileFeature); context.HttpContext.Features.Set(context.OriginalSendFileFeature);
// Remove IResponseCachingFeature // Remove IResponseCachingFeature
context.HttpContext.Features.Set<IResponseCachingFeature>(null); RemoveResponseCachingFeature(context.HttpContext);
} }
internal static bool ContentIsNotModified(ResponseCachingContext context) internal static bool ContentIsNotModified(ResponseCachingContext context)
@ -388,8 +416,8 @@ namespace Microsoft.AspNetCore.ResponseCaching
} }
else else
{ {
var ifUnmodifiedSince = context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince]; var ifModifiedSince = context.HttpContext.Request.Headers[HeaderNames.IfModifiedSince];
if (!StringValues.IsNullOrEmpty(ifUnmodifiedSince)) if (!StringValues.IsNullOrEmpty(ifModifiedSince))
{ {
DateTimeOffset modified; DateTimeOffset modified;
if (!HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.LastModified], out modified) && if (!HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.LastModified], out modified) &&
@ -398,11 +426,11 @@ namespace Microsoft.AspNetCore.ResponseCaching
return false; return false;
} }
DateTimeOffset unmodifiedSince; DateTimeOffset modifiedSince;
if (HeaderUtilities.TryParseDate(ifUnmodifiedSince, out unmodifiedSince) && if (HeaderUtilities.TryParseDate(ifModifiedSince, out modifiedSince) &&
modified <= unmodifiedSince) modified <= modifiedSince)
{ {
context.Logger.LogNotModifiedIfUnmodifiedSinceSatisfied(modified, unmodifiedSince); context.Logger.LogNotModifiedIfModifiedSinceSatisfied(modified, modifiedSince);
return true; return true;
} }
} }

View File

@ -5,7 +5,9 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
@ -157,14 +159,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
[Fact] [Fact]
public void ContentIsNotModified_IfUnmodifiedSince_FallsbackToDateHeader() public void ContentIsNotModified_IfModifiedSince_FallsbackToDateHeader()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var sink = new TestSink(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink); var context = TestUtils.CreateTestContext(sink);
context.CachedResponseHeaders = new HeaderDictionary(); context.CachedResponseHeaders = new HeaderDictionary();
context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); context.HttpContext.Request.Headers[HeaderNames.IfModifiedSince] = HeaderUtilities.FormatDate(utcNow);
// Verify modifications in the past succeeds // Verify modifications in the past succeeds
context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10));
@ -183,19 +185,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
// Verify logging // Verify logging
TestUtils.AssertLoggedMessages( TestUtils.AssertLoggedMessages(
sink.Writes, sink.Writes,
LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied, LoggedMessage.NotModifiedIfModifiedSinceSatisfied,
LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied); LoggedMessage.NotModifiedIfModifiedSinceSatisfied);
} }
[Fact] [Fact]
public void ContentIsNotModified_IfUnmodifiedSince_LastModifiedOverridesDateHeader() public void ContentIsNotModified_IfModifiedSince_LastModifiedOverridesDateHeader()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var sink = new TestSink(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink); var context = TestUtils.CreateTestContext(sink);
context.CachedResponseHeaders = new HeaderDictionary(); context.CachedResponseHeaders = new HeaderDictionary();
context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); context.HttpContext.Request.Headers[HeaderNames.IfModifiedSince] = HeaderUtilities.FormatDate(utcNow);
// Verify modifications in the past succeeds // Verify modifications in the past succeeds
context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10));
@ -217,20 +219,20 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
// Verify logging // Verify logging
TestUtils.AssertLoggedMessages( TestUtils.AssertLoggedMessages(
sink.Writes, sink.Writes,
LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied, LoggedMessage.NotModifiedIfModifiedSinceSatisfied,
LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied); LoggedMessage.NotModifiedIfModifiedSinceSatisfied);
} }
[Fact] [Fact]
public void ContentIsNotModified_IfNoneMatch_Overrides_IfUnmodifiedSince_ToTrue() public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToTrue()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var sink = new TestSink(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink); var context = TestUtils.CreateTestContext(sink);
context.CachedResponseHeaders = new HeaderDictionary(); context.CachedResponseHeaders = new HeaderDictionary();
// This would fail the IfUnmodifiedSince checks // This would fail the IfModifiedSince checks
context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); context.HttpContext.Request.Headers[HeaderNames.IfModifiedSince] = HeaderUtilities.FormatDate(utcNow);
context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10));
context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = EntityTagHeaderValue.Any.ToString(); context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = EntityTagHeaderValue.Any.ToString();
@ -241,15 +243,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
[Fact] [Fact]
public void ContentIsNotModified_IfNoneMatch_Overrides_IfUnmodifiedSince_ToFalse() public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToFalse()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var sink = new TestSink(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink); var context = TestUtils.CreateTestContext(sink);
context.CachedResponseHeaders = new HeaderDictionary(); context.CachedResponseHeaders = new HeaderDictionary();
// This would pass the IfUnmodifiedSince checks // This would pass the IfModifiedSince checks
context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); context.HttpContext.Request.Headers[HeaderNames.IfModifiedSince] = HeaderUtilities.FormatDate(utcNow);
context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10));
context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "\"E1\""; context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "\"E1\"";
@ -328,27 +330,56 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() public async Task OnResponseStartingAsync_IfAllowResponseCaptureIsTrue_SetsResponseTime()
{ {
var sink = new TestSink(); var clock = new TestClock
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider()); {
UtcNow = DateTimeOffset.UtcNow
};
var middleware = TestUtils.CreateTestMiddleware(options: new ResponseCachingOptions
{
SystemClock = clock
});
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.ResponseTime = null;
Assert.False(context.ShouldCacheResponse); await middleware.OnResponseStartingAsync(context);
middleware.ShimResponseStream(context); Assert.Equal(clock.UtcNow, context.ResponseTime);
await middleware.FinalizeCacheHeadersAsync(context);
Assert.False(context.ShouldCacheResponse);
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable() public async Task OnResponseStartingAsync_IfAllowResponseCaptureIsTrue_SetsResponseTimeOnlyOnce()
{
var clock = new TestClock
{
UtcNow = DateTimeOffset.UtcNow
};
var middleware = TestUtils.CreateTestMiddleware(options: new ResponseCachingOptions
{
SystemClock = clock
});
var context = TestUtils.CreateTestContext();
var initialTime = clock.UtcNow;
context.ResponseTime = null;
await middleware.OnResponseStartingAsync(context);
Assert.Equal(initialTime, context.ResponseTime);
clock.UtcNow += TimeSpan.FromSeconds(10);
await middleware.OnResponseStartingAsync(context);
Assert.NotEqual(clock.UtcNow, context.ResponseTime);
Assert.Equal(initialTime, context.ResponseTime);
}
[Fact]
public async Task FinalizeCacheHeadersAsync_UpdateShouldCacheResponse_IfResponseCacheable()
{ {
var sink = new TestSink(); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider()); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider());
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
{ {
Public = true Public = true
@ -363,7 +394,22 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_DefaultResponseValidity_Is10Seconds() public async Task FinalizeCacheHeadersAsync_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable()
{
var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider());
var context = TestUtils.CreateTestContext();
middleware.ShimResponseStream(context);
await middleware.FinalizeCacheHeadersAsync(context);
Assert.False(context.ShouldCacheResponse);
Assert.Empty(sink.Writes);
}
[Fact]
public async Task FinalizeCacheHeadersAsync_DefaultResponseValidity_Is10Seconds()
{ {
var sink = new TestSink(); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var middleware = TestUtils.CreateTestMiddleware(testSink: sink);
@ -376,15 +422,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_ResponseValidity_UseExpiryIfAvailable() public async Task FinalizeCacheHeadersAsync_ResponseValidity_UseExpiryIfAvailable()
{ {
var utcNow = DateTimeOffset.MinValue; var clock = new TestClock
{
UtcNow = DateTimeOffset.MinValue
};
var sink = new TestSink(); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions
{
SystemClock = clock
});
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.ResponseTime = utcNow; context.ResponseTime = clock.UtcNow;
context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(11)); context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11));
await middleware.FinalizeCacheHeadersAsync(context); await middleware.FinalizeCacheHeadersAsync(context);
@ -393,17 +445,26 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_ResponseValidity_UseMaxAgeIfAvailable() public async Task FinalizeCacheHeadersAsync_ResponseValidity_UseMaxAgeIfAvailable()
{ {
var clock = new TestClock
{
UtcNow = DateTimeOffset.UtcNow
};
var sink = new TestSink(); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions
{
SystemClock = clock
});
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.ResponseTime = clock.UtcNow;
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
{ {
MaxAge = TimeSpan.FromSeconds(12) MaxAge = TimeSpan.FromSeconds(12)
}.ToString(); }.ToString();
context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(context.ResponseTime.Value + TimeSpan.FromSeconds(11)); context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11));
await middleware.FinalizeCacheHeadersAsync(context); await middleware.FinalizeCacheHeadersAsync(context);
@ -412,18 +473,26 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_ResponseValidity_UseSharedMaxAgeIfAvailable() public async Task FinalizeCacheHeadersAsync_ResponseValidity_UseSharedMaxAgeIfAvailable()
{ {
var clock = new TestClock
{
UtcNow = DateTimeOffset.UtcNow
};
var sink = new TestSink(); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions
{
SystemClock = clock
});
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.ResponseTime = clock.UtcNow;
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
{ {
MaxAge = TimeSpan.FromSeconds(12), MaxAge = TimeSpan.FromSeconds(12),
SharedMaxAge = TimeSpan.FromSeconds(13) SharedMaxAge = TimeSpan.FromSeconds(13)
}.ToString(); }.ToString();
context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11));
context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(context.ResponseTime.Value + TimeSpan.FromSeconds(11));
await middleware.FinalizeCacheHeadersAsync(context); await middleware.FinalizeCacheHeadersAsync(context);
@ -432,7 +501,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_IfNotEquivalentToPrevious() public async Task FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_IfNotEquivalentToPrevious()
{ {
var cache = new TestResponseCache(); var cache = new TestResponseCache();
var sink = new TestSink(); var sink = new TestSink();
@ -451,19 +520,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}; };
context.CachedVaryByRules = cachedVaryByRules; context.CachedVaryByRules = cachedVaryByRules;
await middleware.TryServeFromCacheAsync(context);
await middleware.FinalizeCacheHeadersAsync(context); await middleware.FinalizeCacheHeadersAsync(context);
Assert.Equal(1, cache.SetCount); Assert.Equal(1, cache.SetCount);
Assert.NotSame(cachedVaryByRules, context.CachedVaryByRules); Assert.NotSame(cachedVaryByRules, context.CachedVaryByRules);
TestUtils.AssertLoggedMessages( TestUtils.AssertLoggedMessages(
sink.Writes, sink.Writes,
LoggedMessage.NoResponseServed,
LoggedMessage.VaryByRulesUpdated); LoggedMessage.VaryByRulesUpdated);
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_IfEquivalentToPrevious() public async Task FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_IfEquivalentToPrevious()
{ {
var cache = new TestResponseCache(); var cache = new TestResponseCache();
var sink = new TestSink(); var sink = new TestSink();
@ -483,7 +550,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}; };
context.CachedVaryByRules = cachedVaryByRules; context.CachedVaryByRules = cachedVaryByRules;
await middleware.TryServeFromCacheAsync(context);
await middleware.FinalizeCacheHeadersAsync(context); await middleware.FinalizeCacheHeadersAsync(context);
// An update to the cache is always made but the entry should be the same // An update to the cache is always made but the entry should be the same
@ -491,7 +557,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Assert.Same(cachedVaryByRules, context.CachedVaryByRules); Assert.Same(cachedVaryByRules, context.CachedVaryByRules);
TestUtils.AssertLoggedMessages( TestUtils.AssertLoggedMessages(
sink.Writes, sink.Writes,
LoggedMessage.NoResponseServed,
LoggedMessage.VaryByRulesUpdated); LoggedMessage.VaryByRulesUpdated);
} }
@ -515,7 +580,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Theory] [Theory]
[MemberData(nameof(NullOrEmptyVaryRules))] [MemberData(nameof(NullOrEmptyVaryRules))]
public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_NullOrEmptyRules(StringValues vary) public async Task FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_NullOrEmptyRules(StringValues vary)
{ {
var cache = new TestResponseCache(); var cache = new TestResponseCache();
var sink = new TestSink(); var sink = new TestSink();
@ -528,40 +593,43 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
VaryByQueryKeys = vary VaryByQueryKeys = vary
}); });
await middleware.TryServeFromCacheAsync(context);
await middleware.FinalizeCacheHeadersAsync(context); await middleware.FinalizeCacheHeadersAsync(context);
// Vary rules should not be updated // Vary rules should not be updated
Assert.Equal(0, cache.SetCount); Assert.Equal(0, cache.SetCount);
TestUtils.AssertLoggedMessages( Assert.Empty(sink.Writes);
sink.Writes,
LoggedMessage.NoResponseServed);
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_DoNotAddDate_IfSpecified() public async Task FinalizeCacheHeadersAsync_AddsDate_IfNoneSpecified()
{ {
var utcNow = DateTimeOffset.MinValue; var clock = new TestClock
{
UtcNow = DateTimeOffset.UtcNow
};
var sink = new TestSink(); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions
{
SystemClock = clock
});
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.ResponseTime = utcNow;
Assert.True(StringValues.IsNullOrEmpty(context.HttpContext.Response.Headers[HeaderNames.Date])); Assert.True(StringValues.IsNullOrEmpty(context.HttpContext.Response.Headers[HeaderNames.Date]));
await middleware.FinalizeCacheHeadersAsync(context); await middleware.FinalizeCacheHeadersAsync(context);
Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers[HeaderNames.Date]); Assert.Equal(HeaderUtilities.FormatDate(clock.UtcNow), context.HttpContext.Response.Headers[HeaderNames.Date]);
Assert.Empty(sink.Writes); Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_AddsDate_IfNoneSpecified() public async Task FinalizeCacheHeadersAsync_DoNotAddDate_IfSpecified()
{ {
var utcNow = DateTimeOffset.MinValue; var utcNow = DateTimeOffset.MinValue;
var sink = new TestSink(); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var middleware = TestUtils.CreateTestMiddleware(testSink: sink);
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow);
context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); context.ResponseTime = utcNow + TimeSpan.FromSeconds(10);
@ -574,7 +642,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_StoresCachedResponse_InState() public async Task FinalizeCacheHeadersAsync_StoresCachedResponse_InState()
{ {
var sink = new TestSink(); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var middleware = TestUtils.CreateTestMiddleware(testSink: sink);
@ -589,20 +657,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_SplitsVaryHeaderByCommas() public async Task FinalizeCacheHeadersAsync_SplitsVaryHeaderByCommas()
{ {
var sink = new TestSink(); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var middleware = TestUtils.CreateTestMiddleware(testSink: sink);
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.HttpContext.Response.Headers[HeaderNames.Vary] = "HeaderB, heaDera"; context.HttpContext.Response.Headers[HeaderNames.Vary] = "HeaderB, heaDera";
await middleware.TryServeFromCacheAsync(context);
await middleware.FinalizeCacheHeadersAsync(context); await middleware.FinalizeCacheHeadersAsync(context);
Assert.Equal(new StringValues(new[] { "HEADERA", "HEADERB" }), context.CachedVaryByRules.Headers); Assert.Equal(new StringValues(new[] { "HEADERA", "HEADERB" }), context.CachedVaryByRules.Headers);
TestUtils.AssertLoggedMessages( TestUtils.AssertLoggedMessages(
sink.Writes, sink.Writes,
LoggedMessage.NoResponseServed,
LoggedMessage.VaryByRulesUpdated); LoggedMessage.VaryByRulesUpdated);
} }
@ -614,11 +681,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.ShouldCacheResponse = true;
middleware.ShimResponseStream(context); middleware.ShimResponseStream(context);
context.HttpContext.Response.ContentLength = 20; context.HttpContext.Response.ContentLength = 20;
await context.HttpContext.Response.WriteAsync(new string('0', 20)); await context.HttpContext.Response.WriteAsync(new string('0', 20));
context.ShouldCacheResponse = true;
context.CachedResponse = new CachedResponse(); context.CachedResponse = new CachedResponse();
context.BaseKey = "BaseKey"; context.BaseKey = "BaseKey";
context.CachedResponseValidFor = TimeSpan.FromSeconds(10); context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
@ -639,11 +707,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.ShouldCacheResponse = true;
middleware.ShimResponseStream(context); middleware.ShimResponseStream(context);
context.HttpContext.Response.ContentLength = 9; context.HttpContext.Response.ContentLength = 9;
await context.HttpContext.Response.WriteAsync(new string('0', 10)); await context.HttpContext.Response.WriteAsync(new string('0', 10));
context.ShouldCacheResponse = true;
context.CachedResponse = new CachedResponse(); context.CachedResponse = new CachedResponse();
context.BaseKey = "BaseKey"; context.BaseKey = "BaseKey";
context.CachedResponseValidFor = TimeSpan.FromSeconds(10); context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
@ -664,10 +733,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.ShouldCacheResponse = true;
middleware.ShimResponseStream(context); middleware.ShimResponseStream(context);
await context.HttpContext.Response.WriteAsync(new string('0', 10)); await context.HttpContext.Response.WriteAsync(new string('0', 10));
context.ShouldCacheResponse = true;
context.CachedResponse = new CachedResponse() context.CachedResponse = new CachedResponse()
{ {
Headers = new HeaderDictionary() Headers = new HeaderDictionary()
@ -691,10 +761,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.ShouldCacheResponse = false;
middleware.ShimResponseStream(context); middleware.ShimResponseStream(context);
await context.HttpContext.Response.WriteAsync(new string('0', 10)); await context.HttpContext.Response.WriteAsync(new string('0', 10));
context.ShouldCacheResponse = false;
await middleware.FinalizeCacheBodyAsync(context); await middleware.FinalizeCacheBodyAsync(context);
@ -712,10 +782,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.ShouldCacheResponse = true;
middleware.ShimResponseStream(context); middleware.ShimResponseStream(context);
await context.HttpContext.Response.WriteAsync(new string('0', 10)); await context.HttpContext.Response.WriteAsync(new string('0', 10));
context.ShouldCacheResponse = true;
context.ResponseCachingStream.DisableBuffering(); context.ResponseCachingStream.DisableBuffering();
await middleware.FinalizeCacheBodyAsync(context); await middleware.FinalizeCacheBodyAsync(context);
@ -727,16 +797,52 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
[Fact] [Fact]
public void ShimResponseStream_SecondInvocation_Throws() public void AddResponseCachingFeature_SecondInvocation_Throws()
{ {
var middleware = TestUtils.CreateTestMiddleware(); var httpContext = new DefaultHttpContext();
var context = TestUtils.CreateTestContext();
// Should not throw // Should not throw
middleware.ShimResponseStream(context); ResponseCachingMiddleware.AddResponseCachingFeature(httpContext);
// Should throw // Should throw
Assert.ThrowsAny<InvalidOperationException>(() => middleware.ShimResponseStream(context)); Assert.ThrowsAny<InvalidOperationException>(() => ResponseCachingMiddleware.AddResponseCachingFeature(httpContext));
}
private class FakeResponseFeature : HttpResponseFeature
{
public override void OnStarting(Func<object, Task> callback, object state) { }
}
[Theory]
// If allowResponseCaching is false, other settings will not matter but are included for completeness
[InlineData(false, false, false)]
[InlineData(false, false, true)]
[InlineData(false, true, false)]
[InlineData(false, true, true)]
[InlineData(true, false, false)]
[InlineData(true, false, true)]
[InlineData(true, true, false)]
[InlineData(true, true, true)]
public async Task Invoke_AddsResponseCachingFeature_Always(bool allowResponseCaching, bool allowCacheLookup, bool allowCacheStorage)
{
var responseCachingFeatureAdded = false;
var middleware = TestUtils.CreateTestMiddleware(next: httpContext =>
{
responseCachingFeatureAdded = httpContext.Features.Get<IResponseCachingFeature>() != null;
return TaskCache.CompletedTask;
},
policyProvider: new TestResponseCachingPolicyProvider
{
AttemptResponseCachingValue = allowResponseCaching,
AllowCacheLookupValue = allowCacheLookup,
AllowCacheStorageValue = allowCacheStorage
});
var context = new DefaultHttpContext();
context.Features.Set<IHttpResponseFeature>(new FakeResponseFeature());
await middleware.Invoke(context);
Assert.True(responseCachingFeatureAdded);
} }
[Fact] [Fact]

View File

@ -3,7 +3,6 @@
using System; using System;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Logging.Testing;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
@ -27,13 +26,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Theory] [Theory]
[MemberData(nameof(CacheableMethods))] [MemberData(nameof(CacheableMethods))]
public void IsRequestCacheable_CacheableMethods_Allowed(string method) public void AttemptResponseCaching_CacheableMethods_Allowed(string method)
{ {
var sink = new TestSink(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink); var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Request.Method = method; context.HttpContext.Request.Method = method;
Assert.True(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); Assert.True(new ResponseCachingPolicyProvider().AttemptResponseCaching(context));
Assert.Empty(sink.Writes); Assert.Empty(sink.Writes);
} }
public static TheoryData<string> NonCacheableMethods public static TheoryData<string> NonCacheableMethods
@ -56,51 +55,34 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Theory] [Theory]
[MemberData(nameof(NonCacheableMethods))] [MemberData(nameof(NonCacheableMethods))]
public void IsRequestCacheable_UncacheableMethods_NotAllowed(string method) public void AttemptResponseCaching_UncacheableMethods_NotAllowed(string method)
{ {
var sink = new TestSink(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink); var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Request.Method = method; context.HttpContext.Request.Method = method;
Assert.False(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); Assert.False(new ResponseCachingPolicyProvider().AttemptResponseCaching(context));
TestUtils.AssertLoggedMessages( TestUtils.AssertLoggedMessages(
sink.Writes, sink.Writes,
LoggedMessage.RequestMethodNotCacheable); LoggedMessage.RequestMethodNotCacheable);
} }
[Fact] [Fact]
public void IsRequestCacheable_AuthorizationHeaders_NotAllowed() public void AttemptResponseCaching_AuthorizationHeaders_NotAllowed()
{ {
var sink = new TestSink(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink); var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Method = HttpMethods.Get;
context.HttpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW"; context.HttpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW";
Assert.False(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); Assert.False(new ResponseCachingPolicyProvider().AttemptResponseCaching(context));
TestUtils.AssertLoggedMessages( TestUtils.AssertLoggedMessages(
sink.Writes, sink.Writes,
LoggedMessage.RequestWithAuthorizationNotCacheable); LoggedMessage.RequestWithAuthorizationNotCacheable);
} }
[Fact] [Fact]
public void IsRequestCacheable_NoCache_NotAllowed() public void AllowCacheStorage_NoStore_Allowed()
{
var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Request.Method = HttpMethods.Get;
context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
{
NoCache = true
}.ToString();
Assert.False(new ResponseCachingPolicyProvider().IsRequestCacheable(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.RequestWithNoCacheNotCacheable);
}
[Fact]
public void IsRequestCacheable_NoStore_Allowed()
{ {
var sink = new TestSink(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink); var context = TestUtils.CreateTestContext(sink);
@ -110,26 +92,43 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
NoStore = true NoStore = true
}.ToString(); }.ToString();
Assert.True(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); Assert.True(new ResponseCachingPolicyProvider().AllowCacheLookup(context));
Assert.Empty(sink.Writes); Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public void IsRequestCacheable_LegacyDirectives_NotAllowed() public void AllowCacheLookup_NoCache_NotAllowed()
{
var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Request.Method = HttpMethods.Get;
context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
{
NoCache = true
}.ToString();
Assert.False(new ResponseCachingPolicyProvider().AllowCacheLookup(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.RequestWithNoCacheNotCacheable);
}
[Fact]
public void AllowCacheLookup_LegacyDirectives_NotAllowed()
{ {
var sink = new TestSink(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink); var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Method = HttpMethods.Get;
context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache";
Assert.False(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); Assert.False(new ResponseCachingPolicyProvider().AllowCacheLookup(context));
TestUtils.AssertLoggedMessages( TestUtils.AssertLoggedMessages(
sink.Writes, sink.Writes,
LoggedMessage.RequestWithPragmaNoCacheNotCacheable); LoggedMessage.RequestWithPragmaNoCacheNotCacheable);
} }
[Fact] [Fact]
public void IsRequestCacheable_LegacyDirectives_OverridenByCacheControl() public void AllowCacheLookup_LegacyDirectives_OverridenByCacheControl()
{ {
var sink = new TestSink(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink); var context = TestUtils.CreateTestContext(sink);
@ -137,7 +136,22 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache";
context.HttpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10"; context.HttpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10";
Assert.True(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); Assert.True(new ResponseCachingPolicyProvider().AllowCacheLookup(context));
Assert.Empty(sink.Writes);
}
[Fact]
public void AllowCacheStorage_NoStore_NotAllowed()
{
var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Request.Method = HttpMethods.Get;
context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
{
NoStore = true
}.ToString();
Assert.False(new ResponseCachingPolicyProvider().AllowCacheStorage(context));
Assert.Empty(sink.Writes); Assert.Empty(sink.Writes);
} }
@ -184,26 +198,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
LoggedMessage.ResponseWithNoCacheNotCacheable); LoggedMessage.ResponseWithNoCacheNotCacheable);
} }
[Fact]
public void IsResponseCacheable_RequestNoStore_NotAllowed()
{
var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
{
NoStore = true
}.ToString();
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
{
Public = true
}.ToString();
Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ResponseWithNoStoreNotCacheable);
}
[Fact] [Fact]
public void IsResponseCacheable_ResponseNoStore_NotAllowed() public void IsResponseCacheable_ResponseNoStore_NotAllowed()
{ {
@ -289,6 +283,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
[Theory] [Theory]
[InlineData(StatusCodes.Status100Continue)]
[InlineData(StatusCodes.Status101SwitchingProtocols)]
[InlineData(StatusCodes.Status102Processing)]
[InlineData(StatusCodes.Status201Created)] [InlineData(StatusCodes.Status201Created)]
[InlineData(StatusCodes.Status202Accepted)] [InlineData(StatusCodes.Status202Accepted)]
[InlineData(StatusCodes.Status203NonAuthoritative)] [InlineData(StatusCodes.Status203NonAuthoritative)]
@ -296,6 +293,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[InlineData(StatusCodes.Status205ResetContent)] [InlineData(StatusCodes.Status205ResetContent)]
[InlineData(StatusCodes.Status206PartialContent)] [InlineData(StatusCodes.Status206PartialContent)]
[InlineData(StatusCodes.Status207MultiStatus)] [InlineData(StatusCodes.Status207MultiStatus)]
[InlineData(StatusCodes.Status208AlreadyReported)]
[InlineData(StatusCodes.Status226IMUsed)]
[InlineData(StatusCodes.Status300MultipleChoices)] [InlineData(StatusCodes.Status300MultipleChoices)]
[InlineData(StatusCodes.Status301MovedPermanently)] [InlineData(StatusCodes.Status301MovedPermanently)]
[InlineData(StatusCodes.Status302Found)] [InlineData(StatusCodes.Status302Found)]
@ -325,9 +324,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[InlineData(StatusCodes.Status417ExpectationFailed)] [InlineData(StatusCodes.Status417ExpectationFailed)]
[InlineData(StatusCodes.Status418ImATeapot)] [InlineData(StatusCodes.Status418ImATeapot)]
[InlineData(StatusCodes.Status419AuthenticationTimeout)] [InlineData(StatusCodes.Status419AuthenticationTimeout)]
[InlineData(StatusCodes.Status421MisdirectedRequest)]
[InlineData(StatusCodes.Status422UnprocessableEntity)] [InlineData(StatusCodes.Status422UnprocessableEntity)]
[InlineData(StatusCodes.Status423Locked)] [InlineData(StatusCodes.Status423Locked)]
[InlineData(StatusCodes.Status424FailedDependency)] [InlineData(StatusCodes.Status424FailedDependency)]
[InlineData(StatusCodes.Status426UpgradeRequired)]
[InlineData(StatusCodes.Status428PreconditionRequired)]
[InlineData(StatusCodes.Status429TooManyRequests)]
[InlineData(StatusCodes.Status431RequestHeaderFieldsTooLarge)]
[InlineData(StatusCodes.Status451UnavailableForLegalReasons)] [InlineData(StatusCodes.Status451UnavailableForLegalReasons)]
[InlineData(StatusCodes.Status500InternalServerError)] [InlineData(StatusCodes.Status500InternalServerError)]
[InlineData(StatusCodes.Status501NotImplemented)] [InlineData(StatusCodes.Status501NotImplemented)]
@ -337,6 +341,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[InlineData(StatusCodes.Status505HttpVersionNotsupported)] [InlineData(StatusCodes.Status505HttpVersionNotsupported)]
[InlineData(StatusCodes.Status506VariantAlsoNegotiates)] [InlineData(StatusCodes.Status506VariantAlsoNegotiates)]
[InlineData(StatusCodes.Status507InsufficientStorage)] [InlineData(StatusCodes.Status507InsufficientStorage)]
[InlineData(StatusCodes.Status508LoopDetected)]
[InlineData(StatusCodes.Status510NotExtended)]
[InlineData(StatusCodes.Status511NetworkAuthenticationRequired)]
public void IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode) public void IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode)
{ {
var sink = new TestSink(); var sink = new TestSink();
@ -687,6 +694,29 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
LoggedMessage.ExpirationMaxStaleSatisfied); LoggedMessage.ExpirationMaxStaleSatisfied);
} }
[Fact]
public void IsCachedEntryFresh_MaxStaleInfiniteOverridesFreshness_ToFresh()
{
var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(5),
MaxStale = true // No value specified means a MaxStaleLimit of infinity
}.ToString();
context.CachedResponseHeaders = new HeaderDictionary();
context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(5),
}.ToString();
context.CachedEntryAge = TimeSpan.FromSeconds(6);
Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ExpirationInfiniteMaxStaleSatisfied);
}
[Fact] [Fact]
public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ButStillNotFresh() public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ButStillNotFresh()
{ {
@ -735,5 +765,30 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
sink.Writes, sink.Writes,
LoggedMessage.ExpirationMustRevalidate); LoggedMessage.ExpirationMustRevalidate);
} }
[Fact]
public void IsCachedEntryFresh_ProxyRevalidateOverridesRequestMaxStale_ToNotFresh()
{
var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(5),
MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit
MaxStaleLimit = TimeSpan.FromSeconds(2)
}.ToString();
context.CachedResponseHeaders = new HeaderDictionary();
context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(5),
MustRevalidate = true
}.ToString();
context.CachedEntryAge = TimeSpan.FromSeconds(6);
Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ExpirationMustRevalidate);
}
} }
} }

View File

@ -123,10 +123,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
{ {
var client = server.CreateClient(); var client = server.CreateClient();
client.DefaultRequestHeaders.CacheControl =
new System.Net.Http.Headers.CacheControlHeaderValue { NoCache = true };
var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, ""));
// verify the response is cached
var cachedResponse = await client.SendAsync(TestUtils.CreateRequest(method, ""));
await AssertCachedResponseAsync(initialResponse, cachedResponse);
// assert cached response no longer served
client.DefaultRequestHeaders.CacheControl =
new System.Net.Http.Headers.CacheControlHeaderValue { NoCache = true };
var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, ""));
await AssertFreshResponseAsync(initialResponse, subsequentResponse); await AssertFreshResponseAsync(initialResponse, subsequentResponse);
@ -146,10 +152,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
using (var server = new TestServer(builder)) using (var server = new TestServer(builder))
{ {
var client = server.CreateClient(); var client = server.CreateClient();
client.DefaultRequestHeaders.Pragma.Clear();
client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("no-cache"));
var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, ""));
// verify the response is cached
var cachedResponse = await client.SendAsync(TestUtils.CreateRequest(method, ""));
await AssertCachedResponseAsync(initialResponse, cachedResponse);
// assert cached response no longer served
client.DefaultRequestHeaders.Pragma.Clear();
client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("no-cache"));
var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, ""));
await AssertFreshResponseAsync(initialResponse, subsequentResponse); await AssertFreshResponseAsync(initialResponse, subsequentResponse);
@ -565,7 +577,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
[Fact] [Fact]
public async void ServesCachedContent_IfSubsequentRequest_ContainsNoStore() public async void ServesCachedContent_IfSubsequentRequestContainsNoStore()
{ {
var builders = TestUtils.CreateBuildersWithResponseCaching(); var builders = TestUtils.CreateBuildersWithResponseCaching();
@ -587,7 +599,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
[Fact] [Fact]
public async void ServesFreshContent_IfInitialRequestContains_NoStore() public async void ServesFreshContent_IfInitialRequestContainsNoStore()
{ {
var builders = TestUtils.CreateBuildersWithResponseCaching(); var builders = TestUtils.CreateBuildersWithResponseCaching();
@ -608,6 +620,31 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
} }
[Fact]
public async void ServesFreshContent_IfInitialResponseContainsNoStore()
{
var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) =>
{
var headers = context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
NoStore = true
};
await TestUtils.TestRequestDelegate(context);
});
foreach (var builder in builders)
{
using (var server = new TestServer(builder))
{
var client = server.CreateClient();
var initialResponse = await client.GetAsync("");
var subsequentResponse = await client.GetAsync("");
await AssertFreshResponseAsync(initialResponse, subsequentResponse);
}
}
}
[Fact] [Fact]
public async void Serves304_IfIfModifiedSince_Satisfied() public async void Serves304_IfIfModifiedSince_Satisfied()
{ {
@ -619,7 +656,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{ {
var client = server.CreateClient(); var client = server.CreateClient();
var initialResponse = await client.GetAsync(""); var initialResponse = await client.GetAsync("");
client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MaxValue; client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.MaxValue;
var subsequentResponse = await client.GetAsync(""); var subsequentResponse = await client.GetAsync("");
initialResponse.EnsureSuccessStatusCode(); initialResponse.EnsureSuccessStatusCode();
@ -639,7 +676,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{ {
var client = server.CreateClient(); var client = server.CreateClient();
var initialResponse = await client.GetAsync(""); var initialResponse = await client.GetAsync("");
client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue; client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
var subsequentResponse = await client.GetAsync(""); var subsequentResponse = await client.GetAsync("");
await AssertCachedResponseAsync(initialResponse, subsequentResponse); await AssertCachedResponseAsync(initialResponse, subsequentResponse);

View File

@ -20,6 +20,7 @@ using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Xunit; using Xunit;
using ISystemClock = Microsoft.AspNetCore.ResponseCaching.Internal.ISystemClock;
namespace Microsoft.AspNetCore.ResponseCaching.Tests namespace Microsoft.AspNetCore.ResponseCaching.Tests
{ {
@ -42,11 +43,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
var uniqueId = Guid.NewGuid().ToString(); var uniqueId = Guid.NewGuid().ToString();
headers.CacheControl = new CacheControlHeaderValue if (headers.CacheControl == null)
{ {
Public = true, headers.CacheControl = new CacheControlHeaderValue
MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null {
}; Public = true,
MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null
};
}
else
{
headers.CacheControl.Public = true;
headers.CacheControl.MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null;
}
headers.Date = DateTimeOffset.UtcNow; headers.Date = DateTimeOffset.UtcNow;
headers.Headers["X-Value"] = uniqueId; headers.Headers["X-Value"] = uniqueId;
@ -103,12 +112,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
internal static ResponseCachingMiddleware CreateTestMiddleware( internal static ResponseCachingMiddleware CreateTestMiddleware(
RequestDelegate next = null,
IResponseCache cache = null, IResponseCache cache = null,
ResponseCachingOptions options = null, ResponseCachingOptions options = null,
TestSink testSink = null, TestSink testSink = null,
IResponseCachingKeyProvider keyProvider = null, IResponseCachingKeyProvider keyProvider = null,
IResponseCachingPolicyProvider policyProvider = null) IResponseCachingPolicyProvider policyProvider = null)
{ {
if (next == null)
{
next = httpContext => TaskCache.CompletedTask;
}
if (cache == null) if (cache == null)
{ {
cache = new TestResponseCache(); cache = new TestResponseCache();
@ -127,7 +141,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
} }
return new ResponseCachingMiddleware( return new ResponseCachingMiddleware(
httpContext => TaskCache.CompletedTask, next,
Options.Create(options), Options.Create(options),
testSink == null ? (ILoggerFactory)NullLoggerFactory.Instance : new TestLoggerFactory(testSink, true), testSink == null ? (ILoggerFactory)NullLoggerFactory.Instance : new TestLoggerFactory(testSink, true),
policyProvider, policyProvider,
@ -188,7 +202,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
internal static LoggedMessage ResponseWithUnsuccessfulStatusCodeNotCacheable => new LoggedMessage(17, LogLevel.Debug); internal static LoggedMessage ResponseWithUnsuccessfulStatusCodeNotCacheable => new LoggedMessage(17, LogLevel.Debug);
internal static LoggedMessage NotModifiedIfNoneMatchStar => new LoggedMessage(18, LogLevel.Debug); internal static LoggedMessage NotModifiedIfNoneMatchStar => new LoggedMessage(18, LogLevel.Debug);
internal static LoggedMessage NotModifiedIfNoneMatchMatched => new LoggedMessage(19, LogLevel.Debug); internal static LoggedMessage NotModifiedIfNoneMatchMatched => new LoggedMessage(19, LogLevel.Debug);
internal static LoggedMessage NotModifiedIfUnmodifiedSinceSatisfied => new LoggedMessage(20, LogLevel.Debug); internal static LoggedMessage NotModifiedIfModifiedSinceSatisfied => new LoggedMessage(20, LogLevel.Debug);
internal static LoggedMessage NotModifiedServed => new LoggedMessage(21, LogLevel.Information); internal static LoggedMessage NotModifiedServed => new LoggedMessage(21, LogLevel.Information);
internal static LoggedMessage CachedResponseServed => new LoggedMessage(22, LogLevel.Information); internal static LoggedMessage CachedResponseServed => new LoggedMessage(22, LogLevel.Information);
internal static LoggedMessage GatewayTimeoutServed => new LoggedMessage(23, LogLevel.Information); internal static LoggedMessage GatewayTimeoutServed => new LoggedMessage(23, LogLevel.Information);
@ -197,6 +211,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
internal static LoggedMessage ResponseCached => new LoggedMessage(26, LogLevel.Information); internal static LoggedMessage ResponseCached => new LoggedMessage(26, LogLevel.Information);
internal static LoggedMessage ResponseNotCached => new LoggedMessage(27, LogLevel.Information); internal static LoggedMessage ResponseNotCached => new LoggedMessage(27, LogLevel.Information);
internal static LoggedMessage ResponseContentLengthMismatchNotCached => new LoggedMessage(28, LogLevel.Warning); internal static LoggedMessage ResponseContentLengthMismatchNotCached => new LoggedMessage(28, LogLevel.Warning);
internal static LoggedMessage ExpirationInfiniteMaxStaleSatisfied => new LoggedMessage(29, LogLevel.Debug);
private LoggedMessage(int evenId, LogLevel logLevel) private LoggedMessage(int evenId, LogLevel logLevel)
{ {
@ -218,11 +233,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
internal class TestResponseCachingPolicyProvider : IResponseCachingPolicyProvider internal class TestResponseCachingPolicyProvider : IResponseCachingPolicyProvider
{ {
public bool IsCachedEntryFresh(ResponseCachingContext context) => true; public bool AllowCacheLookupValue { get; set; } = false;
public bool AllowCacheStorageValue { get; set; } = false;
public bool AttemptResponseCachingValue { get; set; } = false;
public bool IsCachedEntryFreshValue { get; set; } = true;
public bool IsResponseCacheableValue { get; set; } = true;
public bool IsRequestCacheable(ResponseCachingContext context) => true; public bool AllowCacheLookup(ResponseCachingContext context) => AllowCacheLookupValue;
public bool IsResponseCacheable(ResponseCachingContext context) => true; public bool AllowCacheStorage(ResponseCachingContext context) => AllowCacheStorageValue;
public bool AttemptResponseCaching(ResponseCachingContext context) => AttemptResponseCachingValue;
public bool IsCachedEntryFresh(ResponseCachingContext context) => IsCachedEntryFreshValue;
public bool IsResponseCacheable(ResponseCachingContext context) => IsResponseCacheableValue;
} }
internal class TestResponseCachingKeyProvider : IResponseCachingKeyProvider internal class TestResponseCachingKeyProvider : IResponseCachingKeyProvider
@ -284,4 +309,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
return TaskCache.CompletedTask; return TaskCache.CompletedTask;
} }
} }
internal class TestClock : ISystemClock
{
public DateTimeOffset UtcNow { get; set; }
}
} }