diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs
index 77b3f28..51a0400 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs
@@ -6,21 +6,35 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
public interface IResponseCachingPolicyProvider
{
///
- /// 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.
///
/// The .
- /// true if the request is cacheable; otherwise false.
- bool IsRequestCacheable(ResponseCachingContext context);
+ /// true if response caching logic should be attempted; otherwise false.
+ bool AttemptResponseCaching(ResponseCachingContext context);
///
- /// 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.
+ ///
+ /// The .
+ /// true if cache lookup for this request is allowed; otherwise false.
+ bool AllowCacheLookup(ResponseCachingContext context);
+
+ ///
+ /// Determine whether storage of the response is allowed for the incoming HTTP request.
+ ///
+ /// The .
+ /// true if storage of the response for this request is allowed; otherwise false.
+ bool AllowCacheStorage(ResponseCachingContext context);
+
+ ///
+ /// Determine whether the response received by the middleware can be cached for future requests.
///
/// The .
/// true if the response is cacheable; otherwise false.
bool IsResponseCacheable(ResponseCachingContext context);
///
- /// 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.
///
/// The .
/// true if the cached entry is fresh; otherwise false.
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs
index b900731..f8a0bf3 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs
@@ -31,7 +31,7 @@ internal static class LoggerExtensions
private static Action _logResponseWithUnsuccessfulStatusCodeNotCacheable;
private static Action _logNotModifiedIfNoneMatchStar;
private static Action _logNotModifiedIfNoneMatchMatched;
- private static Action _logNotModifiedIfUnmodifiedSinceSatisfied;
+ private static Action _logNotModifiedIfModifiedSinceSatisfied;
private static Action _logNotModifiedServed;
private static Action _logCachedResponseServed;
private static Action _logGatewayTimeoutServed;
@@ -40,6 +40,7 @@ internal static class LoggerExtensions
private static Action _logResponseCached;
private static Action _logResponseNotCached;
private static Action _logResponseContentLengthMismatchNotCached;
+ private static Action _logExpirationInfiniteMaxStaleSatisfied;
static LoggerExtensions()
{
@@ -70,7 +71,7 @@ static LoggerExtensions()
_logExpirationMustRevalidate = LoggerMessage.Define(
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(
logLevel: LogLevel.Debug,
eventId: 8,
@@ -119,10 +120,10 @@ static LoggerExtensions()
logLevel: LogLevel.Debug,
eventId: 19,
formatString: $"The ETag {{ETag}} in the '{HeaderNames.IfNoneMatch}' header matched the ETag of a cached entry.");
- _logNotModifiedIfUnmodifiedSinceSatisfied = LoggerMessage.Define(
+ _logNotModifiedIfModifiedSinceSatisfied = LoggerMessage.Define(
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 @@ static LoggerExtensions()
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(
+ 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 @@ internal static void LogNotModifiedIfNoneMatchMatched(this ILogger logger, Entit
_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 @@ internal static void LogResponseContentLengthMismatchNotCached(this ILogger logg
{
_logResponseContentLengthMismatchNotCached(logger, null);
}
+
+ internal static void LogExpirationInfiniteMaxStaleSatisfied(this ILogger logger, TimeSpan age, TimeSpan maxAge)
+ {
+ _logExpirationInfiniteMaxStaleSatisfied(logger, age, maxAge, null);
+ }
}
}
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs
index eeed0d9..f9f8e86 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs
@@ -37,7 +37,7 @@ internal ResponseCachingContext(HttpContext httpContext, ILogger logger)
internal ILogger Logger { get; }
- internal bool ShouldCacheResponse { get; set; }
+ internal bool ShouldCacheResponse { get; set; }
internal string BaseKey { get; set; }
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs
index c37fcac..2108ff3 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs
@@ -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 @@ public virtual bool IsRequestCacheable(ResponseCachingContext context)
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 @@ public virtual bool IsRequestCacheable(ResponseCachingContext context)
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 @@ public virtual bool IsResponseCacheable(ResponseCachingContext context)
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 @@ public virtual bool IsCachedEntryFresh(ResponseCachingContext context)
// 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);
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs
index 2ac2000..ed41f3c 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs
@@ -74,39 +74,53 @@ public async Task Invoke(HttpContext httpContext)
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 TryServeCachedResponseAsync(ResponseCachingContext context, IResponseCacheEntry cacheEntry)
@@ -220,6 +234,12 @@ internal async Task FinalizeCacheHeadersAsync(ResponseCachingContext context)
(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 @@ internal async Task FinalizeCacheHeadersAsync(ResponseCachingContext context)
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 @@ internal Task OnResponseStartingAsync(ResponseCachingContext context)
}
}
+ internal static void AddResponseCachingFeature(HttpContext context)
+ {
+ if (context.Features.Get() != 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(new ResponseCachingFeature());
+ }
+
internal void ShimResponseStream(ResponseCachingContext context)
{
// Shim response stream
@@ -337,13 +366,12 @@ internal void ShimResponseStream(ResponseCachingContext context)
}
// Add IResponseCachingFeature
- if (context.HttpContext.Features.Get() != 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(new ResponseCachingFeature());
+ AddResponseCachingFeature(context.HttpContext);
}
+ internal static void RemoveResponseCachingFeature(HttpContext context) =>
+ context.Features.Set(null);
+
internal static void UnshimResponseStream(ResponseCachingContext context)
{
// Unshim response stream
@@ -353,7 +381,7 @@ internal static void UnshimResponseStream(ResponseCachingContext context)
context.HttpContext.Features.Set(context.OriginalSendFileFeature);
// Remove IResponseCachingFeature
- context.HttpContext.Features.Set(null);
+ RemoveResponseCachingFeature(context.HttpContext);
}
internal static bool ContentIsNotModified(ResponseCachingContext context)
@@ -388,8 +416,8 @@ internal static bool ContentIsNotModified(ResponseCachingContext context)
}
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 @@ internal static bool ContentIsNotModified(ResponseCachingContext context)
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;
}
}
diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs
index a31d7c3..fc66045 100644
--- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs
+++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs
@@ -5,7 +5,9 @@
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 @@ public void ContentIsNotModified_NotConditionalRequest_False()
}
[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 @@ public void ContentIsNotModified_IfUnmodifiedSince_FallsbackToDateHeader()
// 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 @@ public void ContentIsNotModified_IfUnmodifiedSince_LastModifiedOverridesDateHead
// 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 @@ public void ContentIsNotModified_IfNoneMatch_Overrides_IfUnmodifiedSince_ToTrue(
}
[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 @@ public void ContentIsNotModified_IfNoneMatch_MatchesAtLeastOneValue_True()
}
[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.Equal(clock.UtcNow, context.ResponseTime);
+ }
- Assert.False(context.ShouldCacheResponse);
- Assert.Empty(sink.Writes);
+ [Fact]
+ 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 FinalizeCacheHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable()
+ 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 @@ public async Task FinalizeCacheHeaders_UpdateShouldCacheResponse_IfResponseIsCac
}
[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 @@ public async Task FinalizeCacheHeaders_DefaultResponseValidity_Is10Seconds()
}
[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 @@ public async Task FinalizeCacheHeaders_ResponseValidity_UseExpiryIfAvailable()
}
[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 @@ public async Task FinalizeCacheHeaders_ResponseValidity_UseMaxAgeIfAvailable()
}
[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 @@ public async Task FinalizeCacheHeaders_ResponseValidity_UseSharedMaxAgeIfAvailab
}
[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 @@ public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_IfNotEquivalentTo
};
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 @@ public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_IfEquivalentToPre
};
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 @@ public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_IfEquivalentToPre
Assert.Same(cachedVaryByRules, context.CachedVaryByRules);
TestUtils.AssertLoggedMessages(
sink.Writes,
- LoggedMessage.NoResponseServed,
LoggedMessage.VaryByRulesUpdated);
}
@@ -515,7 +580,7 @@ public static TheoryData NullOrEmptyVaryRules
[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 @@ public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_NullOrEmptyRules(
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 @@ public async Task FinalizeCacheHeaders_AddsDate_IfNoneSpecified()
}
[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 @@ public async Task FinalizeCacheHeaders_StoresCachedResponse_InState()
}
[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 @@ public async Task FinalizeCacheBody_Cache_IfContentLengthMatches()
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 @@ public async Task FinalizeCacheBody_DoNotCache_IfContentLengthMismatches()
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 @@ public async Task FinalizeCacheBody_Cache_IfContentLengthAbsent()
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 @@ public async Task FinalizeCacheBody_DoNotCache_IfShouldCacheResponseFalse()
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 @@ public async Task FinalizeCacheBody_DoNotCache_IfBufferingDisabled()
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 @@ public async Task FinalizeCacheBody_DoNotCache_IfBufferingDisabled()
}
[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(() => middleware.ShimResponseStream(context));
+ Assert.ThrowsAny(() => ResponseCachingMiddleware.AddResponseCachingFeature(httpContext));
+ }
+
+ private class FakeResponseFeature : HttpResponseFeature
+ {
+ public override void OnStarting(Func