Add logging

This commit is contained in:
John Luo 2016-09-29 23:14:31 -07:00
parent 9e5dbee208
commit 3b2a2eb892
9 changed files with 829 additions and 115 deletions

View File

@ -0,0 +1,300 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
/// <summary>
/// Defines *all* the logger messages produced by response caching
/// </summary>
internal static class LoggerExtensions
{
private static Action<ILogger, string, Exception> _logRequestMethodNotCacheable;
private static Action<ILogger, Exception> _logRequestWithAuthorizationNotCacheable;
private static Action<ILogger, Exception> _logRequestWithNoCacheNotCacheable;
private static Action<ILogger, Exception> _logRequestWithPragmaNoCacheNotCacheable;
private static Action<ILogger, TimeSpan, Exception> _logExpirationMinFreshAdded;
private static Action<ILogger, TimeSpan, TimeSpan, Exception> _logExpirationSharedMaxAgeExceeded;
private static Action<ILogger, TimeSpan, TimeSpan, Exception> _logExpirationMustRevalidate;
private static Action<ILogger, TimeSpan, TimeSpan, TimeSpan, Exception> _logExpirationMaxStaleSatisfied;
private static Action<ILogger, TimeSpan, TimeSpan, Exception> _logExpirationMaxAgeExceeded;
private static Action<ILogger, DateTimeOffset, DateTimeOffset, Exception> _logExpirationExpiresExceeded;
private static Action<ILogger, Exception> _logResponseWithoutPublicNotCacheable;
private static Action<ILogger, Exception> _logResponseWithNoStoreNotCacheable;
private static Action<ILogger, Exception> _logResponseWithNoCacheNotCacheable;
private static Action<ILogger, Exception> _logResponseWithSetCookieNotCacheable;
private static Action<ILogger, Exception> _logResponseWithVaryStarNotCacheable;
private static Action<ILogger, Exception> _logResponseWithPrivateNotCacheable;
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, Exception> _logNotModifiedServed;
private static Action<ILogger, Exception> _logCachedResponseServed;
private static Action<ILogger, Exception> _logGatewayTimeoutServed;
private static Action<ILogger, Exception> _logNoResponseServed;
private static Action<ILogger, string, string, Exception> _logVaryByRulesUpdated;
private static Action<ILogger, Exception> _logResponseCached;
private static Action<ILogger, Exception> _logResponseNotCached;
private static Action<ILogger, Exception> _logResponseContentLengthMismatchNotCached;
static LoggerExtensions()
{
_logRequestMethodNotCacheable = LoggerMessage.Define<string>(
logLevel: LogLevel.Debug,
eventId: 1,
formatString: "The request cannot be served from cache because it uses the HTTP method: {Method}.");
_logRequestWithAuthorizationNotCacheable = LoggerMessage.Define(
logLevel: LogLevel.Debug,
eventId: 2,
formatString: $"The request cannot be served from cache because it contains an '{HeaderNames.Authorization}' header.");
_logRequestWithNoCacheNotCacheable = LoggerMessage.Define(
logLevel: LogLevel.Debug,
eventId: 3,
formatString: "The request cannot be served from cache because it contains a 'no-cache' cache directive.");
_logRequestWithPragmaNoCacheNotCacheable = LoggerMessage.Define(
logLevel: LogLevel.Debug,
eventId: 4,
formatString: "The request cannot be served from cache because it contains a 'no-cache' pragma directive.");
_logExpirationMinFreshAdded = LoggerMessage.Define<TimeSpan>(
logLevel: LogLevel.Debug,
eventId: 5,
formatString: "Adding a minimum freshness requirement of {Duration} specified by the 'min-fresh' cache directive.");
_logExpirationSharedMaxAgeExceeded = LoggerMessage.Define<TimeSpan, TimeSpan>(
logLevel: LogLevel.Debug,
eventId: 6,
formatString: "The age of the entry is {Age} and has exceeded the maximum age for shared caches of {SharedMaxAge} specified by the 's-maxage' cache directive.");
_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.");
_logExpirationMaxStaleSatisfied = LoggerMessage.Define<TimeSpan, TimeSpan, TimeSpan>(
logLevel: LogLevel.Debug,
eventId: 8,
formatString: "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. However, it satisfied the maximum stale allowance of {MaxStale} specified by the 'max-stale' cache directive.");
_logExpirationMaxAgeExceeded = LoggerMessage.Define<TimeSpan, TimeSpan>(
logLevel: LogLevel.Debug,
eventId: 9,
formatString: "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive.");
_logExpirationExpiresExceeded = LoggerMessage.Define<DateTimeOffset, DateTimeOffset>(
logLevel: LogLevel.Debug,
eventId: 10,
formatString: $"The response time of the entry is {{ResponseTime}} and has exceeded the expiry date of {{Expired}} specified by the '{HeaderNames.Expires}' header.");
_logResponseWithoutPublicNotCacheable = LoggerMessage.Define(
logLevel: LogLevel.Debug,
eventId: 11,
formatString: "Response is not cacheable because it does not contain the 'public' cache directive.");
_logResponseWithNoStoreNotCacheable = LoggerMessage.Define(
logLevel: LogLevel.Debug,
eventId: 12,
formatString: "Response is not cacheable because it or its corresponding request contains a 'no-store' cache directive.");
_logResponseWithNoCacheNotCacheable = LoggerMessage.Define(
logLevel: LogLevel.Debug,
eventId: 13,
formatString: "Response is not cacheable because it contains a 'no-cache' cache directive.");
_logResponseWithSetCookieNotCacheable = LoggerMessage.Define(
logLevel: LogLevel.Debug,
eventId: 14,
formatString: $"Response is not cacheable because it contains a '{HeaderNames.SetCookie}' header.");
_logResponseWithVaryStarNotCacheable = LoggerMessage.Define(
logLevel: LogLevel.Debug,
eventId: 15,
formatString: $"Response is not cacheable because it contains a '{HeaderNames.Vary}' header with a value of *.");
_logResponseWithPrivateNotCacheable = LoggerMessage.Define(
logLevel: LogLevel.Debug,
eventId: 16,
formatString: "Response is not cacheable because it contains the 'private' cache directive.");
_logResponseWithUnsuccessfulStatusCodeNotCacheable = LoggerMessage.Define<int>(
logLevel: LogLevel.Debug,
eventId: 17,
formatString: "Response is not cacheable because its status code {StatusCode} does not indicate success.");
_logNotModifiedIfNoneMatchStar = LoggerMessage.Define(
logLevel: LogLevel.Debug,
eventId: 18,
formatString: $"The '{HeaderNames.IfNoneMatch}' header of the request contains a value of *.");
_logNotModifiedIfNoneMatchMatched = LoggerMessage.Define<EntityTagHeaderValue>(
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>(
logLevel: LogLevel.Debug,
eventId: 20,
formatString: $"The last modified date of {{LastModified}} is before the date {{IfUnmodifiedSince}} specified in the '{HeaderNames.IfUnmodifiedSince}' header.");
_logNotModifiedServed = LoggerMessage.Define(
logLevel: LogLevel.Information,
eventId: 21,
formatString: "The content requested has not been modified.");
_logCachedResponseServed = LoggerMessage.Define(
logLevel: LogLevel.Information,
eventId: 22,
formatString: "Serving response from cache.");
_logGatewayTimeoutServed = LoggerMessage.Define(
logLevel: LogLevel.Information,
eventId: 23,
formatString: "No cached response available for this request and the 'only-if-cached' cache directive was specified.");
_logNoResponseServed = LoggerMessage.Define(
logLevel: LogLevel.Information,
eventId: 24,
formatString: "No cached response available for this request.");
_logVaryByRulesUpdated = LoggerMessage.Define<string, string>(
logLevel: LogLevel.Debug,
eventId: 25,
formatString: "Vary by rules were updated. Headers: {Headers}, Query keys: {QueryKeys}");
_logResponseCached = LoggerMessage.Define(
logLevel: LogLevel.Information,
eventId: 26,
formatString: "The response has been cached.");
_logResponseNotCached = LoggerMessage.Define(
logLevel: LogLevel.Information,
eventId: 27,
formatString: "The response could not be cached for this request.");
_logResponseContentLengthMismatchNotCached = LoggerMessage.Define(
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.");
}
internal static void LogRequestMethodNotCacheable(this ILogger logger, string method)
{
_logRequestMethodNotCacheable(logger, method, null);
}
internal static void LogRequestWithAuthorizationNotCacheable(this ILogger logger)
{
_logRequestWithAuthorizationNotCacheable(logger, null);
}
internal static void LogRequestWithNoCacheNotCacheable(this ILogger logger)
{
_logRequestWithNoCacheNotCacheable(logger, null);
}
internal static void LogRequestWithPragmaNoCacheNotCacheable(this ILogger logger)
{
_logRequestWithPragmaNoCacheNotCacheable(logger, null);
}
internal static void LogExpirationMinFreshAdded(this ILogger logger, TimeSpan duration)
{
_logExpirationMinFreshAdded(logger, duration, null);
}
internal static void LogExpirationSharedMaxAgeExceeded(this ILogger logger, TimeSpan age, TimeSpan sharedMaxAge)
{
_logExpirationSharedMaxAgeExceeded(logger, age, sharedMaxAge, null);
}
internal static void LogExpirationMustRevalidate(this ILogger logger, TimeSpan age, TimeSpan maxAge)
{
_logExpirationMustRevalidate(logger, age, maxAge, null);
}
internal static void LogExpirationMaxStaleSatisfied(this ILogger logger, TimeSpan age, TimeSpan maxAge, TimeSpan maxStale)
{
_logExpirationMaxStaleSatisfied(logger, age, maxAge, maxStale, null);
}
internal static void LogExpirationMaxAgeExceeded(this ILogger logger, TimeSpan age, TimeSpan sharedMaxAge)
{
_logExpirationMaxAgeExceeded(logger, age, sharedMaxAge, null);
}
internal static void LogExpirationExpiresExceeded(this ILogger logger, DateTimeOffset responseTime, DateTimeOffset expires)
{
_logExpirationExpiresExceeded(logger, responseTime, expires, null);
}
internal static void LogResponseWithoutPublicNotCacheable(this ILogger logger)
{
_logResponseWithoutPublicNotCacheable(logger, null);
}
internal static void LogResponseWithNoStoreNotCacheable(this ILogger logger)
{
_logResponseWithNoStoreNotCacheable(logger, null);
}
internal static void LogResponseWithNoCacheNotCacheable(this ILogger logger)
{
_logResponseWithNoCacheNotCacheable(logger, null);
}
internal static void LogResponseWithSetCookieNotCacheable(this ILogger logger)
{
_logResponseWithSetCookieNotCacheable(logger, null);
}
internal static void LogResponseWithVaryStarNotCacheable(this ILogger logger)
{
_logResponseWithVaryStarNotCacheable(logger, null);
}
internal static void LogResponseWithPrivateNotCacheable(this ILogger logger)
{
_logResponseWithPrivateNotCacheable(logger, null);
}
internal static void LogResponseWithUnsuccessfulStatusCodeNotCacheable(this ILogger logger, int statusCode)
{
_logResponseWithUnsuccessfulStatusCodeNotCacheable(logger, statusCode, null);
}
internal static void LogNotModifiedIfNoneMatchStar(this ILogger logger)
{
_logNotModifiedIfNoneMatchStar(logger, null);
}
internal static void LogNotModifiedIfNoneMatchMatched(this ILogger logger, EntityTagHeaderValue etag)
{
_logNotModifiedIfNoneMatchMatched(logger, etag, null);
}
internal static void LogNotModifiedIfUnmodifiedSinceSatisfied(this ILogger logger, DateTimeOffset lastModified, DateTimeOffset ifUnmodifiedSince)
{
_logNotModifiedIfUnmodifiedSinceSatisfied(logger, lastModified, ifUnmodifiedSince, null);
}
internal static void LogNotModifiedServed(this ILogger logger)
{
_logNotModifiedServed(logger, null);
}
internal static void LogCachedResponseServed(this ILogger logger)
{
_logCachedResponseServed(logger, null);
}
internal static void LogGatewayTimeoutServed(this ILogger logger)
{
_logGatewayTimeoutServed(logger, null);
}
internal static void LogNoResponseServed(this ILogger logger)
{
_logNoResponseServed(logger, null);
}
internal static void LogVaryByRulesUpdated(this ILogger logger, string headers, string queryKeys)
{
_logVaryByRulesUpdated(logger, headers, queryKeys, null);
}
internal static void LogResponseCached(this ILogger logger)
{
_logResponseCached(logger, null);
}
internal static void LogResponseNotCached(this ILogger logger)
{
_logResponseNotCached(logger, null);
}
internal static void LogResponseContentLengthMismatchNotCached(this ILogger logger)
{
_logResponseContentLengthMismatchNotCached(logger, null);
}
}
}

View File

@ -6,6 +6,7 @@ using System.IO;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.Http.Headers;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.ResponseCaching.Internal namespace Microsoft.AspNetCore.ResponseCaching.Internal
@ -23,10 +24,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
private DateTimeOffset? _responseExpires; private DateTimeOffset? _responseExpires;
private bool _parsedResponseExpires; private bool _parsedResponseExpires;
internal ResponseCacheContext( internal ResponseCacheContext(HttpContext httpContext, ILogger logger)
HttpContext httpContext)
{ {
HttpContext = httpContext; HttpContext = httpContext;
Logger = logger;
} }
public HttpContext HttpContext { get; } public HttpContext HttpContext { get; }
@ -37,6 +38,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
public CachedVaryByRules CachedVaryByRules { get; internal set; } public CachedVaryByRules CachedVaryByRules { get; internal set; }
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

@ -18,12 +18,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
var request = context.HttpContext.Request; var request = context.HttpContext.Request;
if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsHead(request.Method)) if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsHead(request.Method))
{ {
context.Logger.LogRequestMethodNotCacheable(request.Method);
return false; return false;
} }
// Verify existence of authorization headers // Verify existence of authorization headers
if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.Authorization])) if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.Authorization]))
{ {
context.Logger.LogRequestWithAuthorizationNotCacheable();
return false; return false;
} }
@ -32,6 +34,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
{ {
if (context.RequestCacheControlHeaderValue.NoCache) if (context.RequestCacheControlHeaderValue.NoCache)
{ {
context.Logger.LogRequestWithNoCacheNotCacheable();
return false; return false;
} }
} }
@ -43,6 +46,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
{ {
if (string.Equals("no-cache", directive, StringComparison.OrdinalIgnoreCase)) if (string.Equals("no-cache", directive, StringComparison.OrdinalIgnoreCase))
{ {
context.Logger.LogRequestWithPragmaNoCacheNotCacheable();
return false; return false;
} }
} }
@ -56,18 +60,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
// Only cache pages explicitly marked with public // Only cache pages explicitly marked with public
if (!context.ResponseCacheControlHeaderValue.Public) if (!context.ResponseCacheControlHeaderValue.Public)
{ {
context.Logger.LogResponseWithoutPublicNotCacheable();
return false; return false;
} }
// Check no-store // Check no-store
if (context.RequestCacheControlHeaderValue.NoStore || context.ResponseCacheControlHeaderValue.NoStore) if (context.RequestCacheControlHeaderValue.NoStore || context.ResponseCacheControlHeaderValue.NoStore)
{ {
context.Logger.LogResponseWithNoStoreNotCacheable();
return false; return false;
} }
// Check no-cache // Check no-cache
if (context.ResponseCacheControlHeaderValue.NoCache) if (context.ResponseCacheControlHeaderValue.NoCache)
{ {
context.Logger.LogResponseWithNoCacheNotCacheable();
return false; return false;
} }
@ -76,6 +83,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
// Do not cache responses with Set-Cookie headers // Do not cache responses with Set-Cookie headers
if (!StringValues.IsNullOrEmpty(response.Headers[HeaderNames.SetCookie])) if (!StringValues.IsNullOrEmpty(response.Headers[HeaderNames.SetCookie]))
{ {
context.Logger.LogResponseWithSetCookieNotCacheable();
return false; return false;
} }
@ -83,18 +91,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
var varyHeader = response.Headers[HeaderNames.Vary]; var varyHeader = response.Headers[HeaderNames.Vary];
if (varyHeader.Count == 1 && string.Equals(varyHeader, "*", StringComparison.OrdinalIgnoreCase)) if (varyHeader.Count == 1 && string.Equals(varyHeader, "*", StringComparison.OrdinalIgnoreCase))
{ {
context.Logger.LogResponseWithVaryStarNotCacheable();
return false; return false;
} }
// Check private // Check private
if (context.ResponseCacheControlHeaderValue.Private) if (context.ResponseCacheControlHeaderValue.Private)
{ {
context.Logger.LogResponseWithPrivateNotCacheable();
return false; return false;
} }
// Check response code // Check response code
if (response.StatusCode != StatusCodes.Status200OK) if (response.StatusCode != StatusCodes.Status200OK)
{ {
context.Logger.LogResponseWithUnsuccessfulStatusCodeNotCacheable(response.StatusCode);
return false; return false;
} }
@ -105,6 +116,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
!context.ResponseCacheControlHeaderValue.MaxAge.HasValue && !context.ResponseCacheControlHeaderValue.MaxAge.HasValue &&
context.ResponseTime.Value >= context.ResponseExpires) context.ResponseTime.Value >= context.ResponseExpires)
{ {
context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, context.ResponseExpires.Value);
return false; return false;
} }
} }
@ -113,22 +125,27 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
var age = context.ResponseTime.Value - context.ResponseDate.Value; var age = context.ResponseTime.Value - context.ResponseDate.Value;
// Validate shared max age // Validate shared max age
if (age >= context.ResponseCacheControlHeaderValue.SharedMaxAge) var sharedMaxAge = context.ResponseCacheControlHeaderValue.SharedMaxAge;
if (age >= sharedMaxAge)
{ {
context.Logger.LogExpirationSharedMaxAgeExceeded(age, sharedMaxAge.Value);
return false; return false;
} }
else if (!context.ResponseCacheControlHeaderValue.SharedMaxAge.HasValue) else if (!sharedMaxAge.HasValue)
{ {
// Validate max age // Validate max age
if (age >= context.ResponseCacheControlHeaderValue.MaxAge) var maxAge = context.ResponseCacheControlHeaderValue.MaxAge;
if (age >= maxAge)
{ {
context.Logger.LogExpirationMaxAgeExceeded(age, maxAge.Value);
return false; return false;
} }
else if (!context.ResponseCacheControlHeaderValue.MaxAge.HasValue) else if (!maxAge.HasValue)
{ {
// Validate expiration // Validate expiration
if (context.ResponseTime.Value >= context.ResponseExpires) if (context.ResponseTime.Value >= context.ResponseExpires)
{ {
context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, context.ResponseExpires.Value);
return false; return false;
} }
} }
@ -144,41 +161,55 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
var cachedControlHeaders = context.CachedResponseHeaders.CacheControl ?? EmptyCacheControl; var cachedControlHeaders = context.CachedResponseHeaders.CacheControl ?? EmptyCacheControl;
// Add min-fresh requirements // Add min-fresh requirements
if (context.RequestCacheControlHeaderValue.MinFresh.HasValue) var minFresh = context.RequestCacheControlHeaderValue.MinFresh;
if (minFresh.HasValue)
{ {
age += context.RequestCacheControlHeaderValue.MinFresh.Value; age += minFresh.Value;
context.Logger.LogExpirationMinFreshAdded(minFresh.Value);
} }
// Validate shared max age, this overrides any max age settings for shared caches // Validate shared max age, this overrides any max age settings for shared caches
if (age >= cachedControlHeaders.SharedMaxAge) var sharedMaxAge = cachedControlHeaders.SharedMaxAge;
if (age >= sharedMaxAge)
{ {
// shared max age implies must revalidate // shared max age implies must revalidate
context.Logger.LogExpirationSharedMaxAgeExceeded(age, sharedMaxAge.Value);
return false; return false;
} }
else if (!cachedControlHeaders.SharedMaxAge.HasValue) else if (!sharedMaxAge.HasValue)
{ {
var cachedMaxAge = cachedControlHeaders.MaxAge;
var requestMaxAge = context.RequestCacheControlHeaderValue.MaxAge;
var lowestMaxAge = cachedMaxAge < requestMaxAge ? cachedMaxAge : requestMaxAge ?? cachedMaxAge;
// Validate max age // Validate max age
if (age >= cachedControlHeaders.MaxAge || age >= context.RequestCacheControlHeaderValue.MaxAge) if (age >= lowestMaxAge)
{ {
// Must revalidate // Must revalidate
if (cachedControlHeaders.MustRevalidate) if (cachedControlHeaders.MustRevalidate)
{ {
context.Logger.LogExpirationMustRevalidate(age, lowestMaxAge.Value);
return false; return false;
} }
// Request allows stale values // Request allows stale values
if (age < context.RequestCacheControlHeaderValue.MaxStaleLimit) var maxStaleLimit = context.RequestCacheControlHeaderValue.MaxStaleLimit;
if (maxStaleLimit.HasValue && age - lowestMaxAge < maxStaleLimit)
{ {
context.Logger.LogExpirationMaxStaleSatisfied(age, lowestMaxAge.Value, maxStaleLimit.Value);
return true; return true;
} }
context.Logger.LogExpirationMaxAgeExceeded(age, lowestMaxAge.Value);
return false; return false;
} }
else if (!cachedControlHeaders.MaxAge.HasValue && !context.RequestCacheControlHeaderValue.MaxAge.HasValue) else if (!cachedMaxAge.HasValue && !requestMaxAge.HasValue)
{ {
// Validate expiration // Validate expiration
if (context.ResponseTime.Value >= context.CachedResponseHeaders.Expires) var responseTime = context.ResponseTime.Value;
var expires = context.CachedResponseHeaders.Expires;
if (responseTime >= expires)
{ {
context.Logger.LogExpirationExpiresExceeded(responseTime, expires.Value);
return false; return false;
} }
} }

View File

@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.Http.Headers;
using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
@ -22,15 +22,17 @@ namespace Microsoft.AspNetCore.ResponseCaching
private static readonly TimeSpan DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10); private static readonly TimeSpan DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10);
private readonly RequestDelegate _next; private readonly RequestDelegate _next;
private readonly IResponseCacheStore _store;
private readonly ResponseCacheOptions _options; private readonly ResponseCacheOptions _options;
private readonly ILogger _logger;
private readonly IResponseCachePolicyProvider _policyProvider; private readonly IResponseCachePolicyProvider _policyProvider;
private readonly IResponseCacheStore _store;
private readonly IResponseCacheKeyProvider _keyProvider; private readonly IResponseCacheKeyProvider _keyProvider;
private readonly Func<object, Task> _onStartingCallback; private readonly Func<object, Task> _onStartingCallback;
public ResponseCacheMiddleware( public ResponseCacheMiddleware(
RequestDelegate next, RequestDelegate next,
IOptions<ResponseCacheOptions> options, IOptions<ResponseCacheOptions> options,
ILoggerFactory loggerFactory,
IResponseCachePolicyProvider policyProvider, IResponseCachePolicyProvider policyProvider,
IResponseCacheStore store, IResponseCacheStore store,
IResponseCacheKeyProvider keyProvider) IResponseCacheKeyProvider keyProvider)
@ -39,34 +41,39 @@ namespace Microsoft.AspNetCore.ResponseCaching
{ {
throw new ArgumentNullException(nameof(next)); throw new ArgumentNullException(nameof(next));
} }
if (store == null)
{
throw new ArgumentNullException(nameof(store));
}
if (options == null) if (options == null)
{ {
throw new ArgumentNullException(nameof(options)); throw new ArgumentNullException(nameof(options));
} }
if (loggerFactory == null)
{
throw new ArgumentNullException(nameof(loggerFactory));
}
if (policyProvider == null) if (policyProvider == null)
{ {
throw new ArgumentNullException(nameof(policyProvider)); throw new ArgumentNullException(nameof(policyProvider));
} }
if (store == null)
{
throw new ArgumentNullException(nameof(store));
}
if (keyProvider == null) if (keyProvider == null)
{ {
throw new ArgumentNullException(nameof(keyProvider)); throw new ArgumentNullException(nameof(keyProvider));
} }
_next = next; _next = next;
_store = store;
_options = options.Value; _options = options.Value;
_logger = loggerFactory.CreateLogger<ResponseCacheMiddleware>();
_policyProvider = policyProvider; _policyProvider = policyProvider;
_store = store;
_keyProvider = keyProvider; _keyProvider = keyProvider;
_onStartingCallback = state => OnResponseStartingAsync((ResponseCacheContext)state); _onStartingCallback = state => OnResponseStartingAsync((ResponseCacheContext)state);
} }
public async Task Invoke(HttpContext httpContext) public async Task Invoke(HttpContext httpContext)
{ {
var context = new ResponseCacheContext(httpContext); var context = new ResponseCacheContext(httpContext, _logger);
// Should we attempt any caching logic? // Should we attempt any caching logic?
if (_policyProvider.IsRequestCacheable(context)) if (_policyProvider.IsRequestCacheable(context))
@ -115,8 +122,9 @@ namespace Microsoft.AspNetCore.ResponseCaching
if (_policyProvider.IsCachedEntryFresh(context)) if (_policyProvider.IsCachedEntryFresh(context))
{ {
// Check conditional request rules // Check conditional request rules
if (ConditionalRequestSatisfied(context)) if (ContentIsNotModified(context))
{ {
_logger.LogNotModifiedServed();
context.HttpContext.Response.StatusCode = StatusCodes.Status304NotModified; context.HttpContext.Response.StatusCode = StatusCodes.Status304NotModified;
} }
else else
@ -150,8 +158,8 @@ namespace Microsoft.AspNetCore.ResponseCaching
context.HttpContext.Abort(); context.HttpContext.Abort();
} }
} }
_logger.LogCachedResponseServed();
} }
return true; return true;
} }
@ -183,13 +191,14 @@ namespace Microsoft.AspNetCore.ResponseCaching
return true; return true;
} }
if (context.RequestCacheControlHeaderValue.OnlyIfCached) if (context.RequestCacheControlHeaderValue.OnlyIfCached)
{ {
_logger.LogGatewayTimeoutServed();
context.HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; context.HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
return true; return true;
} }
_logger.LogNoResponseServed();
return false; return false;
} }
@ -229,6 +238,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
} }
// Always overwrite the CachedVaryByRules to update the expiry information // Always overwrite the CachedVaryByRules to update the expiry information
_logger.LogVaryByRulesUpdated(normalizedVaryHeaders, normalizedVaryQueryKeys);
await _store.SetAsync(context.BaseKey, context.CachedVaryByRules, context.CachedResponseValidFor); await _store.SetAsync(context.BaseKey, context.CachedVaryByRules, context.CachedResponseValidFor);
context.StorageVaryKey = _keyProvider.CreateStorageVaryByKey(context); context.StorageVaryKey = _keyProvider.CreateStorageVaryByKey(context);
@ -272,8 +282,17 @@ namespace Microsoft.AspNetCore.ResponseCaching
if (!contentLength.HasValue || contentLength == bufferStream.Length) if (!contentLength.HasValue || contentLength == bufferStream.Length)
{ {
context.CachedResponse.Body = bufferStream; context.CachedResponse.Body = bufferStream;
_logger.LogResponseCached();
await _store.SetAsync(context.StorageVaryKey ?? context.BaseKey, context.CachedResponse, context.CachedResponseValidFor); await _store.SetAsync(context.StorageVaryKey ?? context.BaseKey, context.CachedResponse, context.CachedResponseValidFor);
} }
else
{
_logger.LogResponseContentLengthMismatchNotCached();
}
}
else
{
_logger.LogResponseNotCached();
} }
} }
@ -320,7 +339,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
context.HttpContext.RemoveResponseCacheFeature(); context.HttpContext.RemoveResponseCacheFeature();
} }
internal static bool ConditionalRequestSatisfied(ResponseCacheContext context) internal static bool ContentIsNotModified(ResponseCacheContext context)
{ {
var cachedResponseHeaders = context.CachedResponseHeaders; var cachedResponseHeaders = context.CachedResponseHeaders;
var ifNoneMatchHeader = context.TypedRequestHeaders.IfNoneMatch; var ifNoneMatchHeader = context.TypedRequestHeaders.IfNoneMatch;
@ -329,6 +348,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
{ {
if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any)) if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any))
{ {
context.Logger.LogNotModifiedIfNoneMatchStar();
return true; return true;
} }
@ -338,6 +358,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
{ {
if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: false)) if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: false))
{ {
context.Logger.LogNotModifiedIfNoneMatchMatched(tag);
return true; return true;
} }
} }
@ -346,9 +367,14 @@ namespace Microsoft.AspNetCore.ResponseCaching
else else
{ {
var ifUnmodifiedSince = context.TypedRequestHeaders.IfUnmodifiedSince; var ifUnmodifiedSince = context.TypedRequestHeaders.IfUnmodifiedSince;
if (ifUnmodifiedSince != null && (cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= ifUnmodifiedSince) if (ifUnmodifiedSince != null)
{ {
return true; var lastModified = cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date;
if (lastModified <= ifUnmodifiedSince)
{
context.Logger.LogNotModifiedIfUnmodifiedSinceSatisfied(lastModified.Value, ifUnmodifiedSince.Value);
return true;
}
} }
} }

View File

@ -25,6 +25,7 @@
"Microsoft.AspNetCore.Http": "1.1.0-*", "Microsoft.AspNetCore.Http": "1.1.0-*",
"Microsoft.AspNetCore.Http.Extensions": "1.1.0-*", "Microsoft.AspNetCore.Http.Extensions": "1.1.0-*",
"Microsoft.Extensions.Caching.Memory": "1.1.0-*", "Microsoft.Extensions.Caching.Memory": "1.1.0-*",
"Microsoft.Extensions.Logging.Abstractions": "1.1.0-*",
"Microsoft.Extensions.TaskCache.Sources": { "Microsoft.Extensions.TaskCache.Sources": {
"version": "1.1.0-*", "version": "1.1.0-*",
"type": "build" "type": "build"

View File

@ -7,6 +7,9 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.Http.Headers;
using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Xunit; using Xunit;
@ -19,7 +22,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504() public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504()
{ {
var store = new TestResponseCacheStore(); var store = new TestResponseCacheStore();
var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider()); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store, keyProvider: new TestResponseCacheKeyProvider());
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
{ {
@ -28,24 +32,32 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Assert.True(await middleware.TryServeFromCacheAsync(context)); Assert.True(await middleware.TryServeFromCacheAsync(context));
Assert.Equal(StatusCodes.Status504GatewayTimeout, context.HttpContext.Response.StatusCode); Assert.Equal(StatusCodes.Status504GatewayTimeout, context.HttpContext.Response.StatusCode);
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.GatewayTimeoutServed);
} }
[Fact] [Fact]
public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails() public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails()
{ {
var store = new TestResponseCacheStore(); var store = new TestResponseCacheStore();
var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey")); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey"));
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
Assert.False(await middleware.TryServeFromCacheAsync(context)); Assert.False(await middleware.TryServeFromCacheAsync(context));
Assert.Equal(1, store.GetCount); Assert.Equal(1, store.GetCount);
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.NoResponseServed);
} }
[Fact] [Fact]
public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds() public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds()
{ {
var store = new TestResponseCacheStore(); var store = new TestResponseCacheStore();
var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey")); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey"));
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
await store.SetAsync( await store.SetAsync(
@ -58,13 +70,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Assert.True(await middleware.TryServeFromCacheAsync(context)); Assert.True(await middleware.TryServeFromCacheAsync(context));
Assert.Equal(1, store.GetCount); Assert.Equal(1, store.GetCount);
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.CachedResponseServed);
} }
[Fact] [Fact]
public async Task TryServeFromCacheAsync_VaryByRuleFound_CachedResponseNotFound_Fails() public async Task TryServeFromCacheAsync_VaryByRuleFound_CachedResponseNotFound_Fails()
{ {
var store = new TestResponseCacheStore(); var store = new TestResponseCacheStore();
var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey", "VaryKey")); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey", "VaryKey"));
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
await store.SetAsync( await store.SetAsync(
@ -74,13 +90,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Assert.False(await middleware.TryServeFromCacheAsync(context)); Assert.False(await middleware.TryServeFromCacheAsync(context));
Assert.Equal(2, store.GetCount); Assert.Equal(2, store.GetCount);
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.NoResponseServed);
} }
[Fact] [Fact]
public async Task TryServeFromCacheAsync_VaryByRuleFound_CachedResponseFound_Succeeds() public async Task TryServeFromCacheAsync_VaryByRuleFound_CachedResponseFound_Succeeds()
{ {
var store = new TestResponseCacheStore(); var store = new TestResponseCacheStore();
var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey", new[] { "VaryKey", "VaryKey2" })); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey", new[] { "VaryKey", "VaryKey2" }));
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
await store.SetAsync( await store.SetAsync(
@ -97,44 +117,83 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Assert.True(await middleware.TryServeFromCacheAsync(context)); Assert.True(await middleware.TryServeFromCacheAsync(context));
Assert.Equal(3, store.GetCount); Assert.Equal(3, store.GetCount);
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.CachedResponseServed);
} }
[Fact] [Fact]
public void ConditionalRequestSatisfied_NotConditionalRequest_Fails() public async Task TryServeFromCacheAsync_CachedResponseFound_Serves304IfPossible()
{ {
var store = new TestResponseCacheStore();
var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey"));
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "*";
await store.SetAsync(
"BaseKey",
new CachedResponse()
{
Body = new SegmentReadStream(new List<byte[]>(0), 0)
},
TimeSpan.Zero);
Assert.True(await middleware.TryServeFromCacheAsync(context));
Assert.Equal(1, store.GetCount);
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.NotModifiedServed);
}
[Fact]
public void ContentIsNotModified_NotConditionalRequest_False()
{
var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
Assert.False(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); Assert.False(ResponseCacheMiddleware.ContentIsNotModified(context));
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public void ConditionalRequestSatisfied_IfUnmodifiedSince_FallsbackToDateHeader() public void ContentIsNotModified_IfUnmodifiedSince_FallsbackToDateHeader()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; context.TypedRequestHeaders.IfUnmodifiedSince = utcNow;
// Verify modifications in the past succeeds // Verify modifications in the past succeeds
context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10); context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10);
Assert.True(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); Assert.True(ResponseCacheMiddleware.ContentIsNotModified(context));
Assert.Equal(1, sink.Writes.Count);
// Verify modifications at present succeeds // Verify modifications at present succeeds
context.CachedResponseHeaders.Date = utcNow; context.CachedResponseHeaders.Date = utcNow;
Assert.True(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); Assert.True(ResponseCacheMiddleware.ContentIsNotModified(context));
Assert.Equal(2, sink.Writes.Count);
// Verify modifications in the future fails // Verify modifications in the future fails
context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10);
Assert.False(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); Assert.False(ResponseCacheMiddleware.ContentIsNotModified(context));
// Verify logging
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied,
LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied);
} }
[Fact] [Fact]
public void ConditionalRequestSatisfied_IfUnmodifiedSince_LastModifiedOverridesDateHeader() public void ContentIsNotModified_IfUnmodifiedSince_LastModifiedOverridesDateHeader()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; context.TypedRequestHeaders.IfUnmodifiedSince = utcNow;
@ -142,24 +201,33 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
// Verify modifications in the past succeeds // Verify modifications in the past succeeds
context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10);
context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10);
Assert.True(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); Assert.True(ResponseCacheMiddleware.ContentIsNotModified(context));
Assert.Equal(1, sink.Writes.Count);
// Verify modifications at present // Verify modifications at present
context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10);
context.CachedResponseHeaders.LastModified = utcNow; context.CachedResponseHeaders.LastModified = utcNow;
Assert.True(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); Assert.True(ResponseCacheMiddleware.ContentIsNotModified(context));
Assert.Equal(2, sink.Writes.Count);
// Verify modifications in the future fails // Verify modifications in the future fails
context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10); context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10);
context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10);
Assert.False(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); Assert.False(ResponseCacheMiddleware.ContentIsNotModified(context));
// Verify logging
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied,
LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied);
} }
[Fact] [Fact]
public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToPass() public void ContentIsNotModified_IfNoneMatch_Overrides_IfUnmodifiedSince_ToTrue()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
// This would fail the IfUnmodifiedSince checks // This would fail the IfUnmodifiedSince checks
@ -167,14 +235,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10);
context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { EntityTagHeaderValue.Any }); context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { EntityTagHeaderValue.Any });
Assert.True(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); Assert.True(ResponseCacheMiddleware.ContentIsNotModified(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.NotModifiedIfNoneMatchStar);
} }
[Fact] [Fact]
public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToFail() public void ContentIsNotModified_IfNoneMatch_Overrides_IfUnmodifiedSince_ToFalse()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
// This would pass the IfUnmodifiedSince checks // This would pass the IfUnmodifiedSince checks
@ -182,18 +254,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10);
context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") }); context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
Assert.False(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); Assert.False(ResponseCacheMiddleware.ContentIsNotModified(context));
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public void ConditionalRequestSatisfied_IfNoneMatch_AnyWithoutETagInResponse_Passes() public void ContentIsNotModified_IfNoneMatch_AnyWithoutETagInResponse_False()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") }); context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
Assert.False(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); Assert.False(ResponseCacheMiddleware.ContentIsNotModified(context));
Assert.Empty(sink.Writes);
} }
public static TheoryData<EntityTagHeaderValue, EntityTagHeaderValue> EquivalentWeakETags public static TheoryData<EntityTagHeaderValue, EntityTagHeaderValue> EquivalentWeakETags
@ -212,9 +287,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Theory] [Theory]
[MemberData(nameof(EquivalentWeakETags))] [MemberData(nameof(EquivalentWeakETags))]
public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithMatch_Passes(EntityTagHeaderValue responseETag, EntityTagHeaderValue requestETag) public void ContentIsNotModified_IfNoneMatch_ExplicitWithMatch_True(EntityTagHeaderValue responseETag, EntityTagHeaderValue requestETag)
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
ETag = responseETag ETag = responseETag
@ -222,13 +298,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { requestETag }); context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { requestETag });
Assert.True(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); Assert.True(ResponseCacheMiddleware.ContentIsNotModified(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.NotModifiedIfNoneMatchMatched);
} }
[Fact] [Fact]
public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithoutMatch_Fails() public void ContentIsNotModified_IfNoneMatch_ExplicitWithoutMatch_False()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
ETag = new EntityTagHeaderValue("\"E2\"") ETag = new EntityTagHeaderValue("\"E2\"")
@ -236,13 +316,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") }); context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
Assert.False(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); Assert.False(ResponseCacheMiddleware.ContentIsNotModified(context));
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() public async Task FinalizeCacheHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable()
{ {
var middleware = TestUtils.CreateTestMiddleware(policyProvider: new ResponseCachePolicyProvider()); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachePolicyProvider());
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
Assert.False(context.ShouldCacheResponse); Assert.False(context.ShouldCacheResponse);
@ -251,12 +333,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
await middleware.FinalizeCacheHeadersAsync(context); await middleware.FinalizeCacheHeadersAsync(context);
Assert.False(context.ShouldCacheResponse); Assert.False(context.ShouldCacheResponse);
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable() public async Task FinalizeCacheHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable()
{ {
var middleware = TestUtils.CreateTestMiddleware(policyProvider: new ResponseCachePolicyProvider()); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachePolicyProvider());
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
@ -268,24 +352,28 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
await middleware.FinalizeCacheHeadersAsync(context); await middleware.FinalizeCacheHeadersAsync(context);
Assert.True(context.ShouldCacheResponse); Assert.True(context.ShouldCacheResponse);
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_DefaultResponseValidity_Is10Seconds() public async Task FinalizeCacheHeaders_DefaultResponseValidity_Is10Seconds()
{ {
var middleware = TestUtils.CreateTestMiddleware(); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink);
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
await middleware.FinalizeCacheHeadersAsync(context); await middleware.FinalizeCacheHeadersAsync(context);
Assert.Equal(TimeSpan.FromSeconds(10), context.CachedResponseValidFor); Assert.Equal(TimeSpan.FromSeconds(10), context.CachedResponseValidFor);
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_ResponseValidity_UseExpiryIfAvailable() public async Task FinalizeCacheHeaders_ResponseValidity_UseExpiryIfAvailable()
{ {
var utcNow = DateTimeOffset.MinValue; var utcNow = DateTimeOffset.MinValue;
var middleware = TestUtils.CreateTestMiddleware(); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink);
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.ResponseTime = utcNow; context.ResponseTime = utcNow;
@ -294,12 +382,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
await middleware.FinalizeCacheHeadersAsync(context); await middleware.FinalizeCacheHeadersAsync(context);
Assert.Equal(TimeSpan.FromSeconds(11), context.CachedResponseValidFor); Assert.Equal(TimeSpan.FromSeconds(11), context.CachedResponseValidFor);
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_ResponseValidity_UseMaxAgeIfAvailable() public async Task FinalizeCacheHeaders_ResponseValidity_UseMaxAgeIfAvailable()
{ {
var middleware = TestUtils.CreateTestMiddleware(); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink);
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
@ -311,12 +401,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
await middleware.FinalizeCacheHeadersAsync(context); await middleware.FinalizeCacheHeadersAsync(context);
Assert.Equal(TimeSpan.FromSeconds(12), context.CachedResponseValidFor); Assert.Equal(TimeSpan.FromSeconds(12), context.CachedResponseValidFor);
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_ResponseValidity_UseSharedMaxAgeIfAvailable() public async Task FinalizeCacheHeaders_ResponseValidity_UseSharedMaxAgeIfAvailable()
{ {
var middleware = TestUtils.CreateTestMiddleware(); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink);
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
@ -329,13 +421,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
await middleware.FinalizeCacheHeadersAsync(context); await middleware.FinalizeCacheHeadersAsync(context);
Assert.Equal(TimeSpan.FromSeconds(13), context.CachedResponseValidFor); Assert.Equal(TimeSpan.FromSeconds(13), context.CachedResponseValidFor);
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_IfNotEquivalentToPrevious() public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_IfNotEquivalentToPrevious()
{ {
var store = new TestResponseCacheStore(); var store = new TestResponseCacheStore();
var middleware = TestUtils.CreateTestMiddleware(store); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store);
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" }); context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" });
@ -353,13 +447,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Assert.Equal(1, store.SetCount); Assert.Equal(1, store.SetCount);
Assert.NotSame(cachedVaryByRules, context.CachedVaryByRules); Assert.NotSame(cachedVaryByRules, context.CachedVaryByRules);
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.NoResponseServed,
LoggedMessage.VaryByRulesUpdated);
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_DoNotUpdateCachedVaryByRules_IfEquivalentToPrevious() public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_IfEquivalentToPrevious()
{ {
var store = new TestResponseCacheStore(); var store = new TestResponseCacheStore();
var middleware = TestUtils.CreateTestMiddleware(store); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store);
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB" }); context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB" });
@ -379,13 +478,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
// 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
Assert.Equal(1, store.SetCount); Assert.Equal(1, store.SetCount);
Assert.Same(cachedVaryByRules, context.CachedVaryByRules); Assert.Same(cachedVaryByRules, context.CachedVaryByRules);
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.NoResponseServed,
LoggedMessage.VaryByRulesUpdated);
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_DoNotAddDate_IfSpecified() public async Task FinalizeCacheHeaders_DoNotAddDate_IfSpecified()
{ {
var utcNow = DateTimeOffset.MinValue; var utcNow = DateTimeOffset.MinValue;
var middleware = TestUtils.CreateTestMiddleware(); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink);
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.ResponseTime = utcNow; context.ResponseTime = utcNow;
@ -394,13 +498,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
await middleware.FinalizeCacheHeadersAsync(context); await middleware.FinalizeCacheHeadersAsync(context);
Assert.Equal(utcNow, context.TypedResponseHeaders.Date); Assert.Equal(utcNow, context.TypedResponseHeaders.Date);
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_AddsDate_IfNoneSpecified() public async Task FinalizeCacheHeaders_AddsDate_IfNoneSpecified()
{ {
var utcNow = DateTimeOffset.MinValue; var utcNow = DateTimeOffset.MinValue;
var middleware = TestUtils.CreateTestMiddleware(); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink);
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
context.TypedResponseHeaders.Date = utcNow; context.TypedResponseHeaders.Date = utcNow;
context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); context.ResponseTime = utcNow + TimeSpan.FromSeconds(10);
@ -410,12 +516,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
await middleware.FinalizeCacheHeadersAsync(context); await middleware.FinalizeCacheHeadersAsync(context);
Assert.Equal(utcNow, context.TypedResponseHeaders.Date); Assert.Equal(utcNow, context.TypedResponseHeaders.Date);
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_StoresCachedResponse_InState() public async Task FinalizeCacheHeaders_StoresCachedResponse_InState()
{ {
var middleware = TestUtils.CreateTestMiddleware(); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink);
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
Assert.Null(context.CachedResponse); Assert.Null(context.CachedResponse);
@ -423,12 +531,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
await middleware.FinalizeCacheHeadersAsync(context); await middleware.FinalizeCacheHeadersAsync(context);
Assert.NotNull(context.CachedResponse); Assert.NotNull(context.CachedResponse);
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public async Task FinalizeCacheHeaders_SplitsVaryHeaderByCommas() public async Task FinalizeCacheHeaders_SplitsVaryHeaderByCommas()
{ {
var middleware = TestUtils.CreateTestMiddleware(); var sink = new TestSink();
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";
@ -436,13 +546,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
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(
sink.Writes,
LoggedMessage.NoResponseServed,
LoggedMessage.VaryByRulesUpdated);
} }
[Fact] [Fact]
public async Task FinalizeCacheBody_Cache_IfContentLengthMatches() public async Task FinalizeCacheBody_Cache_IfContentLengthMatches()
{ {
var store = new TestResponseCacheStore(); var store = new TestResponseCacheStore();
var middleware = TestUtils.CreateTestMiddleware(store); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store);
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
middleware.ShimResponseStream(context); middleware.ShimResponseStream(context);
@ -457,13 +572,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
await middleware.FinalizeCacheBodyAsync(context); await middleware.FinalizeCacheBodyAsync(context);
Assert.Equal(1, store.SetCount); Assert.Equal(1, store.SetCount);
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ResponseCached);
} }
[Fact] [Fact]
public async Task FinalizeCacheBody_DoNotCache_IfContentLengthMismatches() public async Task FinalizeCacheBody_DoNotCache_IfContentLengthMismatches()
{ {
var store = new TestResponseCacheStore(); var store = new TestResponseCacheStore();
var middleware = TestUtils.CreateTestMiddleware(store); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store);
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
middleware.ShimResponseStream(context); middleware.ShimResponseStream(context);
@ -478,13 +597,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
await middleware.FinalizeCacheBodyAsync(context); await middleware.FinalizeCacheBodyAsync(context);
Assert.Equal(0, store.SetCount); Assert.Equal(0, store.SetCount);
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ResponseContentLengthMismatchNotCached);
} }
[Fact] [Fact]
public async Task FinalizeCacheBody_Cache_IfContentLengthAbsent() public async Task FinalizeCacheBody_Cache_IfContentLengthAbsent()
{ {
var store = new TestResponseCacheStore(); var store = new TestResponseCacheStore();
var middleware = TestUtils.CreateTestMiddleware(store); var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store);
var context = TestUtils.CreateTestContext(); var context = TestUtils.CreateTestContext();
middleware.ShimResponseStream(context); middleware.ShimResponseStream(context);
@ -498,6 +621,52 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
await middleware.FinalizeCacheBodyAsync(context); await middleware.FinalizeCacheBodyAsync(context);
Assert.Equal(1, store.SetCount); Assert.Equal(1, store.SetCount);
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ResponseCached);
}
[Fact]
public async Task FinalizeCacheBody_DoNotCache_IfShouldCacheResponseFalse()
{
var store = new TestResponseCacheStore();
var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store);
var context = TestUtils.CreateTestContext();
middleware.ShimResponseStream(context);
await context.HttpContext.Response.WriteAsync(new string('0', 10));
context.ShouldCacheResponse = false;
await middleware.FinalizeCacheBodyAsync(context);
Assert.Equal(0, store.SetCount);
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ResponseNotCached);
}
[Fact]
public async Task FinalizeCacheBody_DoNotCache_IfBufferingDisabled()
{
var store = new TestResponseCacheStore();
var sink = new TestSink();
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store);
var context = TestUtils.CreateTestContext();
middleware.ShimResponseStream(context);
await context.HttpContext.Response.WriteAsync(new string('0', 10));
context.ShouldCacheResponse = true;
context.ResponseCacheStream.DisableBuffering();
await middleware.FinalizeCacheBodyAsync(context);
Assert.Equal(0, store.SetCount);
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ResponseNotCached);
} }
[Fact] [Fact]

