API review renames and updates
This commit is contained in:
parent
e236e64055
commit
ccfa090e6e
|
|
@ -146,7 +146,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
param[index] = reader.ReadString();
|
param[index] = reader.ReadString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new CachedVaryRules { VaryKeyPrefix = varyKeyPrefix, VaryRules = new VaryRules() { Headers = headers, Params = param } };
|
return new CachedVaryRules { VaryKeyPrefix = varyKeyPrefix, Headers = headers, Params = param };
|
||||||
}
|
}
|
||||||
|
|
||||||
// See serialization format above
|
// See serialization format above
|
||||||
|
|
@ -222,14 +222,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
{
|
{
|
||||||
writer.Write(varyRules.VaryKeyPrefix);
|
writer.Write(varyRules.VaryKeyPrefix);
|
||||||
|
|
||||||
writer.Write(varyRules.VaryRules.Headers.Count);
|
writer.Write(varyRules.Headers.Count);
|
||||||
foreach (var header in varyRules.VaryRules.Headers)
|
foreach (var header in varyRules.Headers)
|
||||||
{
|
{
|
||||||
writer.Write(header);
|
writer.Write(header);
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.Write(varyRules.VaryRules.Params.Count);
|
writer.Write(varyRules.Params.Count);
|
||||||
foreach (var param in varyRules.VaryRules.Params)
|
foreach (var param in varyRules.Params)
|
||||||
{
|
{
|
||||||
writer.Write(param);
|
writer.Write(param);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,18 @@
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
{
|
{
|
||||||
internal class CachedResponse
|
public class CachedResponse
|
||||||
{
|
{
|
||||||
internal string BodyKeyPrefix { get; set; }
|
public string BodyKeyPrefix { get; internal set; }
|
||||||
|
|
||||||
internal DateTimeOffset Created { get; set; }
|
public DateTimeOffset Created { get; internal set; }
|
||||||
|
|
||||||
internal int StatusCode { get; set; }
|
public int StatusCode { get; internal set; }
|
||||||
|
|
||||||
internal IHeaderDictionary Headers { get; set; } = new HeaderDictionary();
|
public IHeaderDictionary Headers { get; internal set; } = new HeaderDictionary();
|
||||||
|
|
||||||
internal byte[] Body { get; set; }
|
public byte[] Body { get; internal set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
{
|
{
|
||||||
internal class CachedResponseBody
|
public class CachedResponseBody
|
||||||
{
|
{
|
||||||
internal byte[] Body { get; set; }
|
public byte[] Body { get; internal set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
using Microsoft.Extensions.Primitives;
|
||||||
{
|
|
||||||
internal class CachedVaryRules
|
|
||||||
{
|
|
||||||
internal string VaryKeyPrefix { get; set; }
|
|
||||||
|
|
||||||
internal VaryRules VaryRules { get; set; }
|
namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
|
{
|
||||||
|
public class CachedVaryRules
|
||||||
|
{
|
||||||
|
public string VaryKeyPrefix { get; internal set; }
|
||||||
|
|
||||||
|
public StringValues Headers { get; internal set; }
|
||||||
|
|
||||||
|
public StringValues Params { get; internal set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,13 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.ObjectPool;
|
using Microsoft.Extensions.ObjectPool;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching
|
namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
{
|
{
|
||||||
public class KeyProvider : IKeyProvider
|
public class CacheKeyProvider : ICacheKeyProvider
|
||||||
{
|
{
|
||||||
// Use the record separator for delimiting components of the cache key to avoid possible collisions
|
// Use the record separator for delimiting components of the cache key to avoid possible collisions
|
||||||
private static readonly char KeyDelimiter = '\x1e';
|
private static readonly char KeyDelimiter = '\x1e';
|
||||||
|
|
@ -21,7 +20,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
private readonly ObjectPool<StringBuilder> _builderPool;
|
private readonly ObjectPool<StringBuilder> _builderPool;
|
||||||
private readonly ResponseCachingOptions _options;
|
private readonly ResponseCachingOptions _options;
|
||||||
|
|
||||||
public KeyProvider(ObjectPoolProvider poolProvider, IOptions<ResponseCachingOptions> options)
|
public CacheKeyProvider(ObjectPoolProvider poolProvider, IOptions<ResponseCachingOptions> options)
|
||||||
{
|
{
|
||||||
if (poolProvider == null)
|
if (poolProvider == null)
|
||||||
{
|
{
|
||||||
|
|
@ -36,26 +35,25 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual IEnumerable<string> CreateLookupBaseKey(HttpContext httpContext)
|
public virtual IEnumerable<string> CreateLookupBaseKeys(ResponseCachingContext context)
|
||||||
{
|
{
|
||||||
return new string[] { CreateStorageBaseKey(httpContext) };
|
return new string[] { CreateStorageBaseKey(context) };
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual IEnumerable<string> CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules)
|
public virtual IEnumerable<string> CreateLookupVaryKeys(ResponseCachingContext context)
|
||||||
{
|
{
|
||||||
return new string[] { CreateStorageVaryKey(httpContext, varyRules) };
|
return new string[] { CreateStorageVaryKey(context) };
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET<delimiter>/PATH
|
// GET<delimiter>/PATH
|
||||||
// TODO: Method invariant retrieval? E.g. HEAD after GET to the same resource.
|
public virtual string CreateStorageBaseKey(ResponseCachingContext context)
|
||||||
public virtual string CreateStorageBaseKey(HttpContext httpContext)
|
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
if (context == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
throw new ArgumentNullException(nameof(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
var request = httpContext.Request;
|
var request = context.HttpContext.Request;
|
||||||
var builder = _builderPool.Get();
|
var builder = _builderPool.Get();
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
@ -74,24 +72,31 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
}
|
}
|
||||||
|
|
||||||
// BaseKey<delimiter>H<delimiter>HeaderName=HeaderValue<delimiter>Q<delimiter>QueryName=QueryValue
|
// BaseKey<delimiter>H<delimiter>HeaderName=HeaderValue<delimiter>Q<delimiter>QueryName=QueryValue
|
||||||
public virtual string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules)
|
public virtual string CreateStorageVaryKey(ResponseCachingContext context)
|
||||||
{
|
{
|
||||||
if (httpContext == null)
|
if (context == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(httpContext));
|
throw new ArgumentNullException(nameof(context));
|
||||||
}
|
|
||||||
if (varyRules == null || (StringValues.IsNullOrEmpty(varyRules.Headers) && StringValues.IsNullOrEmpty(varyRules.Params)))
|
|
||||||
{
|
|
||||||
return httpContext.GetResponseCachingState().CachedVaryRules.VaryKeyPrefix;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var request = httpContext.Request;
|
var varyRules = context.CachedVaryRules;
|
||||||
|
if (varyRules == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{nameof(CachedVaryRules)} must not be null on the {nameof(ResponseCachingContext)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((StringValues.IsNullOrEmpty(varyRules.Headers) && StringValues.IsNullOrEmpty(varyRules.Params)))
|
||||||
|
{
|
||||||
|
return varyRules.VaryKeyPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = context.HttpContext.Request;
|
||||||
var builder = _builderPool.Get();
|
var builder = _builderPool.Get();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Prepend with the Guid of the CachedVaryRules
|
// Prepend with the Guid of the CachedVaryRules
|
||||||
builder.Append(httpContext.GetResponseCachingState().CachedVaryRules.VaryKeyPrefix);
|
builder.Append(varyRules.VaryKeyPrefix);
|
||||||
|
|
||||||
// Vary by headers
|
// Vary by headers
|
||||||
if (varyRules?.Headers.Count > 0)
|
if (varyRules?.Headers.Count > 0)
|
||||||
|
|
@ -102,18 +107,11 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
|
|
||||||
foreach (var header in varyRules.Headers)
|
foreach (var header in varyRules.Headers)
|
||||||
{
|
{
|
||||||
var value = httpContext.Request.Headers[header];
|
|
||||||
|
|
||||||
// TODO: How to handle null/empty string?
|
|
||||||
if (StringValues.IsNullOrEmpty(value))
|
|
||||||
{
|
|
||||||
value = "null";
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.Append(KeyDelimiter)
|
builder.Append(KeyDelimiter)
|
||||||
.Append(header)
|
.Append(header)
|
||||||
.Append("=")
|
.Append("=")
|
||||||
.Append(value);
|
// TODO: Perf - iterate the string values instead?
|
||||||
|
.Append(context.HttpContext.Request.Headers[header]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -127,7 +125,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
if (varyRules.Params.Count == 1 && string.Equals(varyRules.Params[0], "*", StringComparison.Ordinal))
|
if (varyRules.Params.Count == 1 && string.Equals(varyRules.Params[0], "*", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
// Vary by all available query params
|
// Vary by all available query params
|
||||||
foreach (var query in httpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase))
|
foreach (var query in context.HttpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
builder.Append(KeyDelimiter)
|
builder.Append(KeyDelimiter)
|
||||||
.Append(query.Key.ToUpperInvariant())
|
.Append(query.Key.ToUpperInvariant())
|
||||||
|
|
@ -139,18 +137,11 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
{
|
{
|
||||||
foreach (var param in varyRules.Params)
|
foreach (var param in varyRules.Params)
|
||||||
{
|
{
|
||||||
var value = httpContext.Request.Query[param];
|
|
||||||
|
|
||||||
// TODO: How to handle null/empty string?
|
|
||||||
if (StringValues.IsNullOrEmpty(value))
|
|
||||||
{
|
|
||||||
value = "null";
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.Append(KeyDelimiter)
|
builder.Append(KeyDelimiter)
|
||||||
.Append(param)
|
.Append(param)
|
||||||
.Append("=")
|
.Append("=")
|
||||||
.Append(value);
|
// TODO: Perf - iterate the string values instead?
|
||||||
|
.Append(context.HttpContext.Request.Query[param]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -13,13 +13,11 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
{
|
{
|
||||||
private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue();
|
private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue();
|
||||||
|
|
||||||
public virtual bool RequestIsCacheable(HttpContext httpContext)
|
public virtual bool IsRequestCacheable(ResponseCachingContext context)
|
||||||
{
|
{
|
||||||
var state = httpContext.GetResponseCachingState();
|
|
||||||
|
|
||||||
// Verify the method
|
// Verify the method
|
||||||
// TODO: RFC lists POST as a cacheable method when explicit freshness information is provided, but this is not widely implemented. Will revisit.
|
// TODO: RFC lists POST as a cacheable method when explicit freshness information is provided, but this is not widely implemented. Will revisit.
|
||||||
var request = httpContext.Request;
|
var request = context.HttpContext.Request;
|
||||||
if (!string.Equals("GET", request.Method, StringComparison.OrdinalIgnoreCase) &&
|
if (!string.Equals("GET", request.Method, StringComparison.OrdinalIgnoreCase) &&
|
||||||
!string.Equals("HEAD", request.Method, StringComparison.OrdinalIgnoreCase))
|
!string.Equals("HEAD", request.Method, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
|
@ -37,7 +35,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
// TODO: no-cache requests can be retrieved upon validation with origin
|
// TODO: no-cache requests can be retrieved upon validation with origin
|
||||||
if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl]))
|
if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl]))
|
||||||
{
|
{
|
||||||
if (state.RequestCacheControl.NoCache)
|
if (context.RequestCacheControlHeaderValue.NoCache)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -59,31 +57,29 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual bool ResponseIsCacheable(HttpContext httpContext)
|
public virtual bool IsResponseCacheable(ResponseCachingContext context)
|
||||||
{
|
{
|
||||||
var state = httpContext.GetResponseCachingState();
|
|
||||||
|
|
||||||
// Only cache pages explicitly marked with public
|
// Only cache pages explicitly marked with public
|
||||||
// TODO: Consider caching responses that are not marked as public but otherwise cacheable?
|
// TODO: Consider caching responses that are not marked as public but otherwise cacheable?
|
||||||
if (!state.ResponseCacheControl.Public)
|
if (!context.ResponseCacheControlHeaderValue.Public)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check no-store
|
// Check no-store
|
||||||
if (state.RequestCacheControl.NoStore || state.ResponseCacheControl.NoStore)
|
if (context.RequestCacheControlHeaderValue.NoStore || context.ResponseCacheControlHeaderValue.NoStore)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check no-cache
|
// Check no-cache
|
||||||
// TODO: Handle no-cache with headers
|
// TODO: Handle no-cache with headers
|
||||||
if (state.ResponseCacheControl.NoCache)
|
if (context.ResponseCacheControlHeaderValue.NoCache)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = httpContext.Response;
|
var response = context.HttpContext.Response;
|
||||||
|
|
||||||
// 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]))
|
||||||
|
|
@ -101,7 +97,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
// TODO: public MAY override the cacheability checks for private and status codes
|
// TODO: public MAY override the cacheability checks for private and status codes
|
||||||
|
|
||||||
// Check private
|
// Check private
|
||||||
if (state.ResponseCacheControl.Private)
|
if (context.ResponseCacheControlHeaderValue.Private)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -115,35 +111,35 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
|
|
||||||
// Check response freshness
|
// Check response freshness
|
||||||
// TODO: apparent age vs corrected age value
|
// TODO: apparent age vs corrected age value
|
||||||
if (state.ResponseHeaders.Date == null)
|
if (context.TypedResponseHeaders.Date == null)
|
||||||
{
|
{
|
||||||
if (state.ResponseCacheControl.SharedMaxAge == null &&
|
if (context.ResponseCacheControlHeaderValue.SharedMaxAge == null &&
|
||||||
state.ResponseCacheControl.MaxAge == null &&
|
context.ResponseCacheControlHeaderValue.MaxAge == null &&
|
||||||
state.ResponseTime > state.ResponseHeaders.Expires)
|
context.ResponseTime > context.TypedResponseHeaders.Expires)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var age = state.ResponseTime - state.ResponseHeaders.Date.Value;
|
var age = context.ResponseTime - context.TypedResponseHeaders.Date.Value;
|
||||||
|
|
||||||
// Validate shared max age
|
// Validate shared max age
|
||||||
if (age > state.ResponseCacheControl.SharedMaxAge)
|
if (age > context.ResponseCacheControlHeaderValue.SharedMaxAge)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else if (state.ResponseCacheControl.SharedMaxAge == null)
|
else if (context.ResponseCacheControlHeaderValue.SharedMaxAge == null)
|
||||||
{
|
{
|
||||||
// Validate max age
|
// Validate max age
|
||||||
if (age > state.ResponseCacheControl.MaxAge)
|
if (age > context.ResponseCacheControlHeaderValue.MaxAge)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else if (state.ResponseCacheControl.MaxAge == null)
|
else if (context.ResponseCacheControlHeaderValue.MaxAge == null)
|
||||||
{
|
{
|
||||||
// Validate expiration
|
// Validate expiration
|
||||||
if (state.ResponseTime > state.ResponseHeaders.Expires)
|
if (context.ResponseTime > context.TypedResponseHeaders.Expires)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -154,16 +150,15 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual bool CachedEntryIsFresh(HttpContext httpContext, ResponseHeaders cachedResponseHeaders)
|
public virtual bool IsCachedEntryFresh(ResponseCachingContext context)
|
||||||
{
|
{
|
||||||
var state = httpContext.GetResponseCachingState();
|
var age = context.CachedEntryAge;
|
||||||
var age = state.CachedEntryAge;
|
var cachedControlHeaders = context.CachedResponseHeaders.CacheControl ?? EmptyCacheControl;
|
||||||
var cachedControlHeaders = cachedResponseHeaders.CacheControl ?? EmptyCacheControl;
|
|
||||||
|
|
||||||
// Add min-fresh requirements
|
// Add min-fresh requirements
|
||||||
if (state.RequestCacheControl.MinFresh != null)
|
if (context.RequestCacheControlHeaderValue.MinFresh != null)
|
||||||
{
|
{
|
||||||
age += state.RequestCacheControl.MinFresh.Value;
|
age += context.RequestCacheControlHeaderValue.MinFresh.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate shared max age, this overrides any max age settings for shared caches
|
// Validate shared max age, this overrides any max age settings for shared caches
|
||||||
|
|
@ -175,7 +170,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
else if (cachedControlHeaders.SharedMaxAge == null)
|
else if (cachedControlHeaders.SharedMaxAge == null)
|
||||||
{
|
{
|
||||||
// Validate max age
|
// Validate max age
|
||||||
if (age > cachedControlHeaders.MaxAge || age > state.RequestCacheControl.MaxAge)
|
if (age > cachedControlHeaders.MaxAge || age > context.RequestCacheControlHeaderValue.MaxAge)
|
||||||
{
|
{
|
||||||
// Must revalidate
|
// Must revalidate
|
||||||
if (cachedControlHeaders.MustRevalidate)
|
if (cachedControlHeaders.MustRevalidate)
|
||||||
|
|
@ -184,7 +179,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request allows stale values
|
// Request allows stale values
|
||||||
if (age < state.RequestCacheControl.MaxStaleLimit)
|
if (age < context.RequestCacheControlHeaderValue.MaxStaleLimit)
|
||||||
{
|
{
|
||||||
// TODO: Add warning header indicating the response is stale
|
// TODO: Add warning header indicating the response is stale
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -192,10 +187,10 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else if (cachedControlHeaders.MaxAge == null && state.RequestCacheControl.MaxAge == null)
|
else if (cachedControlHeaders.MaxAge == null && context.RequestCacheControlHeaderValue.MaxAge == null)
|
||||||
{
|
{
|
||||||
// Validate expiration
|
// Validate expiration
|
||||||
if (state.ResponseTime > cachedResponseHeaders.Expires)
|
if (context.ResponseTime > context.CachedResponseHeaders.Expires)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,12 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.ResponseCaching.Internal;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching
|
namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
{
|
{
|
||||||
// TODO: Temporary interface for endpoints to specify options for response caching
|
// TODO: Temporary interface for endpoints to specify options for response caching
|
||||||
public static class ResponseCachingHttpContextExtensions
|
public static class ResponseCachingHttpContextExtensions
|
||||||
{
|
{
|
||||||
public static ResponseCachingState GetResponseCachingState(this HttpContext httpContext)
|
|
||||||
{
|
|
||||||
return httpContext.Features.Get<ResponseCachingState>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ResponseCachingFeature GetResponseCachingFeature(this HttpContext httpContext)
|
public static ResponseCachingFeature GetResponseCachingFeature(this HttpContext httpContext)
|
||||||
{
|
{
|
||||||
return httpContext.Features.Get<ResponseCachingFeature>();
|
return httpContext.Features.Get<ResponseCachingFeature>();
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
|
|
||||||
private static IServiceCollection AddResponseCachingServices(this IServiceCollection services)
|
private static IServiceCollection AddResponseCachingServices(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.TryAdd(ServiceDescriptor.Singleton<IKeyProvider, KeyProvider>());
|
services.TryAdd(ServiceDescriptor.Singleton<ICacheKeyProvider, CacheKeyProvider>());
|
||||||
services.TryAdd(ServiceDescriptor.Singleton<ICacheabilityValidator, CacheabilityValidator>());
|
services.TryAdd(ServiceDescriptor.Singleton<ICacheabilityValidator, CacheabilityValidator>());
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
|
{
|
||||||
|
public interface ICacheKeyProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a base key for storing items.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
|
||||||
|
/// <returns>The created base key.</returns>
|
||||||
|
string CreateStorageBaseKey(ResponseCachingContext context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create one or more base keys for looking up items.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
|
||||||
|
/// <returns>An ordered <see cref="IEnumerable{T}"/> containing the base keys to try when looking up items.</returns>
|
||||||
|
IEnumerable<string> CreateLookupBaseKeys(ResponseCachingContext context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a vary key for storing items.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
|
||||||
|
/// <returns>The created vary key.</returns>
|
||||||
|
string CreateStorageVaryKey(ResponseCachingContext context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create one or more vary keys for looking up items.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
|
||||||
|
/// <returns>An ordered <see cref="IEnumerable{T}"/> containing the vary keys to try when looking up items.</returns>
|
||||||
|
IEnumerable<string> CreateLookupVaryKeys(ResponseCachingContext context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Http.Headers;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching
|
namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
{
|
{
|
||||||
public interface ICacheabilityValidator
|
public interface ICacheabilityValidator
|
||||||
|
|
@ -11,23 +8,22 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determine the cacheability of an HTTP request.
|
/// Determine the cacheability of an HTTP request.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
|
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
|
||||||
/// <returns><c>true</c> if the request is cacheable; otherwise <c>false</c>.</returns>
|
/// <returns><c>true</c> if the request is cacheable; otherwise <c>false</c>.</returns>
|
||||||
bool RequestIsCacheable(HttpContext httpContext);
|
bool IsRequestCacheable(ResponseCachingContext context);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determine the cacheability of an HTTP response.
|
/// Determine the cacheability of an HTTP response.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
|
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
|
||||||
/// <returns><c>true</c> if the response is cacheable; otherwise <c>false</c>.</returns>
|
/// <returns><c>true</c> if the response is cacheable; otherwise <c>false</c>.</returns>
|
||||||
bool ResponseIsCacheable(HttpContext httpContext);
|
bool IsResponseCacheable(ResponseCachingContext context);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determine the freshness of the cached entry.
|
/// Determine the freshness of the cached entry.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
|
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
|
||||||
/// <param name="cachedResponseHeaders">The <see cref="ResponseHeaders"/> of the cached entry.</param>
|
|
||||||
/// <returns><c>true</c> if the cached entry is fresh; otherwise <c>false</c>.</returns>
|
/// <returns><c>true</c> if the cached entry is fresh; otherwise <c>false</c>.</returns>
|
||||||
bool CachedEntryIsFresh(HttpContext httpContext, ResponseHeaders cachedResponseHeaders);
|
bool IsCachedEntryFresh(ResponseCachingContext context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching
|
|
||||||
{
|
|
||||||
public interface IKeyProvider
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Create a base key using the HTTP context for storing items.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
|
|
||||||
/// <returns>The created base key.</returns>
|
|
||||||
string CreateStorageBaseKey(HttpContext httpContext);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create one or more base keys using the HTTP context for looking up items.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
|
|
||||||
/// <returns>An ordered <see cref="IEnumerable{T}"/> containing the base keys to try when looking up items.</returns>
|
|
||||||
IEnumerable<string> CreateLookupBaseKey(HttpContext httpContext);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a vary key using the HTTP context and vary rules for storing items.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
|
|
||||||
/// <param name="varyRules">The <see cref="VaryRules"/>.</param>
|
|
||||||
/// <returns>The created vary key.</returns>
|
|
||||||
string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create one or more vary keys using the HTTP context and vary rules for looking up items.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
|
|
||||||
/// <param name="varyRules">The <see cref="VaryRules"/>.</param>
|
|
||||||
/// <returns>An ordered <see cref="IEnumerable{T}"/> containing the vary keys to try when looking up items.</returns>
|
|
||||||
IEnumerable<string> CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
{
|
{
|
||||||
internal static class HttpContextInternalExtensions
|
internal static class InternalHttpContextExtensions
|
||||||
{
|
{
|
||||||
internal static void AddResponseCachingFeature(this HttpContext httpContext)
|
internal static void AddResponseCachingFeature(this HttpContext httpContext)
|
||||||
{
|
{
|
||||||
|
|
@ -21,24 +21,5 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
{
|
{
|
||||||
httpContext.Features.Set<ResponseCachingFeature>(null);
|
httpContext.Features.Set<ResponseCachingFeature>(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void AddResponseCachingState(this HttpContext httpContext)
|
|
||||||
{
|
|
||||||
if (httpContext.GetResponseCachingState() != null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"Another instance of {nameof(ResponseCachingState)} already exists. Only one instance of {nameof(ResponseCachingMiddleware)} can be configured for an application.");
|
|
||||||
}
|
|
||||||
httpContext.Features.Set(new ResponseCachingState(httpContext));
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static void RemoveResponseCachingState(this HttpContext httpContext)
|
|
||||||
{
|
|
||||||
httpContext.Features.Set<ResponseCachingState>(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static ResponseCachingState GetResponseCachingState(this HttpContext httpContext)
|
|
||||||
{
|
|
||||||
return httpContext.Features.Get<ResponseCachingState>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Http.Headers;
|
|
||||||
using Microsoft.Net.Http.Headers;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
|
||||||
{
|
|
||||||
public class ResponseCachingState
|
|
||||||
{
|
|
||||||
private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue();
|
|
||||||
|
|
||||||
private readonly HttpContext _httpContext;
|
|
||||||
|
|
||||||
private RequestHeaders _requestHeaders;
|
|
||||||
private ResponseHeaders _responseHeaders;
|
|
||||||
private CacheControlHeaderValue _requestCacheControl;
|
|
||||||
private CacheControlHeaderValue _responseCacheControl;
|
|
||||||
|
|
||||||
internal ResponseCachingState(HttpContext httpContext)
|
|
||||||
{
|
|
||||||
_httpContext = httpContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ShouldCacheResponse { get; internal set; }
|
|
||||||
|
|
||||||
public string StorageBaseKey { get; internal set; }
|
|
||||||
|
|
||||||
public string StorageVaryKey { get; internal set; }
|
|
||||||
|
|
||||||
public DateTimeOffset ResponseTime { get; internal set; }
|
|
||||||
|
|
||||||
public TimeSpan CachedEntryAge { get; internal set; }
|
|
||||||
|
|
||||||
public TimeSpan CachedResponseValidFor { get; internal set; }
|
|
||||||
|
|
||||||
internal CachedResponse CachedResponse { get; set; }
|
|
||||||
|
|
||||||
internal CachedVaryRules CachedVaryRules { get; set; }
|
|
||||||
|
|
||||||
public RequestHeaders RequestHeaders
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_requestHeaders == null)
|
|
||||||
{
|
|
||||||
_requestHeaders = _httpContext.Request.GetTypedHeaders();
|
|
||||||
}
|
|
||||||
return _requestHeaders;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public ResponseHeaders ResponseHeaders
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_responseHeaders == null)
|
|
||||||
{
|
|
||||||
_responseHeaders = _httpContext.Response.GetTypedHeaders();
|
|
||||||
}
|
|
||||||
return _responseHeaders;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public CacheControlHeaderValue RequestCacheControl
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_requestCacheControl == null)
|
|
||||||
{
|
|
||||||
_requestCacheControl = RequestHeaders.CacheControl ?? EmptyCacheControl;
|
|
||||||
}
|
|
||||||
return _requestCacheControl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public CacheControlHeaderValue ResponseCacheControl
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_responseCacheControl == null)
|
|
||||||
{
|
|
||||||
_responseCacheControl = ResponseHeaders.CacheControl ?? EmptyCacheControl;
|
|
||||||
}
|
|
||||||
return _responseCacheControl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -3,356 +3,102 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Globalization;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
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.AspNetCore.ResponseCaching.Internal;
|
using Microsoft.AspNetCore.ResponseCaching.Internal;
|
||||||
using Microsoft.Extensions.Primitives;
|
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching
|
namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
{
|
{
|
||||||
internal class ResponseCachingContext
|
public class ResponseCachingContext
|
||||||
{
|
{
|
||||||
private readonly HttpContext _httpContext;
|
private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue();
|
||||||
private readonly IResponseCache _cache;
|
|
||||||
private readonly ResponseCachingOptions _options;
|
|
||||||
private readonly ICacheabilityValidator _cacheabilityValidator;
|
|
||||||
private readonly IKeyProvider _keyProvider;
|
|
||||||
|
|
||||||
private ResponseCachingState _state;
|
private RequestHeaders _requestHeaders;
|
||||||
|
private ResponseHeaders _responseHeaders;
|
||||||
|
private CacheControlHeaderValue _requestCacheControl;
|
||||||
|
private CacheControlHeaderValue _responseCacheControl;
|
||||||
|
|
||||||
internal ResponseCachingContext(
|
internal ResponseCachingContext(
|
||||||
HttpContext httpContext,
|
HttpContext httpContext)
|
||||||
IResponseCache cache,
|
|
||||||
ResponseCachingOptions options,
|
|
||||||
ICacheabilityValidator cacheabilityValidator,
|
|
||||||
IKeyProvider keyProvider)
|
|
||||||
{
|
{
|
||||||
_httpContext = httpContext;
|
HttpContext = httpContext;
|
||||||
_cache = cache;
|
|
||||||
_options = options;
|
|
||||||
_cacheabilityValidator = cacheabilityValidator;
|
|
||||||
_keyProvider = keyProvider;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal ResponseCachingState State
|
public HttpContext HttpContext { get; }
|
||||||
{
|
|
||||||
get
|
public bool ShouldCacheResponse { get; internal set; }
|
||||||
{
|
|
||||||
if (_state == null)
|
public string StorageBaseKey { get; internal set; }
|
||||||
{
|
|
||||||
_state = _httpContext.GetResponseCachingState();
|
public string StorageVaryKey { get; internal set; }
|
||||||
}
|
|
||||||
return _state;
|
public DateTimeOffset ResponseTime { get; internal set; }
|
||||||
}
|
|
||||||
}
|
public TimeSpan CachedEntryAge { get; internal set; }
|
||||||
|
|
||||||
|
public TimeSpan CachedResponseValidFor { get; internal set; }
|
||||||
|
|
||||||
|
public CachedResponse CachedResponse { get; internal set; }
|
||||||
|
|
||||||
|
public CachedVaryRules CachedVaryRules { get; internal set; }
|
||||||
|
|
||||||
internal bool ResponseStarted { get; set; }
|
internal bool ResponseStarted { get; set; }
|
||||||
|
|
||||||
private Stream OriginalResponseStream { get; set; }
|
internal Stream OriginalResponseStream { get; set; }
|
||||||
|
|
||||||
private ResponseCacheStream ResponseCacheStream { get; set; }
|
internal ResponseCacheStream ResponseCacheStream { get; set; }
|
||||||
|
|
||||||
private IHttpSendFileFeature OriginalSendFileFeature { get; set; }
|
internal IHttpSendFileFeature OriginalSendFileFeature { get; set; }
|
||||||
|
|
||||||
internal async Task<bool> TryServeCachedResponseAsync(CachedResponse cachedResponse)
|
internal ResponseHeaders CachedResponseHeaders { get; set; }
|
||||||
|
|
||||||
|
internal RequestHeaders TypedRequestHeaders
|
||||||
{
|
{
|
||||||
State.CachedResponse = cachedResponse;
|
get
|
||||||
var cachedResponseHeaders = new ResponseHeaders(State.CachedResponse.Headers);
|
|
||||||
|
|
||||||
State.ResponseTime = _options.SystemClock.UtcNow;
|
|
||||||
var cachedEntryAge = State.ResponseTime - State.CachedResponse.Created;
|
|
||||||
State.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero;
|
|
||||||
|
|
||||||
if (_cacheabilityValidator.CachedEntryIsFresh(_httpContext, cachedResponseHeaders))
|
|
||||||
{
|
{
|
||||||
// Check conditional request rules
|
if (_requestHeaders == null)
|
||||||
if (ConditionalRequestSatisfied(cachedResponseHeaders))
|
|
||||||
{
|
{
|
||||||
_httpContext.Response.StatusCode = StatusCodes.Status304NotModified;
|
_requestHeaders = HttpContext.Request.GetTypedHeaders();
|
||||||
}
|
}
|
||||||
else
|
return _requestHeaders;
|
||||||
{
|
|
||||||
var response = _httpContext.Response;
|
|
||||||
// Copy the cached status code and response headers
|
|
||||||
response.StatusCode = State.CachedResponse.StatusCode;
|
|
||||||
foreach (var header in State.CachedResponse.Headers)
|
|
||||||
{
|
|
||||||
response.Headers.Add(header);
|
|
||||||
}
|
|
||||||
|
|
||||||
response.Headers[HeaderNames.Age] = State.CachedEntryAge.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture);
|
|
||||||
|
|
||||||
var body = State.CachedResponse.Body ??
|
|
||||||
((CachedResponseBody)_cache.Get(State.CachedResponse.BodyKeyPrefix))?.Body;
|
|
||||||
|
|
||||||
// If the body is not found, something went wrong.
|
|
||||||
if (body == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the cached response body
|
|
||||||
if (body.Length > 0)
|
|
||||||
{
|
|
||||||
// Add a content-length if required
|
|
||||||
if (response.ContentLength == null && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding]))
|
|
||||||
{
|
|
||||||
response.ContentLength = body.Length;
|
|
||||||
}
|
|
||||||
await response.Body.WriteAsync(body, 0, body.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// TODO: Validate with endpoint instead
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal async Task<bool> TryServeFromCacheAsync()
|
|
||||||
{
|
|
||||||
foreach (var baseKey in _keyProvider.CreateLookupBaseKey(_httpContext))
|
|
||||||
{
|
|
||||||
var cacheEntry = _cache.Get(baseKey);
|
|
||||||
|
|
||||||
if (cacheEntry is CachedVaryRules)
|
|
||||||
{
|
|
||||||
// Request contains vary rules, recompute key(s) and try again
|
|
||||||
State.CachedVaryRules = cacheEntry as CachedVaryRules;
|
|
||||||
|
|
||||||
foreach (var varyKey in _keyProvider.CreateLookupVaryKey(_httpContext, State.CachedVaryRules.VaryRules))
|
|
||||||
{
|
|
||||||
cacheEntry = _cache.Get(varyKey);
|
|
||||||
|
|
||||||
if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(cacheEntry as CachedResponse))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(cacheEntry as CachedResponse))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (State.RequestCacheControl.OnlyIfCached)
|
|
||||||
{
|
|
||||||
_httpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal bool ConditionalRequestSatisfied(ResponseHeaders cachedResponseHeaders)
|
|
||||||
{
|
|
||||||
var ifNoneMatchHeader = State.RequestHeaders.IfNoneMatch;
|
|
||||||
|
|
||||||
if (ifNoneMatchHeader != null)
|
|
||||||
{
|
|
||||||
if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cachedResponseHeaders.ETag != null)
|
|
||||||
{
|
|
||||||
foreach (var tag in ifNoneMatchHeader)
|
|
||||||
{
|
|
||||||
if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: true))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if ((cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= State.RequestHeaders.IfUnmodifiedSince)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void FinalizeCachingHeaders()
|
|
||||||
{
|
|
||||||
if (_cacheabilityValidator.ResponseIsCacheable(_httpContext))
|
|
||||||
{
|
|
||||||
State.ShouldCacheResponse = true;
|
|
||||||
State.StorageBaseKey = _keyProvider.CreateStorageBaseKey(_httpContext);
|
|
||||||
|
|
||||||
// Create the cache entry now
|
|
||||||
var response = _httpContext.Response;
|
|
||||||
var varyHeaderValue = response.Headers[HeaderNames.Vary];
|
|
||||||
var varyParamsValue = _httpContext.GetResponseCachingFeature()?.VaryParams ?? StringValues.Empty;
|
|
||||||
State.CachedResponseValidFor = State.ResponseCacheControl.SharedMaxAge
|
|
||||||
?? State.ResponseCacheControl.MaxAge
|
|
||||||
?? (State.ResponseHeaders.Expires - State.ResponseTime)
|
|
||||||
// TODO: Heuristics for expiration?
|
|
||||||
?? TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
// Check if any vary rules exist
|
|
||||||
if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue))
|
|
||||||
{
|
|
||||||
// Normalize order and casing of vary by rules
|
|
||||||
var normalizedVaryHeaderValue = GetNormalizedStringValues(varyHeaderValue);
|
|
||||||
var normalizedVaryParamsValue = GetNormalizedStringValues(varyParamsValue);
|
|
||||||
|
|
||||||
// Update vary rules if they are different
|
|
||||||
if (State.CachedVaryRules == null ||
|
|
||||||
!StringValues.Equals(State.CachedVaryRules.VaryRules.Params, normalizedVaryParamsValue) ||
|
|
||||||
!StringValues.Equals(State.CachedVaryRules.VaryRules.Headers, normalizedVaryHeaderValue))
|
|
||||||
{
|
|
||||||
var cachedVaryRules = new CachedVaryRules
|
|
||||||
{
|
|
||||||
VaryKeyPrefix = FastGuid.NewGuid().IdString,
|
|
||||||
VaryRules = new VaryRules()
|
|
||||||
{
|
|
||||||
// TODO: Vary Encoding
|
|
||||||
Headers = normalizedVaryHeaderValue,
|
|
||||||
Params = normalizedVaryParamsValue
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
State.CachedVaryRules = cachedVaryRules;
|
|
||||||
_cache.Set(State.StorageBaseKey, cachedVaryRules, State.CachedResponseValidFor);
|
|
||||||
}
|
|
||||||
|
|
||||||
State.StorageVaryKey = _keyProvider.CreateStorageVaryKey(_httpContext, State.CachedVaryRules.VaryRules);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure date header is set
|
|
||||||
if (State.ResponseHeaders.Date == null)
|
|
||||||
{
|
|
||||||
State.ResponseHeaders.Date = State.ResponseTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the response on the state
|
|
||||||
State.CachedResponse = new CachedResponse
|
|
||||||
{
|
|
||||||
BodyKeyPrefix = FastGuid.NewGuid().IdString,
|
|
||||||
Created = State.ResponseHeaders.Date.Value,
|
|
||||||
StatusCode = _httpContext.Response.StatusCode
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var header in State.ResponseHeaders.Headers)
|
|
||||||
{
|
|
||||||
if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
State.CachedResponse.Headers.Add(header);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
ResponseCacheStream.DisableBuffering();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void FinalizeCachingBody()
|
internal ResponseHeaders TypedResponseHeaders
|
||||||
{
|
{
|
||||||
if (State.ShouldCacheResponse &&
|
get
|
||||||
ResponseCacheStream.BufferingEnabled &&
|
|
||||||
(State.ResponseHeaders.ContentLength == null ||
|
|
||||||
State.ResponseHeaders.ContentLength == ResponseCacheStream.BufferedStream.Length))
|
|
||||||
{
|
{
|
||||||
if (ResponseCacheStream.BufferedStream.Length >= _options.MinimumSplitBodySize)
|
if (_responseHeaders == null)
|
||||||
{
|
{
|
||||||
// Store response and response body separately
|
_responseHeaders = HttpContext.Response.GetTypedHeaders();
|
||||||
_cache.Set(State.StorageVaryKey ?? State.StorageBaseKey, State.CachedResponse, State.CachedResponseValidFor);
|
|
||||||
|
|
||||||
var cachedResponseBody = new CachedResponseBody()
|
|
||||||
{
|
|
||||||
Body = ResponseCacheStream.BufferedStream.ToArray()
|
|
||||||
};
|
|
||||||
|
|
||||||
_cache.Set(State.CachedResponse.BodyKeyPrefix, cachedResponseBody, State.CachedResponseValidFor);
|
|
||||||
}
|
}
|
||||||
else
|
return _responseHeaders;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal CacheControlHeaderValue RequestCacheControlHeaderValue
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_requestCacheControl == null)
|
||||||
{
|
{
|
||||||
// Store response and response body together
|
_requestCacheControl = TypedRequestHeaders.CacheControl ?? EmptyCacheControl;
|
||||||
State.CachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray();
|
|
||||||
_cache.Set(State.StorageVaryKey ?? State.StorageBaseKey, State.CachedResponse, State.CachedResponseValidFor);
|
|
||||||
}
|
}
|
||||||
|
return _requestCacheControl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void OnResponseStarting()
|
internal CacheControlHeaderValue ResponseCacheControlHeaderValue
|
||||||
{
|
{
|
||||||
if (!ResponseStarted)
|
get
|
||||||
{
|
{
|
||||||
ResponseStarted = true;
|
if (_responseCacheControl == null)
|
||||||
State.ResponseTime = _options.SystemClock.UtcNow;
|
|
||||||
|
|
||||||
FinalizeCachingHeaders();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void ShimResponseStream()
|
|
||||||
{
|
|
||||||
// TODO: Consider caching large responses on disk and serving them from there.
|
|
||||||
|
|
||||||
// Shim response stream
|
|
||||||
OriginalResponseStream = _httpContext.Response.Body;
|
|
||||||
ResponseCacheStream = new ResponseCacheStream(OriginalResponseStream, _options.MaximumCachedBodySize);
|
|
||||||
_httpContext.Response.Body = ResponseCacheStream;
|
|
||||||
|
|
||||||
// Shim IHttpSendFileFeature
|
|
||||||
OriginalSendFileFeature = _httpContext.Features.Get<IHttpSendFileFeature>();
|
|
||||||
if (OriginalSendFileFeature != null)
|
|
||||||
{
|
|
||||||
_httpContext.Features.Set<IHttpSendFileFeature>(new SendFileFeatureWrapper(OriginalSendFileFeature, ResponseCacheStream));
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Move this temporary interface with endpoint to HttpAbstractions
|
|
||||||
_httpContext.AddResponseCachingFeature();
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void UnshimResponseStream()
|
|
||||||
{
|
|
||||||
// Unshim response stream
|
|
||||||
_httpContext.Response.Body = OriginalResponseStream;
|
|
||||||
|
|
||||||
// Unshim IHttpSendFileFeature
|
|
||||||
_httpContext.Features.Set(OriginalSendFileFeature);
|
|
||||||
|
|
||||||
// TODO: Move this temporary interface with endpoint to HttpAbstractions
|
|
||||||
_httpContext.RemoveResponseCachingFeature();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize order and casing
|
|
||||||
internal static StringValues GetNormalizedStringValues(StringValues stringVales)
|
|
||||||
{
|
|
||||||
if (stringVales.Count == 1)
|
|
||||||
{
|
|
||||||
return new StringValues(stringVales.ToString().ToUpperInvariant());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var originalArray = stringVales.ToArray();
|
|
||||||
var newArray = new string[originalArray.Length];
|
|
||||||
|
|
||||||
for (int i = 0; i < originalArray.Length; i++)
|
|
||||||
{
|
{
|
||||||
newArray[i] = originalArray[i].ToUpperInvariant();
|
_responseCacheControl = TypedResponseHeaders.CacheControl ?? EmptyCacheControl;
|
||||||
}
|
}
|
||||||
|
return _responseCacheControl;
|
||||||
// Since the casing has already been normalized, use Ordinal comparison
|
|
||||||
Array.Sort(newArray, StringComparer.Ordinal);
|
|
||||||
|
|
||||||
return new StringValues(newArray);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,35 +2,37 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
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.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching
|
namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
{
|
{
|
||||||
public class ResponseCachingMiddleware
|
public class ResponseCachingMiddleware
|
||||||
{
|
{
|
||||||
private static readonly Func<object, Task> OnStartingCallback = state =>
|
private static readonly TimeSpan DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10);
|
||||||
{
|
|
||||||
((ResponseCachingContext)state).OnResponseStarting();
|
|
||||||
return TaskCache.CompletedTask;
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
private readonly IResponseCache _cache;
|
private readonly IResponseCache _cache;
|
||||||
private readonly ResponseCachingOptions _options;
|
private readonly ResponseCachingOptions _options;
|
||||||
private readonly ICacheabilityValidator _cacheabilityValidator;
|
private readonly ICacheabilityValidator _cacheabilityValidator;
|
||||||
private readonly IKeyProvider _keyProvider;
|
private readonly ICacheKeyProvider _cacheKeyProvider;
|
||||||
|
private readonly Func<object, Task> _onStartingCallback;
|
||||||
|
|
||||||
public ResponseCachingMiddleware(
|
public ResponseCachingMiddleware(
|
||||||
RequestDelegate next,
|
RequestDelegate next,
|
||||||
IResponseCache cache,
|
IResponseCache cache,
|
||||||
IOptions<ResponseCachingOptions> options,
|
IOptions<ResponseCachingOptions> options,
|
||||||
ICacheabilityValidator cacheabilityValidator,
|
ICacheabilityValidator cacheabilityValidator,
|
||||||
IKeyProvider keyProvider)
|
ICacheKeyProvider cacheKeyProvider)
|
||||||
{
|
{
|
||||||
if (next == null)
|
if (next == null)
|
||||||
{
|
{
|
||||||
|
|
@ -48,70 +50,352 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(cacheabilityValidator));
|
throw new ArgumentNullException(nameof(cacheabilityValidator));
|
||||||
}
|
}
|
||||||
if (keyProvider == null)
|
if (cacheKeyProvider == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(keyProvider));
|
throw new ArgumentNullException(nameof(cacheKeyProvider));
|
||||||
}
|
}
|
||||||
|
|
||||||
_next = next;
|
_next = next;
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
_cacheabilityValidator = cacheabilityValidator;
|
_cacheabilityValidator = cacheabilityValidator;
|
||||||
_keyProvider = keyProvider;
|
_cacheKeyProvider = cacheKeyProvider;
|
||||||
|
_onStartingCallback = state =>
|
||||||
|
{
|
||||||
|
OnResponseStarting((ResponseCachingContext)state);
|
||||||
|
return TaskCache.CompletedTask;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Invoke(HttpContext context)
|
public async Task Invoke(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
context.AddResponseCachingState();
|
var context = new ResponseCachingContext(httpContext);
|
||||||
|
|
||||||
try
|
// Should we attempt any caching logic?
|
||||||
|
if (_cacheabilityValidator.IsRequestCacheable(context))
|
||||||
{
|
{
|
||||||
var cachingContext = new ResponseCachingContext(
|
// Can this request be served from cache?
|
||||||
context,
|
if (await TryServeFromCacheAsync(context))
|
||||||
_cache,
|
|
||||||
_options,
|
|
||||||
_cacheabilityValidator,
|
|
||||||
_keyProvider);
|
|
||||||
|
|
||||||
// Should we attempt any caching logic?
|
|
||||||
if (_cacheabilityValidator.RequestIsCacheable(context))
|
|
||||||
{
|
{
|
||||||
// Can this request be served from cache?
|
return;
|
||||||
if (await cachingContext.TryServeFromCacheAsync())
|
}
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hook up to listen to the response stream
|
// Hook up to listen to the response stream
|
||||||
cachingContext.ShimResponseStream();
|
ShimResponseStream(context);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Subscribe to OnStarting event
|
// Subscribe to OnStarting event
|
||||||
context.Response.OnStarting(OnStartingCallback, cachingContext);
|
httpContext.Response.OnStarting(_onStartingCallback, context);
|
||||||
|
|
||||||
await _next(context);
|
await _next(httpContext);
|
||||||
|
|
||||||
// If there was no response body, check the response headers now. We can cache things like redirects.
|
// If there was no response body, check the response headers now. We can cache things like redirects.
|
||||||
cachingContext.OnResponseStarting();
|
OnResponseStarting(context);
|
||||||
|
|
||||||
// Finalize the cache entry
|
// Finalize the cache entry
|
||||||
cachingContext.FinalizeCachingBody();
|
FinalizeCachingBody(context);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
cachingContext.UnshimResponseStream();
|
UnshimResponseStream(context);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// TODO: Invalidate resources for successful unsafe methods? Required by RFC
|
||||||
|
await _next(httpContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<bool> TryServeCachedResponseAsync(ResponseCachingContext context, CachedResponse cachedResponse)
|
||||||
|
{
|
||||||
|
context.CachedResponse = cachedResponse;
|
||||||
|
context.CachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers);
|
||||||
|
context.ResponseTime = _options.SystemClock.UtcNow;
|
||||||
|
var cachedEntryAge = context.ResponseTime - context.CachedResponse.Created;
|
||||||
|
context.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero;
|
||||||
|
|
||||||
|
if (_cacheabilityValidator.IsCachedEntryFresh(context))
|
||||||
|
{
|
||||||
|
// Check conditional request rules
|
||||||
|
if (ConditionalRequestSatisfied(context))
|
||||||
|
{
|
||||||
|
context.HttpContext.Response.StatusCode = StatusCodes.Status304NotModified;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// TODO: Invalidate resources for successful unsafe methods? Required by RFC
|
var response = context.HttpContext.Response;
|
||||||
await _next(context);
|
// Copy the cached status code and response headers
|
||||||
|
response.StatusCode = context.CachedResponse.StatusCode;
|
||||||
|
foreach (var header in context.CachedResponse.Headers)
|
||||||
|
{
|
||||||
|
response.Headers.Add(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Headers[HeaderNames.Age] = context.CachedEntryAge.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
var body = context.CachedResponse.Body ??
|
||||||
|
((CachedResponseBody)_cache.Get(context.CachedResponse.BodyKeyPrefix))?.Body;
|
||||||
|
|
||||||
|
// If the body is not found, something went wrong.
|
||||||
|
if (body == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the cached response body
|
||||||
|
if (body.Length > 0)
|
||||||
|
{
|
||||||
|
// Add a content-length if required
|
||||||
|
if (response.ContentLength == null && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding]))
|
||||||
|
{
|
||||||
|
response.ContentLength = body.Length;
|
||||||
|
}
|
||||||
|
await response.Body.WriteAsync(body, 0, body.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// TODO: Validate with endpoint instead
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<bool> TryServeFromCacheAsync(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
foreach (var baseKey in _cacheKeyProvider.CreateLookupBaseKeys(context))
|
||||||
|
{
|
||||||
|
var cacheEntry = _cache.Get(baseKey);
|
||||||
|
|
||||||
|
if (cacheEntry is CachedVaryRules)
|
||||||
|
{
|
||||||
|
// Request contains vary rules, recompute key(s) and try again
|
||||||
|
context.CachedVaryRules = (CachedVaryRules)cacheEntry;
|
||||||
|
|
||||||
|
foreach (var varyKey in _cacheKeyProvider.CreateLookupVaryKeys(context))
|
||||||
|
{
|
||||||
|
cacheEntry = _cache.Get(varyKey);
|
||||||
|
|
||||||
|
if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(context, (CachedResponse)cacheEntry))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(context, (CachedResponse)cacheEntry))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
|
|
||||||
|
if (context.RequestCacheControlHeaderValue.OnlyIfCached)
|
||||||
{
|
{
|
||||||
context.RemoveResponseCachingState();
|
context.HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void FinalizeCachingHeaders(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
if (_cacheabilityValidator.IsResponseCacheable(context))
|
||||||
|
{
|
||||||
|
context.ShouldCacheResponse = true;
|
||||||
|
context.StorageBaseKey = _cacheKeyProvider.CreateStorageBaseKey(context);
|
||||||
|
|
||||||
|
// Create the cache entry now
|
||||||
|
var response = context.HttpContext.Response;
|
||||||
|
var varyHeaderValue = response.Headers[HeaderNames.Vary];
|
||||||
|
var varyParamsValue = context.HttpContext.GetResponseCachingFeature()?.VaryParams ?? StringValues.Empty;
|
||||||
|
context.CachedResponseValidFor = context.ResponseCacheControlHeaderValue.SharedMaxAge ??
|
||||||
|
context.ResponseCacheControlHeaderValue.MaxAge ??
|
||||||
|
(context.TypedResponseHeaders.Expires - context.ResponseTime) ??
|
||||||
|
DefaultExpirationTimeSpan;
|
||||||
|
|
||||||
|
// Check if any vary rules exist
|
||||||
|
if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue))
|
||||||
|
{
|
||||||
|
// Normalize order and casing of vary by rules
|
||||||
|
var normalizedVaryHeaderValue = GetNormalizedStringValues(varyHeaderValue);
|
||||||
|
var normalizedVaryParamsValue = GetNormalizedStringValues(varyParamsValue);
|
||||||
|
|
||||||
|
// Update vary rules if they are different
|
||||||
|
if (context.CachedVaryRules == null ||
|
||||||
|
!StringValues.Equals(context.CachedVaryRules.Params, normalizedVaryParamsValue) ||
|
||||||
|
!StringValues.Equals(context.CachedVaryRules.Headers, normalizedVaryHeaderValue))
|
||||||
|
{
|
||||||
|
context.CachedVaryRules = new CachedVaryRules
|
||||||
|
{
|
||||||
|
VaryKeyPrefix = FastGuid.NewGuid().IdString,
|
||||||
|
Headers = normalizedVaryHeaderValue,
|
||||||
|
Params = normalizedVaryParamsValue
|
||||||
|
};
|
||||||
|
|
||||||
|
_cache.Set(context.StorageBaseKey, context.CachedVaryRules, context.CachedResponseValidFor);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.StorageVaryKey = _cacheKeyProvider.CreateStorageVaryKey(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure date header is set
|
||||||
|
if (context.TypedResponseHeaders.Date == null)
|
||||||
|
{
|
||||||
|
context.TypedResponseHeaders.Date = context.ResponseTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the response on the state
|
||||||
|
context.CachedResponse = new CachedResponse
|
||||||
|
{
|
||||||
|
BodyKeyPrefix = FastGuid.NewGuid().IdString,
|
||||||
|
Created = context.TypedResponseHeaders.Date.Value,
|
||||||
|
StatusCode = context.HttpContext.Response.StatusCode
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var header in context.TypedResponseHeaders.Headers)
|
||||||
|
{
|
||||||
|
if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
context.CachedResponse.Headers.Add(header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
context.ResponseCacheStream.DisableBuffering();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void FinalizeCachingBody(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
if (context.ShouldCacheResponse &&
|
||||||
|
context.ResponseCacheStream.BufferingEnabled &&
|
||||||
|
(context.TypedResponseHeaders.ContentLength == null ||
|
||||||
|
context.TypedResponseHeaders.ContentLength == context.ResponseCacheStream.BufferedStream.Length))
|
||||||
|
{
|
||||||
|
if (context.ResponseCacheStream.BufferedStream.Length >= _options.MinimumSplitBodySize)
|
||||||
|
{
|
||||||
|
// Store response and response body separately
|
||||||
|
_cache.Set(context.StorageVaryKey ?? context.StorageBaseKey, context.CachedResponse, context.CachedResponseValidFor);
|
||||||
|
|
||||||
|
var cachedResponseBody = new CachedResponseBody()
|
||||||
|
{
|
||||||
|
Body = context.ResponseCacheStream.BufferedStream.ToArray()
|
||||||
|
};
|
||||||
|
|
||||||
|
_cache.Set(context.CachedResponse.BodyKeyPrefix, cachedResponseBody, context.CachedResponseValidFor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Store response and response body together
|
||||||
|
context.CachedResponse.Body = context.ResponseCacheStream.BufferedStream.ToArray();
|
||||||
|
_cache.Set(context.StorageVaryKey ?? context.StorageBaseKey, context.CachedResponse, context.CachedResponseValidFor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void OnResponseStarting(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
if (!context.ResponseStarted)
|
||||||
|
{
|
||||||
|
context.ResponseStarted = true;
|
||||||
|
context.ResponseTime = _options.SystemClock.UtcNow;
|
||||||
|
|
||||||
|
FinalizeCachingHeaders(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ShimResponseStream(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
// TODO: Consider caching large responses on disk and serving them from there.
|
||||||
|
|
||||||
|
// Shim response stream
|
||||||
|
context.OriginalResponseStream = context.HttpContext.Response.Body;
|
||||||
|
context.ResponseCacheStream = new ResponseCacheStream(context.OriginalResponseStream, _options.MaximumCachedBodySize);
|
||||||
|
context.HttpContext.Response.Body = context.ResponseCacheStream;
|
||||||
|
|
||||||
|
// Shim IHttpSendFileFeature
|
||||||
|
context.OriginalSendFileFeature = context.HttpContext.Features.Get<IHttpSendFileFeature>();
|
||||||
|
if (context.OriginalSendFileFeature != null)
|
||||||
|
{
|
||||||
|
context.HttpContext.Features.Set<IHttpSendFileFeature>(new SendFileFeatureWrapper(context.OriginalSendFileFeature, context.ResponseCacheStream));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Move this temporary interface with endpoint to HttpAbstractions
|
||||||
|
context.HttpContext.AddResponseCachingFeature();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void UnshimResponseStream(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
// Unshim response stream
|
||||||
|
context.HttpContext.Response.Body = context.OriginalResponseStream;
|
||||||
|
|
||||||
|
// Unshim IHttpSendFileFeature
|
||||||
|
context.HttpContext.Features.Set(context.OriginalSendFileFeature);
|
||||||
|
|
||||||
|
// TODO: Move this temporary interface with endpoint to HttpAbstractions
|
||||||
|
context.HttpContext.RemoveResponseCachingFeature();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool ConditionalRequestSatisfied(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
var cachedResponseHeaders = context.CachedResponseHeaders;
|
||||||
|
var ifNoneMatchHeader = context.TypedRequestHeaders.IfNoneMatch;
|
||||||
|
|
||||||
|
if (ifNoneMatchHeader != null)
|
||||||
|
{
|
||||||
|
if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cachedResponseHeaders.ETag != null)
|
||||||
|
{
|
||||||
|
foreach (var tag in ifNoneMatchHeader)
|
||||||
|
{
|
||||||
|
if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: true))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ((cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= context.TypedRequestHeaders.IfUnmodifiedSince)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize order and casing
|
||||||
|
internal static StringValues GetNormalizedStringValues(StringValues stringValues)
|
||||||
|
{
|
||||||
|
if (stringValues.Count == 1)
|
||||||
|
{
|
||||||
|
return new StringValues(stringValues.ToString().ToUpperInvariant());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var originalArray = stringValues.ToArray();
|
||||||
|
var newArray = new string[originalArray.Length];
|
||||||
|
|
||||||
|
for (int i = 0; i < originalArray.Length; i++)
|
||||||
|
{
|
||||||
|
newArray[i] = originalArray[i].ToUpperInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since the casing has already been normalized, use Ordinal comparison
|
||||||
|
Array.Sort(newArray, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
return new StringValues(newArray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Primitives;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching
|
|
||||||
{
|
|
||||||
public class VaryRules
|
|
||||||
{
|
|
||||||
internal StringValues Headers { get; set; }
|
|
||||||
internal StringValues Params { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -81,8 +81,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
{
|
{
|
||||||
var cachedVaryRule = new CachedVaryRules()
|
var cachedVaryRule = new CachedVaryRules()
|
||||||
{
|
{
|
||||||
VaryKeyPrefix = FastGuid.NewGuid().IdString,
|
VaryKeyPrefix = FastGuid.NewGuid().IdString
|
||||||
VaryRules = new VaryRules()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
|
AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
|
||||||
|
|
@ -95,10 +94,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
var cachedVaryRule = new CachedVaryRules()
|
var cachedVaryRule = new CachedVaryRules()
|
||||||
{
|
{
|
||||||
VaryKeyPrefix = FastGuid.NewGuid().IdString,
|
VaryKeyPrefix = FastGuid.NewGuid().IdString,
|
||||||
VaryRules = new VaryRules()
|
Headers = headers
|
||||||
{
|
|
||||||
Headers = headers
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
|
AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
|
||||||
|
|
@ -111,10 +107,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
var cachedVaryRule = new CachedVaryRules()
|
var cachedVaryRule = new CachedVaryRules()
|
||||||
{
|
{
|
||||||
VaryKeyPrefix = FastGuid.NewGuid().IdString,
|
VaryKeyPrefix = FastGuid.NewGuid().IdString,
|
||||||
VaryRules = new VaryRules()
|
Params = param
|
||||||
{
|
|
||||||
Params = param
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
|
AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
|
||||||
|
|
@ -128,11 +121,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
var cachedVaryRule = new CachedVaryRules()
|
var cachedVaryRule = new CachedVaryRules()
|
||||||
{
|
{
|
||||||
VaryKeyPrefix = FastGuid.NewGuid().IdString,
|
VaryKeyPrefix = FastGuid.NewGuid().IdString,
|
||||||
VaryRules = new VaryRules()
|
Headers = headers,
|
||||||
{
|
Params = param
|
||||||
Headers = headers,
|
|
||||||
Params = param
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
|
AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
|
||||||
|
|
@ -145,10 +135,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
var cachedVaryRule = new CachedVaryRules()
|
var cachedVaryRule = new CachedVaryRules()
|
||||||
{
|
{
|
||||||
VaryKeyPrefix = FastGuid.NewGuid().IdString,
|
VaryKeyPrefix = FastGuid.NewGuid().IdString,
|
||||||
VaryRules = new VaryRules()
|
Headers = headers
|
||||||
{
|
|
||||||
Headers = headers
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
var serializedEntry = CacheEntrySerializer.Serialize(cachedVaryRule);
|
var serializedEntry = CacheEntrySerializer.Serialize(cachedVaryRule);
|
||||||
Array.Reverse(serializedEntry);
|
Array.Reverse(serializedEntry);
|
||||||
|
|
@ -188,8 +175,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
Assert.NotNull(actual);
|
Assert.NotNull(actual);
|
||||||
Assert.NotNull(expected);
|
Assert.NotNull(expected);
|
||||||
Assert.Equal(expected.VaryKeyPrefix, actual.VaryKeyPrefix);
|
Assert.Equal(expected.VaryKeyPrefix, actual.VaryKeyPrefix);
|
||||||
Assert.Equal(expected.VaryRules.Headers, actual.VaryRules.Headers);
|
Assert.Equal(expected.Headers, actual.Headers);
|
||||||
Assert.Equal(expected.VaryRules.Params, actual.VaryRules.Params);
|
Assert.Equal(expected.Params, actual.Params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
using System;
|
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.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -17,10 +16,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[InlineData("HEAD")]
|
[InlineData("HEAD")]
|
||||||
public void RequestIsCacheable_CacheableMethods_Allowed(string method)
|
public void RequestIsCacheable_CacheableMethods_Allowed(string method)
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Request.Method = method;
|
context.HttpContext.Request.Method = method;
|
||||||
|
|
||||||
Assert.True(new CacheabilityValidator().RequestIsCacheable(httpContext));
|
Assert.True(new CacheabilityValidator().IsRequestCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -34,182 +33,182 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[InlineData(null)]
|
[InlineData(null)]
|
||||||
public void RequestIsCacheable_UncacheableMethods_NotAllowed(string method)
|
public void RequestIsCacheable_UncacheableMethods_NotAllowed(string method)
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Request.Method = method;
|
context.HttpContext.Request.Method = method;
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext));
|
Assert.False(new CacheabilityValidator().IsRequestCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RequestIsCacheable_AuthorizationHeaders_NotAllowed()
|
public void RequestIsCacheable_AuthorizationHeaders_NotAllowed()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Request.Method = "GET";
|
context.HttpContext.Request.Method = "GET";
|
||||||
httpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW";
|
context.HttpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW";
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext));
|
Assert.False(new CacheabilityValidator().IsRequestCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RequestIsCacheable_NoCache_NotAllowed()
|
public void RequestIsCacheable_NoCache_NotAllowed()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Request.Method = "GET";
|
context.HttpContext.Request.Method = "GET";
|
||||||
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
NoCache = true
|
NoCache = true
|
||||||
};
|
};
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext));
|
Assert.False(new CacheabilityValidator().IsRequestCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RequestIsCacheable_NoStore_Allowed()
|
public void RequestIsCacheable_NoStore_Allowed()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Request.Method = "GET";
|
context.HttpContext.Request.Method = "GET";
|
||||||
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
NoStore = true
|
NoStore = true
|
||||||
};
|
};
|
||||||
|
|
||||||
Assert.True(new CacheabilityValidator().RequestIsCacheable(httpContext));
|
Assert.True(new CacheabilityValidator().IsRequestCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RequestIsCacheable_LegacyDirectives_NotAllowed()
|
public void RequestIsCacheable_LegacyDirectives_NotAllowed()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Request.Method = "GET";
|
context.HttpContext.Request.Method = "GET";
|
||||||
httpContext.Request.Headers[HeaderNames.Pragma] = "no-cache";
|
context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache";
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext));
|
Assert.False(new CacheabilityValidator().IsRequestCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RequestIsCacheable_LegacyDirectives_OverridenByCacheControl()
|
public void RequestIsCacheable_LegacyDirectives_OverridenByCacheControl()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Request.Method = "GET";
|
context.HttpContext.Request.Method = "GET";
|
||||||
httpContext.Request.Headers[HeaderNames.Pragma] = "no-cache";
|
context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache";
|
||||||
httpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10";
|
context.HttpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10";
|
||||||
|
|
||||||
Assert.True(new CacheabilityValidator().RequestIsCacheable(httpContext));
|
Assert.True(new CacheabilityValidator().IsRequestCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResponseIsCacheable_NoPublic_NotAllowed()
|
public void ResponseIsCacheable_NoPublic_NotAllowed()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
|
Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResponseIsCacheable_Public_Allowed()
|
public void ResponseIsCacheable_Public_Allowed()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
Public = true
|
Public = true
|
||||||
};
|
};
|
||||||
|
|
||||||
Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext));
|
Assert.True(new CacheabilityValidator().IsResponseCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResponseIsCacheable_NoCache_NotAllowed()
|
public void ResponseIsCacheable_NoCache_NotAllowed()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
Public = true,
|
Public = true,
|
||||||
NoCache = true
|
NoCache = true
|
||||||
};
|
};
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
|
Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResponseIsCacheable_RequestNoStore_NotAllowed()
|
public void ResponseIsCacheable_RequestNoStore_NotAllowed()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
NoStore = true
|
NoStore = true
|
||||||
};
|
};
|
||||||
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
Public = true
|
Public = true
|
||||||
};
|
};
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
|
Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResponseIsCacheable_ResponseNoStore_NotAllowed()
|
public void ResponseIsCacheable_ResponseNoStore_NotAllowed()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
Public = true,
|
Public = true,
|
||||||
NoStore = true
|
NoStore = true
|
||||||
};
|
};
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
|
Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResponseIsCacheable_SetCookieHeader_NotAllowed()
|
public void ResponseIsCacheable_SetCookieHeader_NotAllowed()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
Public = true
|
Public = true
|
||||||
};
|
};
|
||||||
httpContext.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue";
|
context.HttpContext.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue";
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
|
Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResponseIsCacheable_VaryHeaderByStar_NotAllowed()
|
public void ResponseIsCacheable_VaryHeaderByStar_NotAllowed()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
Public = true
|
Public = true
|
||||||
};
|
};
|
||||||
httpContext.Response.Headers[HeaderNames.Vary] = "*";
|
context.HttpContext.Response.Headers[HeaderNames.Vary] = "*";
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
|
Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResponseIsCacheable_Private_NotAllowed()
|
public void ResponseIsCacheable_Private_NotAllowed()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
Public = true,
|
Public = true,
|
||||||
Private = true
|
Private = true
|
||||||
};
|
};
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
|
Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(StatusCodes.Status200OK)]
|
[InlineData(StatusCodes.Status200OK)]
|
||||||
public void ResponseIsCacheable_SuccessStatusCodes_Allowed(int statusCode)
|
public void ResponseIsCacheable_SuccessStatusCodes_Allowed(int statusCode)
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Response.StatusCode = statusCode;
|
context.HttpContext.Response.StatusCode = statusCode;
|
||||||
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
Public = true
|
Public = true
|
||||||
};
|
};
|
||||||
|
|
||||||
Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext));
|
Assert.True(new CacheabilityValidator().IsResponseCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -263,146 +262,141 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[InlineData(StatusCodes.Status507InsufficientStorage)]
|
[InlineData(StatusCodes.Status507InsufficientStorage)]
|
||||||
public void ResponseIsCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode)
|
public void ResponseIsCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode)
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Response.StatusCode = statusCode;
|
context.HttpContext.Response.StatusCode = statusCode;
|
||||||
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
Public = true
|
Public = true
|
||||||
};
|
};
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
|
Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResponseIsCacheable_NoExpiryRequirements_IsAllowed()
|
public void ResponseIsCacheable_NoExpiryRequirements_IsAllowed()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Response.StatusCode = StatusCodes.Status200OK;
|
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
|
||||||
var headers = httpContext.Response.GetTypedHeaders();
|
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
headers.CacheControl = new CacheControlHeaderValue()
|
|
||||||
{
|
{
|
||||||
Public = true
|
Public = true
|
||||||
};
|
};
|
||||||
|
|
||||||
var utcNow = DateTimeOffset.UtcNow;
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
headers.Date = utcNow;
|
context.TypedResponseHeaders.Date = utcNow;
|
||||||
httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue;
|
context.ResponseTime = DateTimeOffset.MaxValue;
|
||||||
|
|
||||||
Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext));
|
Assert.True(new CacheabilityValidator().IsResponseCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResponseIsCacheable_PastExpiry_NotAllowed()
|
public void ResponseIsCacheable_PastExpiry_NotAllowed()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Response.StatusCode = StatusCodes.Status200OK;
|
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
|
||||||
var headers = httpContext.Response.GetTypedHeaders();
|
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
headers.CacheControl = new CacheControlHeaderValue()
|
|
||||||
{
|
{
|
||||||
Public = true
|
Public = true
|
||||||
};
|
};
|
||||||
var utcNow = DateTimeOffset.UtcNow;
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
headers.Expires = utcNow;
|
context.TypedResponseHeaders.Expires = utcNow;
|
||||||
|
|
||||||
headers.Date = utcNow;
|
context.TypedResponseHeaders.Date = utcNow;
|
||||||
httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue;
|
context.ResponseTime = DateTimeOffset.MaxValue;
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
|
Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResponseIsCacheable_MaxAgeOverridesExpiry_ToAllowed()
|
public void ResponseIsCacheable_MaxAgeOverridesExpiry_ToAllowed()
|
||||||
{
|
{
|
||||||
var utcNow = DateTimeOffset.UtcNow;
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Response.StatusCode = StatusCodes.Status200OK;
|
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
|
||||||
var headers = httpContext.Response.GetTypedHeaders();
|
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
headers.CacheControl = new CacheControlHeaderValue()
|
|
||||||
{
|
{
|
||||||
Public = true,
|
Public = true,
|
||||||
MaxAge = TimeSpan.FromSeconds(10)
|
MaxAge = TimeSpan.FromSeconds(10)
|
||||||
};
|
};
|
||||||
headers.Expires = utcNow;
|
context.TypedResponseHeaders.Expires = utcNow;
|
||||||
headers.Date = utcNow;
|
context.TypedResponseHeaders.Date = utcNow;
|
||||||
httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(9);
|
context.ResponseTime = utcNow + TimeSpan.FromSeconds(9);
|
||||||
|
|
||||||
Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext));
|
Assert.True(new CacheabilityValidator().IsResponseCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResponseIsCacheable_MaxAgeOverridesExpiry_ToNotAllowed()
|
public void ResponseIsCacheable_MaxAgeOverridesExpiry_ToNotAllowed()
|
||||||
{
|
{
|
||||||
var utcNow = DateTimeOffset.UtcNow;
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Response.StatusCode = StatusCodes.Status200OK;
|
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
|
||||||
var headers = httpContext.Response.GetTypedHeaders();
|
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
headers.CacheControl = new CacheControlHeaderValue()
|
|
||||||
{
|
{
|
||||||
Public = true,
|
Public = true,
|
||||||
MaxAge = TimeSpan.FromSeconds(10)
|
MaxAge = TimeSpan.FromSeconds(10)
|
||||||
};
|
};
|
||||||
headers.Expires = utcNow;
|
context.TypedResponseHeaders.Expires = utcNow;
|
||||||
headers.Date = utcNow;
|
context.TypedResponseHeaders.Date = utcNow;
|
||||||
httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(11);
|
context.ResponseTime = utcNow + TimeSpan.FromSeconds(11);
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
|
Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResponseIsCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed()
|
public void ResponseIsCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed()
|
||||||
{
|
{
|
||||||
var utcNow = DateTimeOffset.UtcNow;
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Response.StatusCode = StatusCodes.Status200OK;
|
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
|
||||||
var headers = httpContext.Response.GetTypedHeaders();
|
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
headers.CacheControl = new CacheControlHeaderValue()
|
|
||||||
{
|
{
|
||||||
Public = true,
|
Public = true,
|
||||||
MaxAge = TimeSpan.FromSeconds(10),
|
MaxAge = TimeSpan.FromSeconds(10),
|
||||||
SharedMaxAge = TimeSpan.FromSeconds(15)
|
SharedMaxAge = TimeSpan.FromSeconds(15)
|
||||||
};
|
};
|
||||||
headers.Date = utcNow;
|
context.TypedResponseHeaders.Date = utcNow;
|
||||||
httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(11);
|
context.ResponseTime = utcNow + TimeSpan.FromSeconds(11);
|
||||||
|
|
||||||
Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext));
|
Assert.True(new CacheabilityValidator().IsResponseCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ResponseIsCacheable_SharedMaxAgeOverridesMaxAge_ToNotFresh()
|
public void ResponseIsCacheable_SharedMaxAgeOverridesMaxAge_ToNotFresh()
|
||||||
{
|
{
|
||||||
var utcNow = DateTimeOffset.UtcNow;
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Response.StatusCode = StatusCodes.Status200OK;
|
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
|
||||||
var headers = httpContext.Response.GetTypedHeaders();
|
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
headers.CacheControl = new CacheControlHeaderValue()
|
|
||||||
{
|
{
|
||||||
Public = true,
|
Public = true,
|
||||||
MaxAge = TimeSpan.FromSeconds(10),
|
MaxAge = TimeSpan.FromSeconds(10),
|
||||||
SharedMaxAge = TimeSpan.FromSeconds(5)
|
SharedMaxAge = TimeSpan.FromSeconds(5)
|
||||||
};
|
};
|
||||||
headers.Date = utcNow;
|
context.TypedResponseHeaders.Date = utcNow;
|
||||||
httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(6);
|
context.ResponseTime = utcNow + TimeSpan.FromSeconds(6);
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext));
|
Assert.False(new CacheabilityValidator().IsResponseCacheable(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EntryIsFresh_NoCachedCacheControl_FallsbackToEmptyCacheControl()
|
public void EntryIsFresh_NoCachedCacheControl_FallsbackToEmptyCacheControl()
|
||||||
{
|
{
|
||||||
var utcNow = DateTimeOffset.UtcNow;
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue;
|
context.ResponseTime = DateTimeOffset.MaxValue;
|
||||||
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
|
||||||
|
|
||||||
Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, new ResponseHeaders(new HeaderDictionary())));
|
Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EntryIsFresh_NoExpiryRequirements_IsFresh()
|
public void EntryIsFresh_NoExpiryRequirements_IsFresh()
|
||||||
{
|
{
|
||||||
var utcNow = DateTimeOffset.UtcNow;
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue;
|
context.ResponseTime = DateTimeOffset.MaxValue;
|
||||||
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
|
||||||
{
|
{
|
||||||
CacheControl = new CacheControlHeaderValue()
|
CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
|
|
@ -410,15 +404,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
|
Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EntryIsFresh_PastExpiry_IsNotFresh()
|
public void EntryIsFresh_PastExpiry_IsNotFresh()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue;
|
context.ResponseTime = DateTimeOffset.MaxValue;
|
||||||
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
|
||||||
{
|
{
|
||||||
CacheControl = new CacheControlHeaderValue()
|
CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
|
|
@ -427,18 +421,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
Expires = DateTimeOffset.UtcNow
|
Expires = DateTimeOffset.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
|
Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EntryIsFresh_MaxAgeOverridesExpiry_ToFresh()
|
public void EntryIsFresh_MaxAgeOverridesExpiry_ToFresh()
|
||||||
{
|
{
|
||||||
var utcNow = DateTimeOffset.UtcNow;
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
var state = httpContext.GetResponseCachingState();
|
context.CachedEntryAge = TimeSpan.FromSeconds(9);
|
||||||
state.CachedEntryAge = TimeSpan.FromSeconds(9);
|
context.ResponseTime = utcNow + context.CachedEntryAge;
|
||||||
state.ResponseTime = utcNow + state.CachedEntryAge;
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
|
||||||
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
|
|
||||||
{
|
{
|
||||||
CacheControl = new CacheControlHeaderValue()
|
CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
|
|
@ -448,18 +441,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
Expires = utcNow
|
Expires = utcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
|
Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EntryIsFresh_MaxAgeOverridesExpiry_ToNotFresh()
|
public void EntryIsFresh_MaxAgeOverridesExpiry_ToNotFresh()
|
||||||
{
|
{
|
||||||
var utcNow = DateTimeOffset.UtcNow;
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
var state = httpContext.GetResponseCachingState();
|
context.CachedEntryAge = TimeSpan.FromSeconds(11);
|
||||||
state.CachedEntryAge = TimeSpan.FromSeconds(11);
|
context.ResponseTime = utcNow + context.CachedEntryAge;
|
||||||
state.ResponseTime = utcNow + state.CachedEntryAge;
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
|
||||||
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
|
|
||||||
{
|
{
|
||||||
CacheControl = new CacheControlHeaderValue()
|
CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
|
|
@ -469,18 +461,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
Expires = utcNow
|
Expires = utcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
|
Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToFresh()
|
public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToFresh()
|
||||||
{
|
{
|
||||||
var utcNow = DateTimeOffset.UtcNow;
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
var state = httpContext.GetResponseCachingState();
|
context.CachedEntryAge = TimeSpan.FromSeconds(11);
|
||||||
state.CachedEntryAge = TimeSpan.FromSeconds(11);
|
context.ResponseTime = utcNow + context.CachedEntryAge;
|
||||||
state.ResponseTime = utcNow + state.CachedEntryAge;
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
|
||||||
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
|
|
||||||
{
|
{
|
||||||
CacheControl = new CacheControlHeaderValue()
|
CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
|
|
@ -491,18 +482,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
Expires = utcNow
|
Expires = utcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
|
Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh()
|
public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh()
|
||||||
{
|
{
|
||||||
var utcNow = DateTimeOffset.UtcNow;
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
var state = httpContext.GetResponseCachingState();
|
context.CachedEntryAge = TimeSpan.FromSeconds(6);
|
||||||
state.CachedEntryAge = TimeSpan.FromSeconds(6);
|
context.ResponseTime = utcNow + context.CachedEntryAge;
|
||||||
state.ResponseTime = utcNow + state.CachedEntryAge;
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
|
||||||
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
|
|
||||||
{
|
{
|
||||||
CacheControl = new CacheControlHeaderValue()
|
CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
|
|
@ -513,18 +503,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
Expires = utcNow
|
Expires = utcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
|
Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EntryIsFresh_MinFreshReducesFreshness_ToNotFresh()
|
public void EntryIsFresh_MinFreshReducesFreshness_ToNotFresh()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
MinFresh = TimeSpan.FromSeconds(3)
|
MinFresh = TimeSpan.FromSeconds(3)
|
||||||
};
|
};
|
||||||
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
|
||||||
{
|
{
|
||||||
CacheControl = new CacheControlHeaderValue()
|
CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
|
|
@ -532,64 +522,64 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
SharedMaxAge = TimeSpan.FromSeconds(5)
|
SharedMaxAge = TimeSpan.FromSeconds(5)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(3);
|
context.CachedEntryAge = TimeSpan.FromSeconds(3);
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
|
Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EntryIsFresh_RequestMaxAgeRestrictAge_ToNotFresh()
|
public void EntryIsFresh_RequestMaxAgeRestrictAge_ToNotFresh()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
MaxAge = TimeSpan.FromSeconds(5)
|
MaxAge = TimeSpan.FromSeconds(5)
|
||||||
};
|
};
|
||||||
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
|
||||||
{
|
{
|
||||||
CacheControl = new CacheControlHeaderValue()
|
CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
MaxAge = TimeSpan.FromSeconds(10),
|
MaxAge = TimeSpan.FromSeconds(10),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(6);
|
context.CachedEntryAge = TimeSpan.FromSeconds(6);
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
|
Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EntryIsFresh_MaxStaleOverridesFreshness_ToFresh()
|
public void EntryIsFresh_MaxStaleOverridesFreshness_ToFresh()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Request.GetTypedHeaders().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(10)
|
||||||
};
|
};
|
||||||
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
|
||||||
{
|
{
|
||||||
CacheControl = new CacheControlHeaderValue()
|
CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
MaxAge = TimeSpan.FromSeconds(5),
|
MaxAge = TimeSpan.FromSeconds(5),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(6);
|
context.CachedEntryAge = TimeSpan.FromSeconds(6);
|
||||||
|
|
||||||
Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
|
Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EntryIsFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh()
|
public void EntryIsFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Request.GetTypedHeaders().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(10)
|
||||||
};
|
};
|
||||||
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
|
||||||
{
|
{
|
||||||
CacheControl = new CacheControlHeaderValue()
|
CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
|
|
@ -597,21 +587,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
MustRevalidate = true
|
MustRevalidate = true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(6);
|
context.CachedEntryAge = TimeSpan.FromSeconds(6);
|
||||||
|
|
||||||
Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
|
Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void EntryIsFresh_IgnoresRequestVerificationWhenSpecified()
|
public void EntryIsFresh_IgnoresRequestVerificationWhenSpecified()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
MinFresh = TimeSpan.FromSeconds(1),
|
MinFresh = TimeSpan.FromSeconds(1),
|
||||||
MaxAge = TimeSpan.FromSeconds(3)
|
MaxAge = TimeSpan.FromSeconds(3)
|
||||||
};
|
};
|
||||||
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
|
||||||
{
|
{
|
||||||
CacheControl = new CacheControlHeaderValue()
|
CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
|
|
@ -619,16 +609,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
SharedMaxAge = TimeSpan.FromSeconds(5)
|
SharedMaxAge = TimeSpan.FromSeconds(5)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(3);
|
context.CachedEntryAge = TimeSpan.FromSeconds(3);
|
||||||
|
|
||||||
Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders));
|
Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context));
|
||||||
}
|
|
||||||
|
|
||||||
private static HttpContext CreateDefaultContext()
|
|
||||||
{
|
|
||||||
var context = new DefaultHttpContext();
|
|
||||||
context.AddResponseCachingState();
|
|
||||||
return context;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,17 +21,5 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
// Should throw
|
// Should throw
|
||||||
Assert.ThrowsAny<InvalidOperationException>(() => httpContext.AddResponseCachingFeature());
|
Assert.ThrowsAny<InvalidOperationException>(() => httpContext.AddResponseCachingFeature());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void AddingSecondResponseCachingState_Throws()
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
|
|
||||||
// Should not throw
|
|
||||||
httpContext.AddResponseCachingState();
|
|
||||||
|
|
||||||
// Should throw
|
|
||||||
Assert.ThrowsAny<InvalidOperationException>(() => httpContext.AddResponseCachingState());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.ResponseCaching.Internal;
|
using Microsoft.AspNetCore.ResponseCaching.Internal;
|
||||||
using Microsoft.Extensions.ObjectPool;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
|
|
@ -13,156 +12,155 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
public class DefaultKeyProviderTests
|
public class DefaultKeyProviderTests
|
||||||
{
|
{
|
||||||
private static readonly char KeyDelimiter = '\x1e';
|
private static readonly char KeyDelimiter = '\x1e';
|
||||||
private static readonly CachedVaryRules TestVaryRules = new CachedVaryRules()
|
|
||||||
{
|
|
||||||
VaryKeyPrefix = FastGuid.NewGuid().IdString
|
|
||||||
};
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DefaultKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodAndPath()
|
public void DefaultKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodAndPath()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
|
||||||
httpContext.Request.Method = "head";
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Request.Path = "/path/subpath";
|
context.HttpContext.Request.Method = "head";
|
||||||
httpContext.Request.Scheme = "https";
|
context.HttpContext.Request.Path = "/path/subpath";
|
||||||
httpContext.Request.Host = new HostString("example.com", 80);
|
context.HttpContext.Request.Scheme = "https";
|
||||||
httpContext.Request.PathBase = "/pathBase";
|
context.HttpContext.Request.Host = new HostString("example.com", 80);
|
||||||
httpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b");
|
context.HttpContext.Request.PathBase = "/pathBase";
|
||||||
var keyProvider = CreateTestKeyProvider();
|
context.HttpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b");
|
||||||
|
|
||||||
Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", keyProvider.CreateStorageBaseKey(httpContext));
|
Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", cacheKeyProvider.CreateStorageBaseKey(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DefaultKeyProvider_CreateStorageBaseKey_CaseInsensitivePath_NormalizesPath()
|
public void DefaultKeyProvider_CreateStorageBaseKey_CaseInsensitivePath_NormalizesPath()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions()
|
||||||
httpContext.Request.Method = "GET";
|
|
||||||
httpContext.Request.Path = "/Path";
|
|
||||||
var keyProvider = CreateTestKeyProvider(new ResponseCachingOptions()
|
|
||||||
{
|
{
|
||||||
CaseSensitivePaths = false
|
CaseSensitivePaths = false
|
||||||
});
|
});
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.HttpContext.Request.Method = "GET";
|
||||||
|
context.HttpContext.Request.Path = "/Path";
|
||||||
|
|
||||||
Assert.Equal($"GET{KeyDelimiter}/PATH", keyProvider.CreateStorageBaseKey(httpContext));
|
Assert.Equal($"GET{KeyDelimiter}/PATH", cacheKeyProvider.CreateStorageBaseKey(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DefaultKeyProvider_CreateStorageBaseKey_CaseSensitivePath_PreservesPathCase()
|
public void DefaultKeyProvider_CreateStorageBaseKey_CaseSensitivePath_PreservesPathCase()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions()
|
||||||
httpContext.Request.Method = "GET";
|
|
||||||
httpContext.Request.Path = "/Path";
|
|
||||||
var keyProvider = CreateTestKeyProvider(new ResponseCachingOptions()
|
|
||||||
{
|
{
|
||||||
CaseSensitivePaths = true
|
CaseSensitivePaths = true
|
||||||
});
|
});
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.HttpContext.Request.Method = "GET";
|
||||||
|
context.HttpContext.Request.Path = "/Path";
|
||||||
|
|
||||||
Assert.Equal($"GET{KeyDelimiter}/Path", keyProvider.CreateStorageBaseKey(httpContext));
|
Assert.Equal($"GET{KeyDelimiter}/Path", cacheKeyProvider.CreateStorageBaseKey(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DefaultKeyProvider_CreateStorageVaryKey_ReturnsCachedVaryGuid_IfVaryRulesIsNullOrEmpty()
|
public void DefaultKeyProvider_CreateStorageVaryKey_Throws_IfVaryRulesIsNull()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
|
||||||
var keyProvider = CreateTestKeyProvider();
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateStorageVaryKey(httpContext, null));
|
Assert.Throws<InvalidOperationException>(() => cacheKeyProvider.CreateStorageVaryKey(context));
|
||||||
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateStorageVaryKey(httpContext, new VaryRules()));
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DefaultKeyProvider_CreateStorageVaryKey_ReturnsCachedVaryGuid_IfVaryRulesIsEmpty()
|
||||||
|
{
|
||||||
|
var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.CachedVaryRules = new CachedVaryRules()
|
||||||
|
{
|
||||||
|
VaryKeyPrefix = FastGuid.NewGuid().IdString
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}", cacheKeyProvider.CreateStorageVaryKey(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly()
|
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
|
||||||
httpContext.Request.Headers["HeaderA"] = "ValueA";
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Request.Headers["HeaderB"] = "ValueB";
|
context.HttpContext.Request.Headers["HeaderA"] = "ValueA";
|
||||||
var keyProvider = CreateTestKeyProvider();
|
context.HttpContext.Request.Headers["HeaderB"] = "ValueB";
|
||||||
|
context.CachedVaryRules = new CachedVaryRules()
|
||||||
|
{
|
||||||
|
Headers = new string[] { "HeaderA", "HeaderC" }
|
||||||
|
};
|
||||||
|
|
||||||
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null",
|
Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=",
|
||||||
keyProvider.CreateStorageVaryKey(httpContext, new VaryRules()
|
cacheKeyProvider.CreateStorageVaryKey(context));
|
||||||
{
|
|
||||||
Headers = new string[] { "HeaderA", "HeaderC" }
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedParamsOnly()
|
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedParamsOnly()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
|
||||||
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
|
var context = TestUtils.CreateTestContext();
|
||||||
var keyProvider = CreateTestKeyProvider();
|
context.HttpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
|
||||||
|
context.CachedVaryRules = new CachedVaryRules()
|
||||||
|
{
|
||||||
|
VaryKeyPrefix = FastGuid.NewGuid().IdString,
|
||||||
|
Params = new string[] { "ParamA", "ParamC" }
|
||||||
|
};
|
||||||
|
|
||||||
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null",
|
Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=",
|
||||||
keyProvider.CreateStorageVaryKey(httpContext, new VaryRules()
|
cacheKeyProvider.CreateStorageVaryKey(context));
|
||||||
{
|
|
||||||
Params = new string[] { "ParamA", "ParamC" }
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesParams_ParamNameCaseInsensitive_UseParamCasing()
|
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesParams_ParamNameCaseInsensitive_UseParamCasing()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
|
||||||
httpContext.Request.QueryString = new QueryString("?parama=ValueA¶mB=ValueB");
|
var context = TestUtils.CreateTestContext();
|
||||||
var keyProvider = CreateTestKeyProvider();
|
context.HttpContext.Request.QueryString = new QueryString("?parama=ValueA¶mB=ValueB");
|
||||||
|
context.CachedVaryRules = new CachedVaryRules()
|
||||||
|
{
|
||||||
|
VaryKeyPrefix = FastGuid.NewGuid().IdString,
|
||||||
|
Params = new string[] { "ParamA", "ParamC" }
|
||||||
|
};
|
||||||
|
|
||||||
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null",
|
Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=",
|
||||||
keyProvider.CreateStorageVaryKey(httpContext, new VaryRules()
|
cacheKeyProvider.CreateStorageVaryKey(context));
|
||||||
{
|
|
||||||
Params = new string[] { "ParamA", "ParamC" }
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesAllQueryParamsGivenAsterisk()
|
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesAllQueryParamsGivenAsterisk()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
|
||||||
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
|
var context = TestUtils.CreateTestContext();
|
||||||
var keyProvider = CreateTestKeyProvider();
|
context.HttpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
|
||||||
|
context.CachedVaryRules = new CachedVaryRules()
|
||||||
|
{
|
||||||
|
VaryKeyPrefix = FastGuid.NewGuid().IdString,
|
||||||
|
Params = new string[] { "*" }
|
||||||
|
};
|
||||||
|
|
||||||
// To support case insensitivity, all param keys are converted to upper case.
|
// To support case insensitivity, all param keys are converted to upper case.
|
||||||
// Explicit params uses the casing specified in the setting.
|
// Explicit params uses the casing specified in the setting.
|
||||||
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB",
|
Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB",
|
||||||
keyProvider.CreateStorageVaryKey(httpContext, new VaryRules()
|
cacheKeyProvider.CreateStorageVaryKey(context));
|
||||||
{
|
|
||||||
Params = new string[] { "*" }
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndParams()
|
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndParams()
|
||||||
{
|
{
|
||||||
var httpContext = CreateDefaultContext();
|
var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
|
||||||
httpContext.Request.Headers["HeaderA"] = "ValueA";
|
var context = TestUtils.CreateTestContext();
|
||||||
httpContext.Request.Headers["HeaderB"] = "ValueB";
|
context.HttpContext.Request.Headers["HeaderA"] = "ValueA";
|
||||||
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
|
context.HttpContext.Request.Headers["HeaderB"] = "ValueB";
|
||||||
var keyProvider = CreateTestKeyProvider();
|
context.HttpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
|
||||||
|
context.CachedVaryRules = new CachedVaryRules()
|
||||||
|
{
|
||||||
|
VaryKeyPrefix = FastGuid.NewGuid().IdString,
|
||||||
|
Headers = new string[] { "HeaderA", "HeaderC" },
|
||||||
|
Params = new string[] { "ParamA", "ParamC" }
|
||||||
|
};
|
||||||
|
|
||||||
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null",
|
Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=",
|
||||||
keyProvider.CreateStorageVaryKey(httpContext, new VaryRules()
|
cacheKeyProvider.CreateStorageVaryKey(context));
|
||||||
{
|
|
||||||
Headers = new string[] { "HeaderA", "HeaderC" },
|
|
||||||
Params = new string[] { "ParamA", "ParamC" }
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HttpContext CreateDefaultContext()
|
|
||||||
{
|
|
||||||
var context = new DefaultHttpContext();
|
|
||||||
context.AddResponseCachingState();
|
|
||||||
context.GetResponseCachingState().CachedVaryRules = TestVaryRules;
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IKeyProvider CreateTestKeyProvider()
|
|
||||||
{
|
|
||||||
return CreateTestKeyProvider(new ResponseCachingOptions());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IKeyProvider CreateTestKeyProvider(ResponseCachingOptions options)
|
|
||||||
{
|
|
||||||
return new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,742 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
|
||||||
using Microsoft.AspNetCore.Http.Headers;
|
|
||||||
using Microsoft.AspNetCore.ResponseCaching.Internal;
|
|
||||||
using Microsoft.Extensions.Internal;
|
|
||||||
using Microsoft.Extensions.ObjectPool;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Microsoft.Extensions.Primitives;
|
|
||||||
using Microsoft.Net.Http.Headers;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|
||||||
{
|
|
||||||
public class ResponseCachingContextTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504()
|
|
||||||
{
|
|
||||||
var cache = new TestResponseCache();
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider());
|
|
||||||
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
|
||||||
{
|
|
||||||
OnlyIfCached = true
|
|
||||||
};
|
|
||||||
|
|
||||||
Assert.True(await context.TryServeFromCacheAsync());
|
|
||||||
Assert.Equal(StatusCodes.Status504GatewayTimeout, httpContext.Response.StatusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails()
|
|
||||||
{
|
|
||||||
var cache = new TestResponseCache();
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }));
|
|
||||||
|
|
||||||
Assert.False(await context.TryServeFromCacheAsync());
|
|
||||||
Assert.Equal(2, cache.GetCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds()
|
|
||||||
{
|
|
||||||
var cache = new TestResponseCache();
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }));
|
|
||||||
|
|
||||||
cache.Set(
|
|
||||||
"BaseKey2",
|
|
||||||
new CachedResponse()
|
|
||||||
{
|
|
||||||
Body = new byte[0]
|
|
||||||
},
|
|
||||||
TimeSpan.Zero);
|
|
||||||
|
|
||||||
Assert.True(await context.TryServeFromCacheAsync());
|
|
||||||
Assert.Equal(2, cache.GetCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseNotFound_Fails()
|
|
||||||
{
|
|
||||||
var cache = new TestResponseCache();
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }));
|
|
||||||
|
|
||||||
cache.Set(
|
|
||||||
"BaseKey2",
|
|
||||||
new CachedVaryRules(),
|
|
||||||
TimeSpan.Zero);
|
|
||||||
|
|
||||||
Assert.False(await context.TryServeFromCacheAsync());
|
|
||||||
Assert.Equal(2, cache.GetCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseFound_Succeeds()
|
|
||||||
{
|
|
||||||
var cache = new TestResponseCache();
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }, new[] { "VaryKey", "VaryKey2" }));
|
|
||||||
|
|
||||||
cache.Set(
|
|
||||||
"BaseKey2",
|
|
||||||
new CachedVaryRules(),
|
|
||||||
TimeSpan.Zero);
|
|
||||||
cache.Set(
|
|
||||||
"BaseKey2VaryKey2",
|
|
||||||
new CachedResponse()
|
|
||||||
{
|
|
||||||
Body = new byte[0]
|
|
||||||
},
|
|
||||||
TimeSpan.Zero);
|
|
||||||
|
|
||||||
Assert.True(await context.TryServeFromCacheAsync());
|
|
||||||
Assert.Equal(6, cache.GetCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ConditionalRequestSatisfied_NotConditionalRequest_Fails()
|
|
||||||
{
|
|
||||||
var context = CreateTestContext(new DefaultHttpContext());
|
|
||||||
var cachedHeaders = new ResponseHeaders(new HeaderDictionary());
|
|
||||||
|
|
||||||
Assert.False(context.ConditionalRequestSatisfied(cachedHeaders));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ConditionalRequestSatisfied_IfUnmodifiedSince_FallsbackToDateHeader()
|
|
||||||
{
|
|
||||||
var utcNow = DateTimeOffset.UtcNow;
|
|
||||||
var cachedHeaders = new ResponseHeaders(new HeaderDictionary());
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var context = CreateTestContext(httpContext);
|
|
||||||
|
|
||||||
httpContext.Request.GetTypedHeaders().IfUnmodifiedSince = utcNow;
|
|
||||||
|
|
||||||
// Verify modifications in the past succeeds
|
|
||||||
cachedHeaders.Date = utcNow - TimeSpan.FromSeconds(10);
|
|
||||||
Assert.True(context.ConditionalRequestSatisfied(cachedHeaders));
|
|
||||||
|
|
||||||
// Verify modifications at present succeeds
|
|
||||||
cachedHeaders.Date = utcNow;
|
|
||||||
Assert.True(context.ConditionalRequestSatisfied(cachedHeaders));
|
|
||||||
|
|
||||||
// Verify modifications in the future fails
|
|
||||||
cachedHeaders.Date = utcNow + TimeSpan.FromSeconds(10);
|
|
||||||
Assert.False(context.ConditionalRequestSatisfied(cachedHeaders));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ConditionalRequestSatisfied_IfUnmodifiedSince_LastModifiedOverridesDateHeader()
|
|
||||||
{
|
|
||||||
var utcNow = DateTimeOffset.UtcNow;
|
|
||||||
var cachedHeaders = new ResponseHeaders(new HeaderDictionary());
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var context = CreateTestContext(httpContext);
|
|
||||||
|
|
||||||
httpContext.Request.GetTypedHeaders().IfUnmodifiedSince = utcNow;
|
|
||||||
|
|
||||||
// Verify modifications in the past succeeds
|
|
||||||
cachedHeaders.Date = utcNow + TimeSpan.FromSeconds(10);
|
|
||||||
cachedHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10);
|
|
||||||
Assert.True(context.ConditionalRequestSatisfied(cachedHeaders));
|
|
||||||
|
|
||||||
// Verify modifications at present
|
|
||||||
cachedHeaders.Date = utcNow + TimeSpan.FromSeconds(10);
|
|
||||||
cachedHeaders.LastModified = utcNow;
|
|
||||||
Assert.True(context.ConditionalRequestSatisfied(cachedHeaders));
|
|
||||||
|
|
||||||
// Verify modifications in the future fails
|
|
||||||
cachedHeaders.Date = utcNow - TimeSpan.FromSeconds(10);
|
|
||||||
cachedHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10);
|
|
||||||
Assert.False(context.ConditionalRequestSatisfied(cachedHeaders));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToPass()
|
|
||||||
{
|
|
||||||
var utcNow = DateTimeOffset.UtcNow;
|
|
||||||
var cachedHeaders = new ResponseHeaders(new HeaderDictionary());
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
|
||||||
var context = CreateTestContext(httpContext);
|
|
||||||
|
|
||||||
// This would fail the IfUnmodifiedSince checks
|
|
||||||
requestHeaders.IfUnmodifiedSince = utcNow;
|
|
||||||
cachedHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
requestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { EntityTagHeaderValue.Any });
|
|
||||||
Assert.True(context.ConditionalRequestSatisfied(cachedHeaders));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToFail()
|
|
||||||
{
|
|
||||||
var utcNow = DateTimeOffset.UtcNow;
|
|
||||||
var cachedHeaders = new ResponseHeaders(new HeaderDictionary());
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var requestHeaders = httpContext.Request.GetTypedHeaders();
|
|
||||||
var context = CreateTestContext(httpContext);
|
|
||||||
|
|
||||||
// This would pass the IfUnmodifiedSince checks
|
|
||||||
requestHeaders.IfUnmodifiedSince = utcNow;
|
|
||||||
cachedHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
requestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
|
|
||||||
Assert.False(context.ConditionalRequestSatisfied(cachedHeaders));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ConditionalRequestSatisfied_IfNoneMatch_AnyWithoutETagInResponse_Passes()
|
|
||||||
{
|
|
||||||
var cachedHeaders = new ResponseHeaders(new HeaderDictionary());
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var context = CreateTestContext(httpContext);
|
|
||||||
|
|
||||||
httpContext.Request.GetTypedHeaders().IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
|
|
||||||
|
|
||||||
Assert.False(context.ConditionalRequestSatisfied(cachedHeaders));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithMatch_Passes()
|
|
||||||
{
|
|
||||||
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
|
|
||||||
{
|
|
||||||
ETag = new EntityTagHeaderValue("\"E1\"")
|
|
||||||
};
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var context = CreateTestContext(httpContext);
|
|
||||||
|
|
||||||
httpContext.Request.GetTypedHeaders().IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
|
|
||||||
|
|
||||||
Assert.True(context.ConditionalRequestSatisfied(cachedHeaders));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithoutMatch_Fails()
|
|
||||||
{
|
|
||||||
var cachedHeaders = new ResponseHeaders(new HeaderDictionary())
|
|
||||||
{
|
|
||||||
ETag = new EntityTagHeaderValue("\"E2\"")
|
|
||||||
};
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var context = CreateTestContext(httpContext);
|
|
||||||
|
|
||||||
httpContext.Request.GetTypedHeaders().IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
|
|
||||||
|
|
||||||
Assert.False(context.ConditionalRequestSatisfied(cachedHeaders));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FinalizeCachingHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable()
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var context = CreateTestContext(httpContext, cacheabilityValidator: new CacheabilityValidator());
|
|
||||||
var state = httpContext.GetResponseCachingState();
|
|
||||||
|
|
||||||
Assert.False(state.ShouldCacheResponse);
|
|
||||||
|
|
||||||
context.ShimResponseStream();
|
|
||||||
context.FinalizeCachingHeaders();
|
|
||||||
|
|
||||||
Assert.False(state.ShouldCacheResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FinalizeCachingHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable()
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
|
||||||
{
|
|
||||||
Public = true
|
|
||||||
};
|
|
||||||
var context = CreateTestContext(httpContext, cacheabilityValidator: new CacheabilityValidator());
|
|
||||||
var state = httpContext.GetResponseCachingState();
|
|
||||||
|
|
||||||
Assert.False(state.ShouldCacheResponse);
|
|
||||||
|
|
||||||
context.FinalizeCachingHeaders();
|
|
||||||
|
|
||||||
Assert.True(state.ShouldCacheResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FinalizeCachingHeaders_DefaultResponseValidity_Is10Seconds()
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var context = CreateTestContext(httpContext);
|
|
||||||
|
|
||||||
context.FinalizeCachingHeaders();
|
|
||||||
|
|
||||||
Assert.Equal(TimeSpan.FromSeconds(10), httpContext.GetResponseCachingState().CachedResponseValidFor);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FinalizeCachingHeaders_ResponseValidity_UseExpiryIfAvailable()
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var context = CreateTestContext(httpContext);
|
|
||||||
|
|
||||||
var state = httpContext.GetResponseCachingState();
|
|
||||||
var utcNow = DateTimeOffset.MinValue;
|
|
||||||
state.ResponseTime = utcNow;
|
|
||||||
state.ResponseHeaders.Expires = utcNow + TimeSpan.FromSeconds(11);
|
|
||||||
|
|
||||||
context.FinalizeCachingHeaders();
|
|
||||||
|
|
||||||
Assert.Equal(TimeSpan.FromSeconds(11), state.CachedResponseValidFor);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FinalizeCachingHeaders_ResponseValidity_UseMaxAgeIfAvailable()
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
|
||||||
{
|
|
||||||
MaxAge = TimeSpan.FromSeconds(12)
|
|
||||||
};
|
|
||||||
var context = CreateTestContext(httpContext);
|
|
||||||
|
|
||||||
var state = httpContext.GetResponseCachingState();
|
|
||||||
state.ResponseTime = DateTimeOffset.UtcNow;
|
|
||||||
state.ResponseHeaders.Expires = state.ResponseTime + TimeSpan.FromSeconds(11);
|
|
||||||
|
|
||||||
context.FinalizeCachingHeaders();
|
|
||||||
|
|
||||||
Assert.Equal(TimeSpan.FromSeconds(12), state.CachedResponseValidFor);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FinalizeCachingHeaders_ResponseValidity_UseSharedMaxAgeIfAvailable()
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
|
||||||
{
|
|
||||||
MaxAge = TimeSpan.FromSeconds(12),
|
|
||||||
SharedMaxAge = TimeSpan.FromSeconds(13)
|
|
||||||
};
|
|
||||||
var context = CreateTestContext(httpContext);
|
|
||||||
|
|
||||||
var state = httpContext.GetResponseCachingState();
|
|
||||||
state.ResponseTime = DateTimeOffset.UtcNow;
|
|
||||||
state.ResponseHeaders.Expires = state.ResponseTime + TimeSpan.FromSeconds(11);
|
|
||||||
|
|
||||||
context.FinalizeCachingHeaders();
|
|
||||||
|
|
||||||
Assert.Equal(TimeSpan.FromSeconds(13), state.CachedResponseValidFor);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FinalizeCachingHeaders_UpdateCachedVaryRules_IfNotEquivalentToPrevious()
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var cache = new TestResponseCache();
|
|
||||||
var context = CreateTestContext(httpContext, cache);
|
|
||||||
var state = httpContext.GetResponseCachingState();
|
|
||||||
|
|
||||||
httpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" });
|
|
||||||
httpContext.AddResponseCachingFeature();
|
|
||||||
httpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMAA" });
|
|
||||||
var cachedVaryRules = new CachedVaryRules()
|
|
||||||
{
|
|
||||||
VaryRules = new VaryRules()
|
|
||||||
{
|
|
||||||
Headers = new StringValues(new[] { "HeaderA", "HeaderB" }),
|
|
||||||
Params = new StringValues(new[] { "ParamA", "ParamB" })
|
|
||||||
}
|
|
||||||
};
|
|
||||||
state.CachedVaryRules = cachedVaryRules;
|
|
||||||
|
|
||||||
context.FinalizeCachingHeaders();
|
|
||||||
|
|
||||||
Assert.Equal(1, cache.SetCount);
|
|
||||||
Assert.NotSame(cachedVaryRules, state.CachedVaryRules);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FinalizeCachingHeaders_DoNotUpdateCachedVaryRules_IfEquivalentToPrevious()
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var cache = new TestResponseCache();
|
|
||||||
var context = CreateTestContext(httpContext, cache);
|
|
||||||
var state = httpContext.GetResponseCachingState();
|
|
||||||
|
|
||||||
httpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB" });
|
|
||||||
httpContext.AddResponseCachingFeature();
|
|
||||||
httpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMA" });
|
|
||||||
var cachedVaryRules = new CachedVaryRules()
|
|
||||||
{
|
|
||||||
VaryKeyPrefix = FastGuid.NewGuid().IdString,
|
|
||||||
VaryRules = new VaryRules()
|
|
||||||
{
|
|
||||||
Headers = new StringValues(new[] { "HEADERA", "HEADERB" }),
|
|
||||||
Params = new StringValues(new[] { "PARAMA", "PARAMB" })
|
|
||||||
}
|
|
||||||
};
|
|
||||||
state.CachedVaryRules = cachedVaryRules;
|
|
||||||
|
|
||||||
context.FinalizeCachingHeaders();
|
|
||||||
|
|
||||||
Assert.Equal(0, cache.SetCount);
|
|
||||||
Assert.Same(cachedVaryRules, state.CachedVaryRules);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FinalizeCachingHeaders_DoNotAddDate_IfSpecified()
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var context = CreateTestContext(httpContext);
|
|
||||||
var state = httpContext.GetResponseCachingState();
|
|
||||||
var utcNow = DateTimeOffset.MinValue;
|
|
||||||
state.ResponseTime = utcNow;
|
|
||||||
|
|
||||||
Assert.Null(state.ResponseHeaders.Date);
|
|
||||||
|
|
||||||
context.FinalizeCachingHeaders();
|
|
||||||
|
|
||||||
Assert.Equal(utcNow, state.ResponseHeaders.Date);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FinalizeCachingHeaders_AddsDate_IfNoneSpecified()
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var context = CreateTestContext(httpContext);
|
|
||||||
var state = httpContext.GetResponseCachingState();
|
|
||||||
var utcNow = DateTimeOffset.MinValue;
|
|
||||||
state.ResponseHeaders.Date = utcNow;
|
|
||||||
state.ResponseTime = utcNow + TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
Assert.Equal(utcNow, state.ResponseHeaders.Date);
|
|
||||||
|
|
||||||
context.FinalizeCachingHeaders();
|
|
||||||
|
|
||||||
Assert.Equal(utcNow, state.ResponseHeaders.Date);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FinalizeCachingHeaders_StoresCachedResponse_InState()
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var context = CreateTestContext(httpContext);
|
|
||||||
var state = httpContext.GetResponseCachingState();
|
|
||||||
|
|
||||||
Assert.Null(state.CachedResponse);
|
|
||||||
|
|
||||||
context.FinalizeCachingHeaders();
|
|
||||||
|
|
||||||
Assert.NotNull(state.CachedResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task FinalizeCachingBody_StoreResponseBodySeparately_IfLargerThanLimit()
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var cache = new TestResponseCache();
|
|
||||||
var context = CreateTestContext(httpContext, cache);
|
|
||||||
|
|
||||||
context.ShimResponseStream();
|
|
||||||
await httpContext.Response.WriteAsync(new string('0', 70 * 1024));
|
|
||||||
|
|
||||||
var state = httpContext.GetResponseCachingState();
|
|
||||||
state.ShouldCacheResponse = true;
|
|
||||||
state.CachedResponse = new CachedResponse()
|
|
||||||
{
|
|
||||||
BodyKeyPrefix = FastGuid.NewGuid().IdString
|
|
||||||
};
|
|
||||||
state.StorageBaseKey = "BaseKey";
|
|
||||||
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
context.FinalizeCachingBody();
|
|
||||||
|
|
||||||
Assert.Equal(2, cache.SetCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task FinalizeCachingBody_StoreResponseBodyInCachedResponse_IfSmallerThanLimit()
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var cache = new TestResponseCache();
|
|
||||||
var context = CreateTestContext(httpContext, cache);
|
|
||||||
|
|
||||||
context.ShimResponseStream();
|
|
||||||
await httpContext.Response.WriteAsync(new string('0', 70 * 1024 - 1));
|
|
||||||
|
|
||||||
var state = httpContext.GetResponseCachingState();
|
|
||||||
state.ShouldCacheResponse = true;
|
|
||||||
state.CachedResponse = new CachedResponse()
|
|
||||||
{
|
|
||||||
BodyKeyPrefix = FastGuid.NewGuid().IdString
|
|
||||||
};
|
|
||||||
state.StorageBaseKey = "BaseKey";
|
|
||||||
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
context.FinalizeCachingBody();
|
|
||||||
|
|
||||||
Assert.Equal(1, cache.SetCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task FinalizeCachingBody_StoreResponseBodySeparately_LimitIsConfigurable()
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var cache = new TestResponseCache();
|
|
||||||
var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions()
|
|
||||||
{
|
|
||||||
MinimumSplitBodySize = 2048
|
|
||||||
});
|
|
||||||
|
|
||||||
context.ShimResponseStream();
|
|
||||||
await httpContext.Response.WriteAsync(new string('0', 1024));
|
|
||||||
|
|
||||||
var state = httpContext.GetResponseCachingState();
|
|
||||||
state.ShouldCacheResponse = true;
|
|
||||||
state.CachedResponse = new CachedResponse()
|
|
||||||
{
|
|
||||||
BodyKeyPrefix = FastGuid.NewGuid().IdString
|
|
||||||
};
|
|
||||||
state.StorageBaseKey = "BaseKey";
|
|
||||||
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
context.FinalizeCachingBody();
|
|
||||||
|
|
||||||
Assert.Equal(1, cache.SetCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task FinalizeCachingBody_Cache_IfContentLengthMatches()
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var cache = new TestResponseCache();
|
|
||||||
var context = CreateTestContext(httpContext, cache);
|
|
||||||
|
|
||||||
context.ShimResponseStream();
|
|
||||||
httpContext.Response.ContentLength = 10;
|
|
||||||
await httpContext.Response.WriteAsync(new string('0', 10));
|
|
||||||
|
|
||||||
var state = httpContext.GetResponseCachingState();
|
|
||||||
state.ShouldCacheResponse = true;
|
|
||||||
state.CachedResponse = new CachedResponse()
|
|
||||||
{
|
|
||||||
BodyKeyPrefix = FastGuid.NewGuid().IdString
|
|
||||||
};
|
|
||||||
state.StorageBaseKey = "BaseKey";
|
|
||||||
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
context.FinalizeCachingBody();
|
|
||||||
|
|
||||||
Assert.Equal(1, cache.SetCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task FinalizeCachingBody_DoNotCache_IfContentLengthMismatches()
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var cache = new TestResponseCache();
|
|
||||||
var context = CreateTestContext(httpContext, cache);
|
|
||||||
|
|
||||||
context.ShimResponseStream();
|
|
||||||
httpContext.Response.ContentLength = 9;
|
|
||||||
await httpContext.Response.WriteAsync(new string('0', 10));
|
|
||||||
|
|
||||||
var state = httpContext.GetResponseCachingState();
|
|
||||||
state.ShouldCacheResponse = true;
|
|
||||||
state.CachedResponse = new CachedResponse()
|
|
||||||
{
|
|
||||||
BodyKeyPrefix = FastGuid.NewGuid().IdString
|
|
||||||
};
|
|
||||||
state.StorageBaseKey = "BaseKey";
|
|
||||||
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
context.FinalizeCachingBody();
|
|
||||||
|
|
||||||
Assert.Equal(0, cache.SetCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task FinalizeCachingBody_Cache_IfContentLengthAbsent()
|
|
||||||
{
|
|
||||||
var httpContext = new DefaultHttpContext();
|
|
||||||
var cache = new TestResponseCache();
|
|
||||||
var context = CreateTestContext(httpContext, cache);
|
|
||||||
|
|
||||||
context.ShimResponseStream();
|
|
||||||
await httpContext.Response.WriteAsync(new string('0', 10));
|
|
||||||
|
|
||||||
var state = httpContext.GetResponseCachingState();
|
|
||||||
state.ShouldCacheResponse = true;
|
|
||||||
state.CachedResponse = new CachedResponse()
|
|
||||||
{
|
|
||||||
BodyKeyPrefix = FastGuid.NewGuid().IdString
|
|
||||||
};
|
|
||||||
state.StorageBaseKey = "BaseKey";
|
|
||||||
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
|
|
||||||
|
|
||||||
context.FinalizeCachingBody();
|
|
||||||
|
|
||||||
Assert.Equal(1, cache.SetCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void NormalizeStringValues_NormalizesCasingToUpper()
|
|
||||||
{
|
|
||||||
var uppercaseStrings = new StringValues(new[] { "STRINGA", "STRINGB" });
|
|
||||||
var lowercaseStrings = new StringValues(new[] { "stringA", "stringB" });
|
|
||||||
|
|
||||||
var normalizedStrings = ResponseCachingContext.GetNormalizedStringValues(lowercaseStrings);
|
|
||||||
|
|
||||||
Assert.Equal(uppercaseStrings, normalizedStrings);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void NormalizeStringValues_NormalizesOrder()
|
|
||||||
{
|
|
||||||
var orderedStrings = new StringValues(new[] { "STRINGA", "STRINGB" });
|
|
||||||
var reverseOrderStrings = new StringValues(new[] { "STRINGB", "STRINGA" });
|
|
||||||
|
|
||||||
var normalizedStrings = ResponseCachingContext.GetNormalizedStringValues(reverseOrderStrings);
|
|
||||||
|
|
||||||
Assert.Equal(orderedStrings, normalizedStrings);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ResponseCachingContext CreateTestContext(
|
|
||||||
HttpContext httpContext,
|
|
||||||
IResponseCache responseCache = null,
|
|
||||||
ResponseCachingOptions options = null,
|
|
||||||
IKeyProvider keyProvider = null,
|
|
||||||
ICacheabilityValidator cacheabilityValidator = null)
|
|
||||||
{
|
|
||||||
if (responseCache == null)
|
|
||||||
{
|
|
||||||
responseCache = new TestResponseCache();
|
|
||||||
}
|
|
||||||
if (options == null)
|
|
||||||
{
|
|
||||||
options = new ResponseCachingOptions();
|
|
||||||
}
|
|
||||||
if (keyProvider == null)
|
|
||||||
{
|
|
||||||
keyProvider = new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options));
|
|
||||||
}
|
|
||||||
if (cacheabilityValidator == null)
|
|
||||||
{
|
|
||||||
cacheabilityValidator = new TestCacheabilityValidator();
|
|
||||||
}
|
|
||||||
|
|
||||||
httpContext.AddResponseCachingState();
|
|
||||||
|
|
||||||
return new ResponseCachingContext(
|
|
||||||
httpContext,
|
|
||||||
responseCache,
|
|
||||||
options,
|
|
||||||
cacheabilityValidator,
|
|
||||||
keyProvider);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TestCacheabilityValidator : ICacheabilityValidator
|
|
||||||
{
|
|
||||||
public bool CachedEntryIsFresh(HttpContext httpContext, ResponseHeaders cachedResponseHeaders) => true;
|
|
||||||
|
|
||||||
public bool RequestIsCacheable(HttpContext httpContext) => true;
|
|
||||||
|
|
||||||
public bool ResponseIsCacheable(HttpContext httpContext) => true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TestKeyProvider : IKeyProvider
|
|
||||||
{
|
|
||||||
private readonly StringValues _baseKey;
|
|
||||||
private readonly StringValues _varyKey;
|
|
||||||
|
|
||||||
public TestKeyProvider(StringValues? lookupBaseKey = null, StringValues? lookupVaryKey = null)
|
|
||||||
{
|
|
||||||
if (lookupBaseKey.HasValue)
|
|
||||||
{
|
|
||||||
_baseKey = lookupBaseKey.Value;
|
|
||||||
}
|
|
||||||
if (lookupVaryKey.HasValue)
|
|
||||||
{
|
|
||||||
_varyKey = lookupVaryKey.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<string> CreateLookupBaseKey(HttpContext httpContext) => _baseKey;
|
|
||||||
|
|
||||||
|
|
||||||
public IEnumerable<string> CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules)
|
|
||||||
{
|
|
||||||
foreach (var baseKey in _baseKey)
|
|
||||||
{
|
|
||||||
foreach (var varyKey in _varyKey)
|
|
||||||
{
|
|
||||||
yield return baseKey + varyKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string CreateBodyKey(HttpContext httpContext)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string CreateStorageBaseKey(HttpContext httpContext)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TestResponseCache : IResponseCache
|
|
||||||
{
|
|
||||||
private readonly IDictionary<string, object> _storage = new Dictionary<string, object>();
|
|
||||||
public int GetCount { get; private set; }
|
|
||||||
public int SetCount { get; private set; }
|
|
||||||
|
|
||||||
public object Get(string key)
|
|
||||||
{
|
|
||||||
GetCount++;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return _storage[key];
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Remove(string key)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Set(string key, object entry, TimeSpan validFor)
|
|
||||||
{
|
|
||||||
SetCount++;
|
|
||||||
_storage[key] = entry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TestHttpSendFileFeature : IHttpSendFileFeature
|
|
||||||
{
|
|
||||||
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
|
|
||||||
{
|
|
||||||
return TaskCache.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,578 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Headers;
|
||||||
|
using Microsoft.AspNetCore.ResponseCaching.Internal;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
|
{
|
||||||
|
public class ResponseCachingMiddlewareTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider());
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
OnlyIfCached = true
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.True(await middleware.TryServeFromCacheAsync(context));
|
||||||
|
Assert.Equal(StatusCodes.Status504GatewayTimeout, context.HttpContext.Response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }));
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
Assert.False(await middleware.TryServeFromCacheAsync(context));
|
||||||
|
Assert.Equal(2, cache.GetCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }));
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
cache.Set(
|
||||||
|
"BaseKey2",
|
||||||
|
new CachedResponse()
|
||||||
|
{
|
||||||
|
Body = new byte[0]
|
||||||
|
},
|
||||||
|
TimeSpan.Zero);
|
||||||
|
|
||||||
|
Assert.True(await middleware.TryServeFromCacheAsync(context));
|
||||||
|
Assert.Equal(2, cache.GetCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseNotFound_Fails()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }));
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
cache.Set(
|
||||||
|
"BaseKey2",
|
||||||
|
new CachedVaryRules(),
|
||||||
|
TimeSpan.Zero);
|
||||||
|
|
||||||
|
Assert.False(await middleware.TryServeFromCacheAsync(context));
|
||||||
|
Assert.Equal(2, cache.GetCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseFound_Succeeds()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }, new[] { "VaryKey", "VaryKey2" }));
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
cache.Set(
|
||||||
|
"BaseKey2",
|
||||||
|
new CachedVaryRules(),
|
||||||
|
TimeSpan.Zero);
|
||||||
|
cache.Set(
|
||||||
|
"BaseKey2VaryKey2",
|
||||||
|
new CachedResponse()
|
||||||
|
{
|
||||||
|
Body = new byte[0]
|
||||||
|
},
|
||||||
|
TimeSpan.Zero);
|
||||||
|
|
||||||
|
Assert.True(await middleware.TryServeFromCacheAsync(context));
|
||||||
|
Assert.Equal(6, cache.GetCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalRequestSatisfied_NotConditionalRequest_Fails()
|
||||||
|
{
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
|
||||||
|
|
||||||
|
Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalRequestSatisfied_IfUnmodifiedSince_FallsbackToDateHeader()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
|
||||||
|
|
||||||
|
context.TypedRequestHeaders.IfUnmodifiedSince = utcNow;
|
||||||
|
|
||||||
|
// Verify modifications in the past succeeds
|
||||||
|
context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10);
|
||||||
|
Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
|
||||||
|
|
||||||
|
// Verify modifications at present succeeds
|
||||||
|
context.CachedResponseHeaders.Date = utcNow;
|
||||||
|
Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
|
||||||
|
|
||||||
|
// Verify modifications in the future fails
|
||||||
|
context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10);
|
||||||
|
Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalRequestSatisfied_IfUnmodifiedSince_LastModifiedOverridesDateHeader()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
|
||||||
|
|
||||||
|
context.TypedRequestHeaders.IfUnmodifiedSince = utcNow;
|
||||||
|
|
||||||
|
// Verify modifications in the past succeeds
|
||||||
|
context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10);
|
||||||
|
context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10);
|
||||||
|
Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
|
||||||
|
|
||||||
|
// Verify modifications at present
|
||||||
|
context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10);
|
||||||
|
context.CachedResponseHeaders.LastModified = utcNow;
|
||||||
|
Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
|
||||||
|
|
||||||
|
// Verify modifications in the future fails
|
||||||
|
context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10);
|
||||||
|
context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10);
|
||||||
|
Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToPass()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
|
||||||
|
|
||||||
|
// This would fail the IfUnmodifiedSince checks
|
||||||
|
context.TypedRequestHeaders.IfUnmodifiedSince = utcNow;
|
||||||
|
context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { EntityTagHeaderValue.Any });
|
||||||
|
Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToFail()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
|
||||||
|
|
||||||
|
// This would pass the IfUnmodifiedSince checks
|
||||||
|
context.TypedRequestHeaders.IfUnmodifiedSince = utcNow;
|
||||||
|
context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
|
||||||
|
Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalRequestSatisfied_IfNoneMatch_AnyWithoutETagInResponse_Passes()
|
||||||
|
{
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary());
|
||||||
|
|
||||||
|
context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
|
||||||
|
|
||||||
|
Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithMatch_Passes()
|
||||||
|
{
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
|
||||||
|
{
|
||||||
|
ETag = new EntityTagHeaderValue("\"E1\"")
|
||||||
|
};
|
||||||
|
|
||||||
|
context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
|
||||||
|
|
||||||
|
Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithoutMatch_Fails()
|
||||||
|
{
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary())
|
||||||
|
{
|
||||||
|
ETag = new EntityTagHeaderValue("\"E2\"")
|
||||||
|
};
|
||||||
|
|
||||||
|
context.TypedRequestHeaders.IfNoneMatch = new List<EntityTagHeaderValue>(new[] { new EntityTagHeaderValue("\"E1\"") });
|
||||||
|
|
||||||
|
Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FinalizeCachingHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable()
|
||||||
|
{
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(cacheabilityValidator: new CacheabilityValidator());
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
Assert.False(context.ShouldCacheResponse);
|
||||||
|
|
||||||
|
middleware.ShimResponseStream(context);
|
||||||
|
middleware.FinalizeCachingHeaders(context);
|
||||||
|
|
||||||
|
Assert.False(context.ShouldCacheResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FinalizeCachingHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable()
|
||||||
|
{
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(cacheabilityValidator: new CacheabilityValidator());
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.False(context.ShouldCacheResponse);
|
||||||
|
|
||||||
|
middleware.FinalizeCachingHeaders(context);
|
||||||
|
|
||||||
|
Assert.True(context.ShouldCacheResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FinalizeCachingHeaders_DefaultResponseValidity_Is10Seconds()
|
||||||
|
{
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware();
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
middleware.FinalizeCachingHeaders(context);
|
||||||
|
|
||||||
|
Assert.Equal(TimeSpan.FromSeconds(10), context.CachedResponseValidFor);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FinalizeCachingHeaders_ResponseValidity_UseExpiryIfAvailable()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.MinValue;
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware();
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
context.ResponseTime = utcNow;
|
||||||
|
context.TypedResponseHeaders.Expires = utcNow + TimeSpan.FromSeconds(11);
|
||||||
|
|
||||||
|
middleware.FinalizeCachingHeaders(context);
|
||||||
|
|
||||||
|
Assert.Equal(TimeSpan.FromSeconds(11), context.CachedResponseValidFor);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FinalizeCachingHeaders_ResponseValidity_UseMaxAgeIfAvailable()
|
||||||
|
{
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware();
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
MaxAge = TimeSpan.FromSeconds(12)
|
||||||
|
};
|
||||||
|
|
||||||
|
context.ResponseTime = DateTimeOffset.UtcNow;
|
||||||
|
context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11);
|
||||||
|
|
||||||
|
middleware.FinalizeCachingHeaders(context);
|
||||||
|
|
||||||
|
Assert.Equal(TimeSpan.FromSeconds(12), context.CachedResponseValidFor);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FinalizeCachingHeaders_ResponseValidity_UseSharedMaxAgeIfAvailable()
|
||||||
|
{
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware();
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
MaxAge = TimeSpan.FromSeconds(12),
|
||||||
|
SharedMaxAge = TimeSpan.FromSeconds(13)
|
||||||
|
};
|
||||||
|
|
||||||
|
context.ResponseTime = DateTimeOffset.UtcNow;
|
||||||
|
context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11);
|
||||||
|
|
||||||
|
middleware.FinalizeCachingHeaders(context);
|
||||||
|
|
||||||
|
Assert.Equal(TimeSpan.FromSeconds(13), context.CachedResponseValidFor);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FinalizeCachingHeaders_UpdateCachedVaryRules_IfNotEquivalentToPrevious()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(cache);
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" });
|
||||||
|
context.HttpContext.AddResponseCachingFeature();
|
||||||
|
context.HttpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMAA" });
|
||||||
|
var cachedVaryRules = new CachedVaryRules()
|
||||||
|
{
|
||||||
|
Headers = new StringValues(new[] { "HeaderA", "HeaderB" }),
|
||||||
|
Params = new StringValues(new[] { "ParamA", "ParamB" })
|
||||||
|
};
|
||||||
|
context.CachedVaryRules = cachedVaryRules;
|
||||||
|
|
||||||
|
middleware.FinalizeCachingHeaders(context);
|
||||||
|
|
||||||
|
Assert.Equal(1, cache.SetCount);
|
||||||
|
Assert.NotSame(cachedVaryRules, context.CachedVaryRules);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FinalizeCachingHeaders_DoNotUpdateCachedVaryRules_IfEquivalentToPrevious()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(cache);
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB" });
|
||||||
|
context.HttpContext.AddResponseCachingFeature();
|
||||||
|
context.HttpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMA" });
|
||||||
|
var cachedVaryRules = new CachedVaryRules()
|
||||||
|
{
|
||||||
|
VaryKeyPrefix = FastGuid.NewGuid().IdString,
|
||||||
|
Headers = new StringValues(new[] { "HEADERA", "HEADERB" }),
|
||||||
|
Params = new StringValues(new[] { "PARAMA", "PARAMB" })
|
||||||
|
};
|
||||||
|
context.CachedVaryRules = cachedVaryRules;
|
||||||
|
|
||||||
|
middleware.FinalizeCachingHeaders(context);
|
||||||
|
|
||||||
|
Assert.Equal(0, cache.SetCount);
|
||||||
|
Assert.Same(cachedVaryRules, context.CachedVaryRules);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FinalizeCachingHeaders_DoNotAddDate_IfSpecified()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.MinValue;
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware();
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.ResponseTime = utcNow;
|
||||||
|
|
||||||
|
Assert.Null(context.TypedResponseHeaders.Date);
|
||||||
|
|
||||||
|
middleware.FinalizeCachingHeaders(context);
|
||||||
|
|
||||||
|
Assert.Equal(utcNow, context.TypedResponseHeaders.Date);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FinalizeCachingHeaders_AddsDate_IfNoneSpecified()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.MinValue;
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware();
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.TypedResponseHeaders.Date = utcNow;
|
||||||
|
context.ResponseTime = utcNow + TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
Assert.Equal(utcNow, context.TypedResponseHeaders.Date);
|
||||||
|
|
||||||
|
middleware.FinalizeCachingHeaders(context);
|
||||||
|
|
||||||
|
Assert.Equal(utcNow, context.TypedResponseHeaders.Date);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FinalizeCachingHeaders_StoresCachedResponse_InState()
|
||||||
|
{
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware();
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
Assert.Null(context.CachedResponse);
|
||||||
|
|
||||||
|
middleware.FinalizeCachingHeaders(context);
|
||||||
|
|
||||||
|
Assert.NotNull(context.CachedResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizeCachingBody_StoreResponseBodySeparately_IfLargerThanLimit()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(cache);
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
middleware.ShimResponseStream(context);
|
||||||
|
await context.HttpContext.Response.WriteAsync(new string('0', 70 * 1024));
|
||||||
|
|
||||||
|
context.ShouldCacheResponse = true;
|
||||||
|
context.CachedResponse = new CachedResponse()
|
||||||
|
{
|
||||||
|
BodyKeyPrefix = FastGuid.NewGuid().IdString
|
||||||
|
};
|
||||||
|
context.StorageBaseKey = "BaseKey";
|
||||||
|
context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
middleware.FinalizeCachingBody(context);
|
||||||
|
|
||||||
|
Assert.Equal(2, cache.SetCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizeCachingBody_StoreResponseBodyInCachedResponse_IfSmallerThanLimit()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(cache);
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
middleware.ShimResponseStream(context);
|
||||||
|
await context.HttpContext.Response.WriteAsync(new string('0', 70 * 1024 - 1));
|
||||||
|
|
||||||
|
context.ShouldCacheResponse = true;
|
||||||
|
context.CachedResponse = new CachedResponse()
|
||||||
|
{
|
||||||
|
BodyKeyPrefix = FastGuid.NewGuid().IdString
|
||||||
|
};
|
||||||
|
context.StorageBaseKey = "BaseKey";
|
||||||
|
context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
middleware.FinalizeCachingBody(context);
|
||||||
|
|
||||||
|
Assert.Equal(1, cache.SetCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizeCachingBody_StoreResponseBodySeparately_LimitIsConfigurable()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(cache, new ResponseCachingOptions()
|
||||||
|
{
|
||||||
|
MinimumSplitBodySize = 2048
|
||||||
|
});
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
middleware.ShimResponseStream(context);
|
||||||
|
await context.HttpContext.Response.WriteAsync(new string('0', 1024));
|
||||||
|
|
||||||
|
context.ShouldCacheResponse = true;
|
||||||
|
context.CachedResponse = new CachedResponse()
|
||||||
|
{
|
||||||
|
BodyKeyPrefix = FastGuid.NewGuid().IdString
|
||||||
|
};
|
||||||
|
context.StorageBaseKey = "BaseKey";
|
||||||
|
context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
middleware.FinalizeCachingBody(context);
|
||||||
|
|
||||||
|
Assert.Equal(1, cache.SetCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizeCachingBody_Cache_IfContentLengthMatches()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(cache);
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
middleware.ShimResponseStream(context);
|
||||||
|
context.HttpContext.Response.ContentLength = 10;
|
||||||
|
await context.HttpContext.Response.WriteAsync(new string('0', 10));
|
||||||
|
|
||||||
|
context.ShouldCacheResponse = true;
|
||||||
|
context.CachedResponse = new CachedResponse()
|
||||||
|
{
|
||||||
|
BodyKeyPrefix = FastGuid.NewGuid().IdString
|
||||||
|
};
|
||||||
|
context.StorageBaseKey = "BaseKey";
|
||||||
|
context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
middleware.FinalizeCachingBody(context);
|
||||||
|
|
||||||
|
Assert.Equal(1, cache.SetCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizeCachingBody_DoNotCache_IfContentLengthMismatches()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(cache);
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
middleware.ShimResponseStream(context);
|
||||||
|
context.HttpContext.Response.ContentLength = 9;
|
||||||
|
await context.HttpContext.Response.WriteAsync(new string('0', 10));
|
||||||
|
|
||||||
|
context.ShouldCacheResponse = true;
|
||||||
|
context.CachedResponse = new CachedResponse()
|
||||||
|
{
|
||||||
|
BodyKeyPrefix = FastGuid.NewGuid().IdString
|
||||||
|
};
|
||||||
|
context.StorageBaseKey = "BaseKey";
|
||||||
|
context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
middleware.FinalizeCachingBody(context);
|
||||||
|
|
||||||
|
Assert.Equal(0, cache.SetCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizeCachingBody_Cache_IfContentLengthAbsent()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(cache);
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
middleware.ShimResponseStream(context);
|
||||||
|
await context.HttpContext.Response.WriteAsync(new string('0', 10));
|
||||||
|
|
||||||
|
context.ShouldCacheResponse = true;
|
||||||
|
context.CachedResponse = new CachedResponse()
|
||||||
|
{
|
||||||
|
BodyKeyPrefix = FastGuid.NewGuid().IdString
|
||||||
|
};
|
||||||
|
context.StorageBaseKey = "BaseKey";
|
||||||
|
context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
middleware.FinalizeCachingBody(context);
|
||||||
|
|
||||||
|
Assert.Equal(1, cache.SetCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NormalizeStringValues_NormalizesCasingToUpper()
|
||||||
|
{
|
||||||
|
var uppercaseStrings = new StringValues(new[] { "STRINGA", "STRINGB" });
|
||||||
|
var lowercaseStrings = new StringValues(new[] { "stringA", "stringB" });
|
||||||
|
|
||||||
|
var normalizedStrings = ResponseCachingMiddleware.GetNormalizedStringValues(lowercaseStrings);
|
||||||
|
|
||||||
|
Assert.Equal(uppercaseStrings, normalizedStrings);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NormalizeStringValues_NormalizesOrder()
|
||||||
|
{
|
||||||
|
var orderedStrings = new StringValues(new[] { "STRINGA", "STRINGB" });
|
||||||
|
var reverseOrderStrings = new StringValues(new[] { "STRINGB", "STRINGA" });
|
||||||
|
|
||||||
|
var normalizedStrings = ResponseCachingMiddleware.GetNormalizedStringValues(reverseOrderStrings);
|
||||||
|
|
||||||
|
Assert.Equal(orderedStrings, normalizedStrings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,11 +6,9 @@ using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Http.Features;
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
using Microsoft.AspNetCore.TestHost;
|
using Microsoft.AspNetCore.TestHost;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Net.Http.Headers;
|
using Microsoft.Net.Http.Headers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -21,7 +19,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_IfAvailable()
|
public async void ServesCachedContent_IfAvailable()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching();
|
var builder = TestUtils.CreateBuilderWithResponseCaching();
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
{
|
{
|
||||||
|
|
@ -36,7 +34,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesFreshContent_IfNotAvailable()
|
public async void ServesFreshContent_IfNotAvailable()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching();
|
var builder = TestUtils.CreateBuilderWithResponseCaching();
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
{
|
{
|
||||||
|
|
@ -51,10 +49,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_IfVaryHeader_Matches()
|
public async void ServesCachedContent_IfVaryHeader_Matches()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
|
||||||
{
|
{
|
||||||
context.Response.Headers[HeaderNames.Vary] = HeaderNames.From;
|
context.Response.Headers[HeaderNames.Vary] = HeaderNames.From;
|
||||||
await DefaultRequestDelegate(context);
|
await TestUtils.DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
|
|
@ -71,10 +69,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesFreshContent_IfVaryHeader_Mismatches()
|
public async void ServesFreshContent_IfVaryHeader_Mismatches()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
|
||||||
{
|
{
|
||||||
context.Response.Headers[HeaderNames.Vary] = HeaderNames.From;
|
context.Response.Headers[HeaderNames.Vary] = HeaderNames.From;
|
||||||
await DefaultRequestDelegate(context);
|
await TestUtils.DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
|
|
@ -92,10 +90,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_IfVaryParams_Matches()
|
public async void ServesCachedContent_IfVaryParams_Matches()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
|
||||||
{
|
{
|
||||||
context.GetResponseCachingFeature().VaryParams = "param";
|
context.GetResponseCachingFeature().VaryParams = "param";
|
||||||
await DefaultRequestDelegate(context);
|
await TestUtils.DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
|
|
@ -111,10 +109,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_IfVaryParamsExplicit_Matches_ParamNameCaseInsensitive()
|
public async void ServesCachedContent_IfVaryParamsExplicit_Matches_ParamNameCaseInsensitive()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
|
||||||
{
|
{
|
||||||
context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "paramb" };
|
context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "paramb" };
|
||||||
await DefaultRequestDelegate(context);
|
await TestUtils.DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
|
|
@ -130,10 +128,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_IfVaryParamsStar_Matches_ParamNameCaseInsensitive()
|
public async void ServesCachedContent_IfVaryParamsStar_Matches_ParamNameCaseInsensitive()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
|
||||||
{
|
{
|
||||||
context.GetResponseCachingFeature().VaryParams = new[] { "*" };
|
context.GetResponseCachingFeature().VaryParams = new[] { "*" };
|
||||||
await DefaultRequestDelegate(context);
|
await TestUtils.DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
|
|
@ -149,10 +147,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_IfVaryParamsExplicit_Matches_OrderInsensitive()
|
public async void ServesCachedContent_IfVaryParamsExplicit_Matches_OrderInsensitive()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
|
||||||
{
|
{
|
||||||
context.GetResponseCachingFeature().VaryParams = new[] { "ParamB", "ParamA" };
|
context.GetResponseCachingFeature().VaryParams = new[] { "ParamB", "ParamA" };
|
||||||
await DefaultRequestDelegate(context);
|
await TestUtils.DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
|
|
@ -168,10 +166,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_IfVaryParamsStar_Matches_OrderInsensitive()
|
public async void ServesCachedContent_IfVaryParamsStar_Matches_OrderInsensitive()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
|
||||||
{
|
{
|
||||||
context.GetResponseCachingFeature().VaryParams = new[] { "*" };
|
context.GetResponseCachingFeature().VaryParams = new[] { "*" };
|
||||||
await DefaultRequestDelegate(context);
|
await TestUtils.DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
|
|
@ -187,10 +185,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesFreshContent_IfVaryParams_Mismatches()
|
public async void ServesFreshContent_IfVaryParams_Mismatches()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
|
||||||
{
|
{
|
||||||
context.GetResponseCachingFeature().VaryParams = "param";
|
context.GetResponseCachingFeature().VaryParams = "param";
|
||||||
await DefaultRequestDelegate(context);
|
await TestUtils.DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
|
|
@ -206,10 +204,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesFreshContent_IfVaryParamsExplicit_Mismatch_ParamValueCaseSensitive()
|
public async void ServesFreshContent_IfVaryParamsExplicit_Mismatch_ParamValueCaseSensitive()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
|
||||||
{
|
{
|
||||||
context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "ParamB" };
|
context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "ParamB" };
|
||||||
await DefaultRequestDelegate(context);
|
await TestUtils.DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
|
|
@ -225,10 +223,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesFreshContent_IfVaryParamsStar_Mismatch_ParamValueCaseSensitive()
|
public async void ServesFreshContent_IfVaryParamsStar_Mismatch_ParamValueCaseSensitive()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
|
||||||
{
|
{
|
||||||
context.GetResponseCachingFeature().VaryParams = new[] { "*" };
|
context.GetResponseCachingFeature().VaryParams = new[] { "*" };
|
||||||
await DefaultRequestDelegate(context);
|
await TestUtils.DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
|
|
@ -244,7 +242,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesFreshContent_IfRequestRequirements_NotMet()
|
public async void ServesFreshContent_IfRequestRequirements_NotMet()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching();
|
var builder = TestUtils.CreateBuilderWithResponseCaching();
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
{
|
{
|
||||||
|
|
@ -263,7 +261,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void Serves504_IfOnlyIfCachedHeader_IsSpecified()
|
public async void Serves504_IfOnlyIfCachedHeader_IsSpecified()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching();
|
var builder = TestUtils.CreateBuilderWithResponseCaching();
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
{
|
{
|
||||||
|
|
@ -283,10 +281,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesFreshContent_IfSetCookie_IsSpecified()
|
public async void ServesFreshContent_IfSetCookie_IsSpecified()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
|
||||||
{
|
{
|
||||||
var headers = context.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue";
|
var headers = context.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue";
|
||||||
await DefaultRequestDelegate(context);
|
await TestUtils.DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
|
|
@ -302,7 +300,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_IfIHttpSendFileFeature_NotUsed()
|
public async void ServesCachedContent_IfIHttpSendFileFeature_NotUsed()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(app =>
|
var builder = TestUtils.CreateBuilderWithResponseCaching(app =>
|
||||||
{
|
{
|
||||||
app.Use(async (context, next) =>
|
app.Use(async (context, next) =>
|
||||||
{
|
{
|
||||||
|
|
@ -324,7 +322,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesFreshContent_IfIHttpSendFileFeature_Used()
|
public async void ServesFreshContent_IfIHttpSendFileFeature_Used()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(
|
var builder = TestUtils.CreateBuilderWithResponseCaching(
|
||||||
app =>
|
app =>
|
||||||
{
|
{
|
||||||
app.Use(async (context, next) =>
|
app.Use(async (context, next) =>
|
||||||
|
|
@ -333,10 +331,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
await next.Invoke();
|
await next.Invoke();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async (context) =>
|
requestDelegate: async (context) =>
|
||||||
{
|
{
|
||||||
await context.Features.Get<IHttpSendFileFeature>().SendFileAsync("dummy", 0, 0, CancellationToken.None);
|
await context.Features.Get<IHttpSendFileFeature>().SendFileAsync("dummy", 0, 0, CancellationToken.None);
|
||||||
await DefaultRequestDelegate(context);
|
await TestUtils.DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
|
|
@ -352,7 +350,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_IfSubsequentRequest_ContainsNoStore()
|
public async void ServesCachedContent_IfSubsequentRequest_ContainsNoStore()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching();
|
var builder = TestUtils.CreateBuilderWithResponseCaching();
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
{
|
{
|
||||||
|
|
@ -371,7 +369,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesFreshContent_IfInitialRequestContains_NoStore()
|
public async void ServesFreshContent_IfInitialRequestContains_NoStore()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching();
|
var builder = TestUtils.CreateBuilderWithResponseCaching();
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
{
|
{
|
||||||
|
|
@ -390,7 +388,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void Serves304_IfIfModifiedSince_Satisfied()
|
public async void Serves304_IfIfModifiedSince_Satisfied()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching();
|
var builder = TestUtils.CreateBuilderWithResponseCaching();
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
{
|
{
|
||||||
|
|
@ -407,7 +405,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_IfIfModifiedSince_NotSatisfied()
|
public async void ServesCachedContent_IfIfModifiedSince_NotSatisfied()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching();
|
var builder = TestUtils.CreateBuilderWithResponseCaching();
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
{
|
{
|
||||||
|
|
@ -423,10 +421,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void Serves304_IfIfNoneMatch_Satisfied()
|
public async void Serves304_IfIfNoneMatch_Satisfied()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
|
||||||
{
|
{
|
||||||
var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\"");
|
var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\"");
|
||||||
await DefaultRequestDelegate(context);
|
await TestUtils.DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
|
|
@ -444,10 +442,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_IfIfNoneMatch_NotSatisfied()
|
public async void ServesCachedContent_IfIfNoneMatch_NotSatisfied()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
|
||||||
{
|
{
|
||||||
var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\"");
|
var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\"");
|
||||||
await DefaultRequestDelegate(context);
|
await TestUtils.DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
|
|
@ -464,7 +462,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_IfBodySize_IsCacheable()
|
public async void ServesCachedContent_IfBodySize_IsCacheable()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(new ResponseCachingOptions()
|
var builder = TestUtils.CreateBuilderWithResponseCaching(options: new ResponseCachingOptions()
|
||||||
{
|
{
|
||||||
MaximumCachedBodySize = 100
|
MaximumCachedBodySize = 100
|
||||||
});
|
});
|
||||||
|
|
@ -482,7 +480,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesFreshContent_IfBodySize_IsNotCacheable()
|
public async void ServesFreshContent_IfBodySize_IsNotCacheable()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(new ResponseCachingOptions()
|
var builder = TestUtils.CreateBuilderWithResponseCaching(options: new ResponseCachingOptions()
|
||||||
{
|
{
|
||||||
MaximumCachedBodySize = 1
|
MaximumCachedBodySize = 1
|
||||||
});
|
});
|
||||||
|
|
@ -500,10 +498,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public async void ServesCachedContent_WithoutReplacingCachedVaryBy_OnCacheMiss()
|
public async void ServesCachedContent_WithoutReplacingCachedVaryBy_OnCacheMiss()
|
||||||
{
|
{
|
||||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) =>
|
||||||
{
|
{
|
||||||
context.Response.Headers[HeaderNames.Vary] = HeaderNames.From;
|
context.Response.Headers[HeaderNames.Vary] = HeaderNames.From;
|
||||||
await DefaultRequestDelegate(context);
|
await TestUtils.DefaultRequestDelegate(context);
|
||||||
});
|
});
|
||||||
|
|
||||||
using (var server = new TestServer(builder))
|
using (var server = new TestServer(builder))
|
||||||
|
|
@ -541,57 +539,5 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age));
|
Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age));
|
||||||
Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
|
Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RequestDelegate DefaultRequestDelegate = async (context) =>
|
|
||||||
{
|
|
||||||
var uniqueId = Guid.NewGuid().ToString();
|
|
||||||
var headers = context.Response.GetTypedHeaders();
|
|
||||||
headers.CacheControl = new CacheControlHeaderValue()
|
|
||||||
{
|
|
||||||
Public = true,
|
|
||||||
MaxAge = TimeSpan.FromSeconds(10)
|
|
||||||
};
|
|
||||||
headers.Date = DateTimeOffset.UtcNow;
|
|
||||||
headers.Headers["X-Value"] = uniqueId;
|
|
||||||
await context.Response.WriteAsync(uniqueId);
|
|
||||||
};
|
|
||||||
|
|
||||||
private static IWebHostBuilder CreateBuilderWithResponseCaching() =>
|
|
||||||
CreateBuilderWithResponseCaching(app => { }, new ResponseCachingOptions(), DefaultRequestDelegate);
|
|
||||||
|
|
||||||
private static IWebHostBuilder CreateBuilderWithResponseCaching(ResponseCachingOptions options) =>
|
|
||||||
CreateBuilderWithResponseCaching(app => { }, options, DefaultRequestDelegate);
|
|
||||||
|
|
||||||
private static IWebHostBuilder CreateBuilderWithResponseCaching(RequestDelegate requestDelegate) =>
|
|
||||||
CreateBuilderWithResponseCaching(app => { }, new ResponseCachingOptions(), requestDelegate);
|
|
||||||
|
|
||||||
private static IWebHostBuilder CreateBuilderWithResponseCaching(Action<IApplicationBuilder> configureDelegate) =>
|
|
||||||
CreateBuilderWithResponseCaching(configureDelegate, new ResponseCachingOptions(), DefaultRequestDelegate);
|
|
||||||
|
|
||||||
private static IWebHostBuilder CreateBuilderWithResponseCaching(Action<IApplicationBuilder> configureDelegate, RequestDelegate requestDelegate) =>
|
|
||||||
CreateBuilderWithResponseCaching(configureDelegate, new ResponseCachingOptions(), requestDelegate);
|
|
||||||
|
|
||||||
private static IWebHostBuilder CreateBuilderWithResponseCaching(Action<IApplicationBuilder> configureDelegate, ResponseCachingOptions options, RequestDelegate requestDelegate)
|
|
||||||
{
|
|
||||||
return new WebHostBuilder()
|
|
||||||
.ConfigureServices(services =>
|
|
||||||
{
|
|
||||||
services.AddDistributedResponseCache();
|
|
||||||
})
|
|
||||||
.Configure(app =>
|
|
||||||
{
|
|
||||||
configureDelegate(app);
|
|
||||||
app.UseResponseCaching(options);
|
|
||||||
app.Run(requestDelegate);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DummySendFileFeature : IHttpSendFileFeature
|
|
||||||
{
|
|
||||||
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
|
|
||||||
{
|
|
||||||
return Task.FromResult(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Internal;
|
||||||
|
using Microsoft.Extensions.ObjectPool;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
|
{
|
||||||
|
internal class TestUtils
|
||||||
|
{
|
||||||
|
internal static RequestDelegate DefaultRequestDelegate = async (context) =>
|
||||||
|
{
|
||||||
|
var uniqueId = Guid.NewGuid().ToString();
|
||||||
|
var headers = context.Response.GetTypedHeaders();
|
||||||
|
headers.CacheControl = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true,
|
||||||
|
MaxAge = TimeSpan.FromSeconds(10)
|
||||||
|
};
|
||||||
|
headers.Date = DateTimeOffset.UtcNow;
|
||||||
|
headers.Headers["X-Value"] = uniqueId;
|
||||||
|
await context.Response.WriteAsync(uniqueId);
|
||||||
|
};
|
||||||
|
|
||||||
|
internal static ICacheKeyProvider CreateTestKeyProvider()
|
||||||
|
{
|
||||||
|
return CreateTestKeyProvider(new ResponseCachingOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static ICacheKeyProvider CreateTestKeyProvider(ResponseCachingOptions options)
|
||||||
|
{
|
||||||
|
return new CacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static IWebHostBuilder CreateBuilderWithResponseCaching(
|
||||||
|
Action<IApplicationBuilder> configureDelegate = null,
|
||||||
|
ResponseCachingOptions options = null,
|
||||||
|
RequestDelegate requestDelegate = null)
|
||||||
|
{
|
||||||
|
if (configureDelegate == null)
|
||||||
|
{
|
||||||
|
configureDelegate = app => { };
|
||||||
|
}
|
||||||
|
if (options == null)
|
||||||
|
{
|
||||||
|
options = new ResponseCachingOptions();
|
||||||
|
}
|
||||||
|
if (requestDelegate == null)
|
||||||
|
{
|
||||||
|
requestDelegate = DefaultRequestDelegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WebHostBuilder()
|
||||||
|
.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
services.AddDistributedResponseCache();
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
configureDelegate(app);
|
||||||
|
app.UseResponseCaching(options);
|
||||||
|
app.Run(requestDelegate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static ResponseCachingMiddleware CreateTestMiddleware(
|
||||||
|
IResponseCache responseCache = null,
|
||||||
|
ResponseCachingOptions options = null,
|
||||||
|
ICacheKeyProvider cacheKeyProvider = null,
|
||||||
|
ICacheabilityValidator cacheabilityValidator = null)
|
||||||
|
{
|
||||||
|
if (responseCache == null)
|
||||||
|
{
|
||||||
|
responseCache = new TestResponseCache();
|
||||||
|
}
|
||||||
|
if (options == null)
|
||||||
|
{
|
||||||
|
options = new ResponseCachingOptions();
|
||||||
|
}
|
||||||
|
if (cacheKeyProvider == null)
|
||||||
|
{
|
||||||
|
cacheKeyProvider = new CacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options));
|
||||||
|
}
|
||||||
|
if (cacheabilityValidator == null)
|
||||||
|
{
|
||||||
|
cacheabilityValidator = new TestCacheabilityValidator();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ResponseCachingMiddleware(
|
||||||
|
httpContext => TaskCache.CompletedTask,
|
||||||
|
responseCache,
|
||||||
|
Options.Create(options),
|
||||||
|
cacheabilityValidator,
|
||||||
|
cacheKeyProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static ResponseCachingContext CreateTestContext()
|
||||||
|
{
|
||||||
|
return new ResponseCachingContext(new DefaultHttpContext());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DummySendFileFeature : IHttpSendFileFeature
|
||||||
|
{
|
||||||
|
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
|
||||||
|
{
|
||||||
|
return TaskCache.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class TestCacheabilityValidator : ICacheabilityValidator
|
||||||
|
{
|
||||||
|
public bool IsCachedEntryFresh(ResponseCachingContext context) => true;
|
||||||
|
|
||||||
|
public bool IsRequestCacheable(ResponseCachingContext context) => true;
|
||||||
|
|
||||||
|
public bool IsResponseCacheable(ResponseCachingContext context) => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class TestKeyProvider : ICacheKeyProvider
|
||||||
|
{
|
||||||
|
private readonly StringValues _baseKey;
|
||||||
|
private readonly StringValues _varyKey;
|
||||||
|
|
||||||
|
public TestKeyProvider(StringValues? lookupBaseKey = null, StringValues? lookupVaryKey = null)
|
||||||
|
{
|
||||||
|
if (lookupBaseKey.HasValue)
|
||||||
|
{
|
||||||
|
_baseKey = lookupBaseKey.Value;
|
||||||
|
}
|
||||||
|
if (lookupVaryKey.HasValue)
|
||||||
|
{
|
||||||
|
_varyKey = lookupVaryKey.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> CreateLookupBaseKeys(ResponseCachingContext context) => _baseKey;
|
||||||
|
|
||||||
|
|
||||||
|
public IEnumerable<string> CreateLookupVaryKeys(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
foreach (var baseKey in _baseKey)
|
||||||
|
{
|
||||||
|
foreach (var varyKey in _varyKey)
|
||||||
|
{
|
||||||
|
yield return baseKey + varyKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CreateStorageBaseKey(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CreateStorageVaryKey(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class TestResponseCache : IResponseCache
|
||||||
|
{
|
||||||
|
private readonly IDictionary<string, object> _storage = new Dictionary<string, object>();
|
||||||
|
public int GetCount { get; private set; }
|
||||||
|
public int SetCount { get; private set; }
|
||||||
|
|
||||||
|
public object Get(string key)
|
||||||
|
{
|
||||||
|
GetCount++;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _storage[key];
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(string key)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set(string key, object entry, TimeSpan validFor)
|
||||||
|
{
|
||||||
|
SetCount++;
|
||||||
|
_storage[key] = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class TestHttpSendFileFeature : IHttpSendFileFeature
|
||||||
|
{
|
||||||
|
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
|
||||||
|
{
|
||||||
|
return TaskCache.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue