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
{
/// <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>
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
/// <returns><c>true</c> if the request is cacheable; otherwise <c>false</c>.</returns>
bool IsRequestCacheable(ResponseCachingContext context);
/// <returns><c>true</c> if response caching logic should be attempted; otherwise <c>false</c>.</returns>
bool AttemptResponseCaching(ResponseCachingContext context);
/// <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>
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
/// <returns><c>true</c> if the response is cacheable; otherwise <c>false</c>.</returns>
bool IsResponseCacheable(ResponseCachingContext context);
/// <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>
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
/// <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, Exception> _logNotModifiedIfNoneMatchStar;
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> _logCachedResponseServed;
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> _logResponseNotCached;
private static Action<ILogger, Exception> _logResponseContentLengthMismatchNotCached;
private static Action<ILogger, TimeSpan, TimeSpan, Exception> _logExpirationInfiniteMaxStaleSatisfied;
static LoggerExtensions()
{
@ -70,7 +71,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
_logExpirationMustRevalidate = LoggerMessage.Define<TimeSpan, TimeSpan>(
logLevel: LogLevel.Debug,
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>(
logLevel: LogLevel.Debug,
eventId: 8,
@ -119,10 +120,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
logLevel: LogLevel.Debug,
eventId: 19,
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,
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(
logLevel: LogLevel.Information,
eventId: 21,
@ -155,6 +156,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
logLevel: LogLevel.Warning,
eventId: 28,
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)
@ -252,9 +257,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
_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)
@ -296,5 +301,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
_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 bool ShouldCacheResponse { get; set; }
internal bool ShouldCacheResponse { get; set; }
internal string BaseKey { get; set; }

View File

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

View File

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

View File

@ -5,7 +5,9 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
@ -157,14 +159,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public void ContentIsNotModified_IfUnmodifiedSince_FallsbackToDateHeader()
public void ContentIsNotModified_IfModifiedSince_FallsbackToDateHeader()
{
var utcNow = DateTimeOffset.UtcNow;
var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
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
context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10));
@ -183,19 +185,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
// Verify logging
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied,
LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied);
LoggedMessage.NotModifiedIfModifiedSinceSatisfied,
LoggedMessage.NotModifiedIfModifiedSinceSatisfied);
}
[Fact]
public void ContentIsNotModified_IfUnmodifiedSince_LastModifiedOverridesDateHeader()
public void ContentIsNotModified_IfModifiedSince_LastModifiedOverridesDateHeader()
{
var utcNow = DateTimeOffset.UtcNow;
var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
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
context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10));
@ -217,20 +219,20 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
// Verify logging
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied,
LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied);
LoggedMessage.NotModifiedIfModifiedSinceSatisfied,
LoggedMessage.NotModifiedIfModifiedSinceSatisfied);
}
[Fact]
public void ContentIsNotModified_IfNoneMatch_Overrides_IfUnmodifiedSince_ToTrue()
public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToTrue()
{
var utcNow = DateTimeOffset.UtcNow;
var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.CachedResponseHeaders = new HeaderDictionary();
// This would fail the IfUnmodifiedSince checks
context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow);
// This would fail the IfModifiedSince checks
context.HttpContext.Request.Headers[HeaderNames.IfModifiedSince] = HeaderUtilities.FormatDate(utcNow);
context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10));
context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = EntityTagHeaderValue.Any.ToString();
@ -241,15 +243,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public void ContentIsNotModified_IfNoneMatch_Overrides_IfUnmodifiedSince_ToFalse()
public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToFalse()
{
var utcNow = DateTimeOffset.UtcNow;
var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.CachedResponseHeaders = new HeaderDictionary();
// This would pass the IfUnmodifiedSince checks
context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow);
// This would pass the IfModifiedSince checks
context.HttpContext.Request.Headers[HeaderNames.IfModifiedSince] = HeaderUtilities.FormatDate(utcNow);
context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10));
context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "\"E1\"";
@ -328,27 +330,56 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public async Task FinalizeCacheHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable()
public async Task OnResponseStartingAsync_IfAllowResponseCaptureIsTrue_SetsResponseTime()
{
var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider());
var clock = new TestClock
{
UtcNow = DateTimeOffset.UtcNow
};
var middleware = TestUtils.CreateTestMiddleware(options: new ResponseCachingOptions
{
SystemClock = clock
});
var context = TestUtils.CreateTestContext();
context.ResponseTime = null;
Assert.False(context.ShouldCacheResponse);
await middleware.OnResponseStartingAsync(context);
middleware.ShimResponseStream(context);
await middleware.FinalizeCacheHeadersAsync(context);
Assert.False(context.ShouldCacheResponse);
Assert.Empty(sink.Writes);
Assert.Equal(clock.UtcNow, context.ResponseTime);
}
[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 middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider());
var context = TestUtils.CreateTestContext();
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
{
Public = true
@ -363,7 +394,22 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[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 middleware = TestUtils.CreateTestMiddleware(testSink: sink);
@ -376,15 +422,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[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 middleware = TestUtils.CreateTestMiddleware(testSink: sink);
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions
{
SystemClock = clock
});
var context = TestUtils.CreateTestContext();
context.ResponseTime = utcNow;
context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(11));
context.ResponseTime = clock.UtcNow;
context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11));
await middleware.FinalizeCacheHeadersAsync(context);
@ -393,17 +445,26 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[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 middleware = TestUtils.CreateTestMiddleware(testSink: sink);
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions
{
SystemClock = clock
});
var context = TestUtils.CreateTestContext();
context.ResponseTime = clock.UtcNow;
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(12)
}.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);
@ -412,18 +473,26 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[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 middleware = TestUtils.CreateTestMiddleware(testSink: sink);
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions
{
SystemClock = clock
});
var context = TestUtils.CreateTestContext();
context.ResponseTime = clock.UtcNow;
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
{
MaxAge = TimeSpan.FromSeconds(12),
SharedMaxAge = TimeSpan.FromSeconds(13)
}.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);
@ -432,7 +501,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_IfNotEquivalentToPrevious()
public async Task FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_IfNotEquivalentToPrevious()
{
var cache = new TestResponseCache();
var sink = new TestSink();
@ -451,19 +520,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
};
context.CachedVaryByRules = cachedVaryByRules;
await middleware.TryServeFromCacheAsync(context);
await middleware.FinalizeCacheHeadersAsync(context);
Assert.Equal(1, cache.SetCount);
Assert.NotSame(cachedVaryByRules, context.CachedVaryByRules);
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.NoResponseServed,
LoggedMessage.VaryByRulesUpdated);
}
[Fact]
public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_IfEquivalentToPrevious()
public async Task FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_IfEquivalentToPrevious()
{
var cache = new TestResponseCache();
var sink = new TestSink();
@ -483,7 +550,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
};
context.CachedVaryByRules = cachedVaryByRules;
await middleware.TryServeFromCacheAsync(context);
await middleware.FinalizeCacheHeadersAsync(context);
// 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);
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.NoResponseServed,
LoggedMessage.VaryByRulesUpdated);
}
@ -515,7 +580,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Theory]
[MemberData(nameof(NullOrEmptyVaryRules))]
public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_NullOrEmptyRules(StringValues vary)
public async Task FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_NullOrEmptyRules(StringValues vary)
{
var cache = new TestResponseCache();
var sink = new TestSink();
@ -528,40 +593,43 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
VaryByQueryKeys = vary
});
await middleware.TryServeFromCacheAsync(context);
await middleware.FinalizeCacheHeadersAsync(context);
// Vary rules should not be updated
Assert.Equal(0, cache.SetCount);
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.NoResponseServed);
Assert.Empty(sink.Writes);
}
[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 middleware = TestUtils.CreateTestMiddleware(testSink: sink);
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions
{
SystemClock = clock
});
var context = TestUtils.CreateTestContext();
context.ResponseTime = utcNow;
Assert.True(StringValues.IsNullOrEmpty(context.HttpContext.Response.Headers[HeaderNames.Date]));
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);
}
[Fact]
public async Task FinalizeCacheHeaders_AddsDate_IfNoneSpecified()
public async Task FinalizeCacheHeadersAsync_DoNotAddDate_IfSpecified()
{
var utcNow = DateTimeOffset.MinValue;
var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink);
var context = TestUtils.CreateTestContext();
context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow);
context.ResponseTime = utcNow + TimeSpan.FromSeconds(10);
@ -574,7 +642,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public async Task FinalizeCacheHeaders_StoresCachedResponse_InState()
public async Task FinalizeCacheHeadersAsync_StoresCachedResponse_InState()
{
var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink);
@ -589,20 +657,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public async Task FinalizeCacheHeaders_SplitsVaryHeaderByCommas()
public async Task FinalizeCacheHeadersAsync_SplitsVaryHeaderByCommas()
{
var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink);
var context = TestUtils.CreateTestContext();
context.HttpContext.Response.Headers[HeaderNames.Vary] = "HeaderB, heaDera";
await middleware.TryServeFromCacheAsync(context);
await middleware.FinalizeCacheHeadersAsync(context);
Assert.Equal(new StringValues(new[] { "HEADERA", "HEADERB" }), context.CachedVaryByRules.Headers);
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.NoResponseServed,
LoggedMessage.VaryByRulesUpdated);
}
@ -614,11 +681,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
var context = TestUtils.CreateTestContext();
context.ShouldCacheResponse = true;
middleware.ShimResponseStream(context);
context.HttpContext.Response.ContentLength = 20;
await context.HttpContext.Response.WriteAsync(new string('0', 20));
context.ShouldCacheResponse = true;
context.CachedResponse = new CachedResponse();
context.BaseKey = "BaseKey";
context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
@ -639,11 +707,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
var context = TestUtils.CreateTestContext();
context.ShouldCacheResponse = true;
middleware.ShimResponseStream(context);
context.HttpContext.Response.ContentLength = 9;
await context.HttpContext.Response.WriteAsync(new string('0', 10));
context.ShouldCacheResponse = true;
context.CachedResponse = new CachedResponse();
context.BaseKey = "BaseKey";
context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
@ -664,10 +733,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
var context = TestUtils.CreateTestContext();
context.ShouldCacheResponse = true;
middleware.ShimResponseStream(context);
await context.HttpContext.Response.WriteAsync(new string('0', 10));
context.ShouldCacheResponse = true;
context.CachedResponse = new CachedResponse()
{
Headers = new HeaderDictionary()
@ -691,10 +761,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
var context = TestUtils.CreateTestContext();
context.ShouldCacheResponse = false;
middleware.ShimResponseStream(context);
await context.HttpContext.Response.WriteAsync(new string('0', 10));
context.ShouldCacheResponse = false;
await middleware.FinalizeCacheBodyAsync(context);
@ -712,10 +782,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
var context = TestUtils.CreateTestContext();
context.ShouldCacheResponse = true;
middleware.ShimResponseStream(context);
await context.HttpContext.Response.WriteAsync(new string('0', 10));
context.ShouldCacheResponse = true;
context.ResponseCachingStream.DisableBuffering();
await middleware.FinalizeCacheBodyAsync(context);
@ -727,16 +797,52 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public void ShimResponseStream_SecondInvocation_Throws()
public void AddResponseCachingFeature_SecondInvocation_Throws()
{
var middleware = TestUtils.CreateTestMiddleware();
var context = TestUtils.CreateTestContext();
var httpContext = new DefaultHttpContext();
// Should not throw
middleware.ShimResponseStream(context);
ResponseCachingMiddleware.AddResponseCachingFeature(httpContext);
// 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]

View File

@ -3,7 +3,6 @@
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Net.Http.Headers;
@ -27,13 +26,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Theory]
[MemberData(nameof(CacheableMethods))]
public void IsRequestCacheable_CacheableMethods_Allowed(string method)
public void AttemptResponseCaching_CacheableMethods_Allowed(string method)
{
var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Request.Method = method;
Assert.True(new ResponseCachingPolicyProvider().IsRequestCacheable(context));
Assert.True(new ResponseCachingPolicyProvider().AttemptResponseCaching(context));
Assert.Empty(sink.Writes);
}
public static TheoryData<string> NonCacheableMethods
@ -56,51 +55,34 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Theory]
[MemberData(nameof(NonCacheableMethods))]
public void IsRequestCacheable_UncacheableMethods_NotAllowed(string method)
public void AttemptResponseCaching_UncacheableMethods_NotAllowed(string method)
{
var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Request.Method = method;
Assert.False(new ResponseCachingPolicyProvider().IsRequestCacheable(context));
Assert.False(new ResponseCachingPolicyProvider().AttemptResponseCaching(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.RequestMethodNotCacheable);
}
[Fact]
public void IsRequestCacheable_AuthorizationHeaders_NotAllowed()
public void AttemptResponseCaching_AuthorizationHeaders_NotAllowed()
{
var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Request.Method = HttpMethods.Get;
context.HttpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW";
Assert.False(new ResponseCachingPolicyProvider().IsRequestCacheable(context));
Assert.False(new ResponseCachingPolicyProvider().AttemptResponseCaching(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.RequestWithAuthorizationNotCacheable);
}
[Fact]
public void IsRequestCacheable_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().IsRequestCacheable(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.RequestWithNoCacheNotCacheable);
}
[Fact]
public void IsRequestCacheable_NoStore_Allowed()
public void AllowCacheStorage_NoStore_Allowed()
{
var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
@ -110,26 +92,43 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
NoStore = true
}.ToString();
Assert.True(new ResponseCachingPolicyProvider().IsRequestCacheable(context));
Assert.True(new ResponseCachingPolicyProvider().AllowCacheLookup(context));
Assert.Empty(sink.Writes);
}
[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 context = TestUtils.CreateTestContext(sink);
context.HttpContext.Request.Method = HttpMethods.Get;
context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache";
Assert.False(new ResponseCachingPolicyProvider().IsRequestCacheable(context));
Assert.False(new ResponseCachingPolicyProvider().AllowCacheLookup(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.RequestWithPragmaNoCacheNotCacheable);
}
[Fact]
public void IsRequestCacheable_LegacyDirectives_OverridenByCacheControl()
public void AllowCacheLookup_LegacyDirectives_OverridenByCacheControl()
{
var sink = new TestSink();
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.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);
}
@ -184,26 +198,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
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]
public void IsResponseCacheable_ResponseNoStore_NotAllowed()
{
@ -289,6 +283,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Theory]
[InlineData(StatusCodes.Status100Continue)]
[InlineData(StatusCodes.Status101SwitchingProtocols)]
[InlineData(StatusCodes.Status102Processing)]
[InlineData(StatusCodes.Status201Created)]
[InlineData(StatusCodes.Status202Accepted)]
[InlineData(StatusCodes.Status203NonAuthoritative)]
@ -296,6 +293,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[InlineData(StatusCodes.Status205ResetContent)]
[InlineData(StatusCodes.Status206PartialContent)]
[InlineData(StatusCodes.Status207MultiStatus)]
[InlineData(StatusCodes.Status208AlreadyReported)]
[InlineData(StatusCodes.Status226IMUsed)]
[InlineData(StatusCodes.Status300MultipleChoices)]
[InlineData(StatusCodes.Status301MovedPermanently)]
[InlineData(StatusCodes.Status302Found)]
@ -325,9 +324,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[InlineData(StatusCodes.Status417ExpectationFailed)]
[InlineData(StatusCodes.Status418ImATeapot)]
[InlineData(StatusCodes.Status419AuthenticationTimeout)]
[InlineData(StatusCodes.Status421MisdirectedRequest)]
[InlineData(StatusCodes.Status422UnprocessableEntity)]
[InlineData(StatusCodes.Status423Locked)]
[InlineData(StatusCodes.Status424FailedDependency)]
[InlineData(StatusCodes.Status426UpgradeRequired)]
[InlineData(StatusCodes.Status428PreconditionRequired)]
[InlineData(StatusCodes.Status429TooManyRequests)]
[InlineData(StatusCodes.Status431RequestHeaderFieldsTooLarge)]
[InlineData(StatusCodes.Status451UnavailableForLegalReasons)]
[InlineData(StatusCodes.Status500InternalServerError)]
[InlineData(StatusCodes.Status501NotImplemented)]
@ -337,6 +341,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[InlineData(StatusCodes.Status505HttpVersionNotsupported)]
[InlineData(StatusCodes.Status506VariantAlsoNegotiates)]
[InlineData(StatusCodes.Status507InsufficientStorage)]
[InlineData(StatusCodes.Status508LoopDetected)]
[InlineData(StatusCodes.Status510NotExtended)]
[InlineData(StatusCodes.Status511NetworkAuthenticationRequired)]
public void IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode)
{
var sink = new TestSink();
@ -687,6 +694,29 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
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]
public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ButStillNotFresh()
{
@ -735,5 +765,30 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
sink.Writes,
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))
{
var client = server.CreateClient();
client.DefaultRequestHeaders.CacheControl =
new System.Net.Http.Headers.CacheControlHeaderValue { NoCache = true };
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, ""));
await AssertFreshResponseAsync(initialResponse, subsequentResponse);
@ -146,10 +152,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
using (var server = new TestServer(builder))
{
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, ""));
// 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, ""));
await AssertFreshResponseAsync(initialResponse, subsequentResponse);
@ -565,7 +577,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public async void ServesCachedContent_IfSubsequentRequest_ContainsNoStore()
public async void ServesCachedContent_IfSubsequentRequestContainsNoStore()
{
var builders = TestUtils.CreateBuildersWithResponseCaching();
@ -587,7 +599,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
public async void ServesFreshContent_IfInitialRequestContains_NoStore()
public async void ServesFreshContent_IfInitialRequestContainsNoStore()
{
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]
public async void Serves304_IfIfModifiedSince_Satisfied()
{
@ -619,7 +656,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
var client = server.CreateClient();
var initialResponse = await client.GetAsync("");
client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MaxValue;
client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.MaxValue;
var subsequentResponse = await client.GetAsync("");
initialResponse.EnsureSuccessStatusCode();
@ -639,7 +676,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
var client = server.CreateClient();
var initialResponse = await client.GetAsync("");
client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue;
client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
var subsequentResponse = await client.GetAsync("");
await AssertCachedResponseAsync(initialResponse, subsequentResponse);

View File

@ -20,6 +20,7 @@ using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Xunit;
using ISystemClock = Microsoft.AspNetCore.ResponseCaching.Internal.ISystemClock;
namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
@ -42,11 +43,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
var uniqueId = Guid.NewGuid().ToString();
headers.CacheControl = new CacheControlHeaderValue
if (headers.CacheControl == null)
{
Public = true,
MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null
};
headers.CacheControl = new CacheControlHeaderValue
{
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.Headers["X-Value"] = uniqueId;
@ -103,12 +112,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
internal static ResponseCachingMiddleware CreateTestMiddleware(
RequestDelegate next = null,
IResponseCache cache = null,
ResponseCachingOptions options = null,
TestSink testSink = null,
IResponseCachingKeyProvider keyProvider = null,
IResponseCachingPolicyProvider policyProvider = null)
{
if (next == null)
{
next = httpContext => TaskCache.CompletedTask;
}
if (cache == null)
{
cache = new TestResponseCache();
@ -127,7 +141,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
return new ResponseCachingMiddleware(
httpContext => TaskCache.CompletedTask,
next,
Options.Create(options),
testSink == null ? (ILoggerFactory)NullLoggerFactory.Instance : new TestLoggerFactory(testSink, true),
policyProvider,
@ -188,7 +202,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
internal static LoggedMessage ResponseWithUnsuccessfulStatusCodeNotCacheable => new LoggedMessage(17, LogLevel.Debug);
internal static LoggedMessage NotModifiedIfNoneMatchStar => new LoggedMessage(18, 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 CachedResponseServed => new LoggedMessage(22, 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 ResponseNotCached => new LoggedMessage(27, LogLevel.Information);
internal static LoggedMessage ResponseContentLengthMismatchNotCached => new LoggedMessage(28, LogLevel.Warning);
internal static LoggedMessage ExpirationInfiniteMaxStaleSatisfied => new LoggedMessage(29, LogLevel.Debug);
private LoggedMessage(int evenId, LogLevel logLevel)
{
@ -218,11 +233,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
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
@ -284,4 +309,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
return TaskCache.CompletedTask;
}
}
internal class TestClock : ISystemClock
{
public DateTimeOffset UtcNow { get; set; }
}
}