View File

@ -5,8 +5,10 @@ using System;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.Http.Headers;
using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Xunit; using Xunit;
using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.ResponseCaching.Tests namespace Microsoft.AspNetCore.ResponseCaching.Tests
{ {
@ -28,10 +30,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[MemberData(nameof(CacheableMethods))] [MemberData(nameof(CacheableMethods))]
public void IsRequestCacheable_CacheableMethods_Allowed(string method) public void IsRequestCacheable_CacheableMethods_Allowed(string method)
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Request.Method = method; context.HttpContext.Request.Method = method;
Assert.True(new ResponseCachePolicyProvider().IsRequestCacheable(context)); Assert.True(new ResponseCachePolicyProvider().IsRequestCacheable(context));
Assert.Empty(sink.Writes);
} }
public static TheoryData<string> NonCacheableMethods public static TheoryData<string> NonCacheableMethods
{ {
@ -55,26 +59,35 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[MemberData(nameof(NonCacheableMethods))] [MemberData(nameof(NonCacheableMethods))]
public void IsRequestCacheable_UncacheableMethods_NotAllowed(string method) public void IsRequestCacheable_UncacheableMethods_NotAllowed(string method)
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Request.Method = method; context.HttpContext.Request.Method = method;
Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context)); Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.RequestMethodNotCacheable);
} }
[Fact] [Fact]
public void IsRequestCacheable_AuthorizationHeaders_NotAllowed() public void IsRequestCacheable_AuthorizationHeaders_NotAllowed()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
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 ResponseCachePolicyProvider().IsRequestCacheable(context)); Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.RequestWithAuthorizationNotCacheable);
} }
[Fact] [Fact]
public void IsRequestCacheable_NoCache_NotAllowed() public void IsRequestCacheable_NoCache_NotAllowed()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Method = HttpMethods.Get;
context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
{ {
@ -82,12 +95,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}; };
Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context)); Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.RequestWithNoCacheNotCacheable);
} }
[Fact] [Fact]
public void IsRequestCacheable_NoStore_Allowed() public void IsRequestCacheable_NoStore_Allowed()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Method = HttpMethods.Get;
context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
{ {
@ -95,53 +112,67 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}; };
Assert.True(new ResponseCachePolicyProvider().IsRequestCacheable(context)); Assert.True(new ResponseCachePolicyProvider().IsRequestCacheable(context));
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public void IsRequestCacheable_LegacyDirectives_NotAllowed() public void IsRequestCacheable_LegacyDirectives_NotAllowed()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
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 ResponseCachePolicyProvider().IsRequestCacheable(context)); Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.RequestWithPragmaNoCacheNotCacheable);
} }
[Fact] [Fact]
public void IsRequestCacheable_LegacyDirectives_OverridenByCacheControl() public void IsRequestCacheable_LegacyDirectives_OverridenByCacheControl()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
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";
context.HttpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10"; context.HttpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10";
Assert.True(new ResponseCachePolicyProvider().IsRequestCacheable(context)); Assert.True(new ResponseCachePolicyProvider().IsRequestCacheable(context));
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public void IsResponseCacheable_NoPublic_NotAllowed() public void IsResponseCacheable_NoPublic_NotAllowed()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ResponseWithoutPublicNotCacheable);
} }
[Fact] [Fact]
public void IsResponseCacheable_Public_Allowed() public void IsResponseCacheable_Public_Allowed()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
Public = true Public = true
}; };
Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context));
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public void IsResponseCacheable_NoCache_NotAllowed() public void IsResponseCacheable_NoCache_NotAllowed()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
Public = true, Public = true,
@ -149,12 +180,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}; };
Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ResponseWithNoCacheNotCacheable);
} }
[Fact] [Fact]
public void IsResponseCacheable_RequestNoStore_NotAllowed() public void IsResponseCacheable_RequestNoStore_NotAllowed()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
{ {
NoStore = true NoStore = true
@ -165,12 +200,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}; };
Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ResponseWithNoStoreNotCacheable);
} }
[Fact] [Fact]
public void IsResponseCacheable_ResponseNoStore_NotAllowed() public void IsResponseCacheable_ResponseNoStore_NotAllowed()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
Public = true, Public = true,
@ -178,12 +217,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}; };
Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ResponseWithNoStoreNotCacheable);
} }
[Fact] [Fact]
public void IsResponseCacheable_SetCookieHeader_NotAllowed() public void IsResponseCacheable_SetCookieHeader_NotAllowed()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
Public = true Public = true
@ -191,12 +234,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.HttpContext.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; context.HttpContext.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue";
Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ResponseWithSetCookieNotCacheable);
} }
[Fact] [Fact]
public void IsResponseCacheable_VaryHeaderByStar_NotAllowed() public void IsResponseCacheable_VaryHeaderByStar_NotAllowed()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
Public = true Public = true
@ -204,12 +251,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.HttpContext.Response.Headers[HeaderNames.Vary] = "*"; context.HttpContext.Response.Headers[HeaderNames.Vary] = "*";
Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ResponseWithVaryStarNotCacheable);
} }
[Fact] [Fact]
public void IsResponseCacheable_Private_NotAllowed() public void IsResponseCacheable_Private_NotAllowed()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
Public = true, Public = true,
@ -217,13 +268,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}; };
Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ResponseWithPrivateNotCacheable);
} }
[Theory] [Theory]
[InlineData(StatusCodes.Status200OK)] [InlineData(StatusCodes.Status200OK)]
public void IsResponseCacheable_SuccessStatusCodes_Allowed(int statusCode) public void IsResponseCacheable_SuccessStatusCodes_Allowed(int statusCode)
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Response.StatusCode = statusCode; context.HttpContext.Response.StatusCode = statusCode;
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
@ -231,6 +286,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}; };
Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context));
Assert.Empty(sink.Writes);
} }
[Theory] [Theory]
@ -284,7 +340,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[InlineData(StatusCodes.Status507InsufficientStorage)] [InlineData(StatusCodes.Status507InsufficientStorage)]
public void IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode) public void IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode)
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Response.StatusCode = statusCode; context.HttpContext.Response.StatusCode = statusCode;
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
@ -292,12 +349,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}; };
Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ResponseWithUnsuccessfulStatusCodeNotCacheable);
} }
[Fact] [Fact]
public void IsResponseCacheable_NoExpiryRequirements_IsAllowed() public void IsResponseCacheable_NoExpiryRequirements_IsAllowed()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
@ -309,12 +370,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.ResponseTime = DateTimeOffset.MaxValue; context.ResponseTime = DateTimeOffset.MaxValue;
Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context));
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public void IsResponseCacheable_AtExpiry_NotAllowed() public void IsResponseCacheable_AtExpiry_NotAllowed()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
@ -327,13 +390,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.ResponseTime = utcNow; context.ResponseTime = utcNow;
Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ExpirationExpiresExceeded);
} }
[Fact] [Fact]
public void IsResponseCacheable_MaxAgeOverridesExpiry_ToAllowed() public void IsResponseCacheable_MaxAgeOverridesExpiry_ToAllowed()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
@ -345,13 +412,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.ResponseTime = utcNow + TimeSpan.FromSeconds(9); context.ResponseTime = utcNow + TimeSpan.FromSeconds(9);
Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context));
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public void IsResponseCacheable_MaxAgeOverridesExpiry_ToNotAllowed() public void IsResponseCacheable_MaxAgeOverridesExpiry_ToNotAllowed()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
@ -363,13 +432,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); context.ResponseTime = utcNow + TimeSpan.FromSeconds(10);
Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ExpirationMaxAgeExceeded);
} }
[Fact] [Fact]
public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed() public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
@ -381,13 +454,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); context.ResponseTime = utcNow + TimeSpan.FromSeconds(11);
Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context));
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToNotAllowed() public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToNotAllowed()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
{ {
@ -399,25 +474,31 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.ResponseTime = utcNow + TimeSpan.FromSeconds(5); context.ResponseTime = utcNow + TimeSpan.FromSeconds(5);
Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ExpirationSharedMaxAgeExceeded);
} }
[Fact] [Fact]
public void IsCachedEntryFresh_NoCachedCacheControl_FallsbackToEmptyCacheControl() public void IsCachedEntryFresh_NoCachedCacheControl_FallsbackToEmptyCacheControl()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.ResponseTime = DateTimeOffset.MaxValue; context.ResponseTime = DateTimeOffset.MaxValue;
context.CachedEntryAge = TimeSpan.MaxValue; context.CachedEntryAge = TimeSpan.MaxValue;
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context));
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public void IsCachedEntryFresh_NoExpiryRequirements_IsFresh() public void IsCachedEntryFresh_NoExpiryRequirements_IsFresh()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.ResponseTime = DateTimeOffset.MaxValue; context.ResponseTime = DateTimeOffset.MaxValue;
context.CachedEntryAge = TimeSpan.MaxValue; context.CachedEntryAge = TimeSpan.MaxValue;
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
@ -429,13 +510,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}; };
Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context));
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public void IsCachedEntryFresh_AtExpiry_IsNotFresh() public void IsCachedEntryFresh_AtExpiry_IsNotFresh()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.ResponseTime = utcNow; context.ResponseTime = utcNow;
context.CachedEntryAge = TimeSpan.Zero; context.CachedEntryAge = TimeSpan.Zero;
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
@ -448,13 +531,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}; };
Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ExpirationExpiresExceeded);
} }
[Fact] [Fact]
public void IsCachedEntryFresh_MaxAgeOverridesExpiry_ToFresh() public void IsCachedEntryFresh_MaxAgeOverridesExpiry_ToFresh()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.CachedEntryAge = TimeSpan.FromSeconds(9); context.CachedEntryAge = TimeSpan.FromSeconds(9);
context.ResponseTime = utcNow + context.CachedEntryAge; context.ResponseTime = utcNow + context.CachedEntryAge;
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
@ -468,13 +555,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}; };
Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context));
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public void IsCachedEntryFresh_MaxAgeOverridesExpiry_ToNotFresh() public void IsCachedEntryFresh_MaxAgeOverridesExpiry_ToNotFresh()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.CachedEntryAge = TimeSpan.FromSeconds(10); context.CachedEntryAge = TimeSpan.FromSeconds(10);
context.ResponseTime = utcNow + context.CachedEntryAge; context.ResponseTime = utcNow + context.CachedEntryAge;
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
@ -488,13 +577,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}; };
Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ExpirationMaxAgeExceeded);
} }
[Fact] [Fact]
public void IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToFresh() public void IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToFresh()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.CachedEntryAge = TimeSpan.FromSeconds(11); context.CachedEntryAge = TimeSpan.FromSeconds(11);
context.ResponseTime = utcNow + context.CachedEntryAge; context.ResponseTime = utcNow + context.CachedEntryAge;
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
@ -509,13 +602,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}; };
Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context));
Assert.Empty(sink.Writes);
} }
[Fact] [Fact]
public void IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh() public void IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh()
{ {
var utcNow = DateTimeOffset.UtcNow; var utcNow = DateTimeOffset.UtcNow;
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.CachedEntryAge = TimeSpan.FromSeconds(5); context.CachedEntryAge = TimeSpan.FromSeconds(5);
context.ResponseTime = utcNow + context.CachedEntryAge; context.ResponseTime = utcNow + context.CachedEntryAge;
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
@ -530,12 +625,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}; };
Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ExpirationSharedMaxAgeExceeded);
} }
[Fact] [Fact]
public void IsCachedEntryFresh_MinFreshReducesFreshness_ToNotFresh() public void IsCachedEntryFresh_MinFreshReducesFreshness_ToNotFresh()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
{ {
MinFresh = TimeSpan.FromSeconds(2) MinFresh = TimeSpan.FromSeconds(2)
@ -551,12 +650,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.CachedEntryAge = TimeSpan.FromSeconds(3); context.CachedEntryAge = TimeSpan.FromSeconds(3);
Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ExpirationMinFreshAdded,
LoggedMessage.ExpirationSharedMaxAgeExceeded);
} }
[Fact] [Fact]
public void IsCachedEntryFresh_RequestMaxAgeRestrictAge_ToNotFresh() public void IsCachedEntryFresh_RequestMaxAgeRestrictAge_ToNotFresh()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
{ {
MaxAge = TimeSpan.FromSeconds(5) MaxAge = TimeSpan.FromSeconds(5)
@ -571,17 +675,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.CachedEntryAge = TimeSpan.FromSeconds(5); context.CachedEntryAge = TimeSpan.FromSeconds(5);
Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ExpirationMaxAgeExceeded);
} }
[Fact] [Fact]
public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ToFresh() public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ToFresh()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
{ {
MaxAge = TimeSpan.FromSeconds(5), MaxAge = TimeSpan.FromSeconds(5),
MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit
MaxStaleLimit = TimeSpan.FromSeconds(10) MaxStaleLimit = TimeSpan.FromSeconds(2)
}; };
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
@ -593,17 +701,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.CachedEntryAge = TimeSpan.FromSeconds(6); context.CachedEntryAge = TimeSpan.FromSeconds(6);
Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ExpirationMaxStaleSatisfied);
} }
[Fact] [Fact]
public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ButStillNotFresh() public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ButStillNotFresh()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
{ {
MaxAge = TimeSpan.FromSeconds(5), MaxAge = TimeSpan.FromSeconds(5),
MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit
MaxStaleLimit = TimeSpan.FromSeconds(6) MaxStaleLimit = TimeSpan.FromSeconds(1)
}; };
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
@ -615,17 +727,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.CachedEntryAge = TimeSpan.FromSeconds(6); context.CachedEntryAge = TimeSpan.FromSeconds(6);
Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ExpirationMaxAgeExceeded);
} }
[Fact] [Fact]
public void IsCachedEntryFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh() public void IsCachedEntryFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh()
{ {
var context = TestUtils.CreateTestContext(); var sink = new TestSink();
var context = TestUtils.CreateTestContext(sink);
context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
{ {
MaxAge = TimeSpan.FromSeconds(5), MaxAge = TimeSpan.FromSeconds(5),
MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit
MaxStaleLimit = TimeSpan.FromSeconds(10) MaxStaleLimit = TimeSpan.FromSeconds(2)
}; };
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
{ {
@ -638,6 +754,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.CachedEntryAge = TimeSpan.FromSeconds(6); context.CachedEntryAge = TimeSpan.FromSeconds(6);
Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context));
TestUtils.AssertLoggedMessages(
sink.Writes,
LoggedMessage.ExpirationMustRevalidate);
} }
} }
} }

View File

@ -12,10 +12,13 @@ using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using Xunit;
namespace Microsoft.AspNetCore.ResponseCaching.Tests namespace Microsoft.AspNetCore.ResponseCaching.Tests
{ {
@ -86,6 +89,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
internal static ResponseCacheMiddleware CreateTestMiddleware( internal static ResponseCacheMiddleware CreateTestMiddleware(
IResponseCacheStore store = null, IResponseCacheStore store = null,
ResponseCacheOptions options = null, ResponseCacheOptions options = null,
TestSink testSink = null,
IResponseCacheKeyProvider keyProvider = null, IResponseCacheKeyProvider keyProvider = null,
IResponseCachePolicyProvider policyProvider = null) IResponseCachePolicyProvider policyProvider = null)
{ {
@ -109,6 +113,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
return new ResponseCacheMiddleware( return new ResponseCacheMiddleware(
httpContext => TaskCache.CompletedTask, httpContext => TaskCache.CompletedTask,
Options.Create(options), Options.Create(options),
testSink == null ? (ILoggerFactory)NullLoggerFactory.Instance : new TestLoggerFactory(testSink, true),
policyProvider, policyProvider,
store, store,
keyProvider); keyProvider);
@ -116,11 +121,70 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
internal static ResponseCacheContext CreateTestContext() internal static ResponseCacheContext CreateTestContext()
{ {
return new ResponseCacheContext(new DefaultHttpContext()) return new ResponseCacheContext(new DefaultHttpContext(), NullLogger.Instance)
{ {
ResponseTime = DateTimeOffset.UtcNow ResponseTime = DateTimeOffset.UtcNow
}; };
} }
internal static ResponseCacheContext CreateTestContext(ITestSink testSink)
{
return new ResponseCacheContext(new DefaultHttpContext(), new TestLogger("ResponseCachingTests", testSink, true))
{
ResponseTime = DateTimeOffset.UtcNow
};
}
internal static void AssertLoggedMessages(List<WriteContext> messages, params LoggedMessage[] expectedMessages)
{
Assert.Equal(messages.Count, expectedMessages.Length);
for (var i = 0; i < messages.Count; i++)
{
Assert.Equal(expectedMessages[i].EventId, messages[i].EventId);
Assert.Equal(expectedMessages[i].LogLevel, messages[i].LogLevel);
}
}
}
internal class LoggedMessage
{
internal static LoggedMessage RequestMethodNotCacheable => new LoggedMessage(1, LogLevel.Debug);
internal static LoggedMessage RequestWithAuthorizationNotCacheable => new LoggedMessage(2, LogLevel.Debug);
internal static LoggedMessage RequestWithNoCacheNotCacheable => new LoggedMessage(3, LogLevel.Debug);
internal static LoggedMessage RequestWithPragmaNoCacheNotCacheable => new LoggedMessage(4, LogLevel.Debug);
internal static LoggedMessage ExpirationMinFreshAdded => new LoggedMessage(5, LogLevel.Debug);
internal static LoggedMessage ExpirationSharedMaxAgeExceeded => new LoggedMessage(6, LogLevel.Debug);
internal static LoggedMessage ExpirationMustRevalidate => new LoggedMessage(7, LogLevel.Debug);
internal static LoggedMessage ExpirationMaxStaleSatisfied => new LoggedMessage(8, LogLevel.Debug);
internal static LoggedMessage ExpirationMaxAgeExceeded => new LoggedMessage(9, LogLevel.Debug);
internal static LoggedMessage ExpirationExpiresExceeded => new LoggedMessage(10, LogLevel.Debug);
internal static LoggedMessage ResponseWithoutPublicNotCacheable => new LoggedMessage(11, LogLevel.Debug);
internal static LoggedMessage ResponseWithNoStoreNotCacheable => new LoggedMessage(12, LogLevel.Debug);
internal static LoggedMessage ResponseWithNoCacheNotCacheable => new LoggedMessage(13, LogLevel.Debug);
internal static LoggedMessage ResponseWithSetCookieNotCacheable => new LoggedMessage(14, LogLevel.Debug);
internal static LoggedMessage ResponseWithVaryStarNotCacheable => new LoggedMessage(15, LogLevel.Debug);
internal static LoggedMessage ResponseWithPrivateNotCacheable => new LoggedMessage(16, LogLevel.Debug);
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 NotModifiedServed => new LoggedMessage(21, LogLevel.Information);
internal static LoggedMessage CachedResponseServed => new LoggedMessage(22, LogLevel.Information);
internal static LoggedMessage GatewayTimeoutServed => new LoggedMessage(23, LogLevel.Information);
internal static LoggedMessage NoResponseServed => new LoggedMessage(24, LogLevel.Information);
internal static LoggedMessage VaryByRulesUpdated => new LoggedMessage(25, LogLevel.Debug);
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);
private LoggedMessage(int evenId, LogLevel logLevel)
{
EventId = evenId;
LogLevel = logLevel;
}
internal int EventId { get; }
internal LogLevel LogLevel { get; }
} }
internal class DummySendFileFeature : IHttpSendFileFeature internal class DummySendFileFeature : IHttpSendFileFeature

View File

@ -7,6 +7,7 @@
"dotnet-test-xunit": "2.2.0-*", "dotnet-test-xunit": "2.2.0-*",
"Microsoft.AspNetCore.ResponseCaching": "0.1.0-*", "Microsoft.AspNetCore.ResponseCaching": "0.1.0-*",
"Microsoft.AspNetCore.TestHost": "1.1.0-*", "Microsoft.AspNetCore.TestHost": "1.1.0-*",
"Microsoft.Extensions.Logging.Testing": "1.1.0-*",
"xunit": "2.2.0-*" "xunit": "2.2.0-*"
}, },
"frameworks": { "frameworks": {