diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs
index 972b9917fe..2013b202b5 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs
@@ -1,6 +1,7 @@
// 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
@@ -8,18 +9,33 @@ namespace Microsoft.AspNetCore.ResponseCaching
public interface IKeyProvider
{
///
- /// Create a base key using the HTTP request.
+ /// Create a base key using the HTTP context for storing items.
///
/// The .
/// The created base key.
- string CreateBaseKey(HttpContext httpContext);
+ string CreateStorageBaseKey(HttpContext httpContext);
///
- /// Create a vary key using the HTTP context and vary rules.
+ /// Create one or more base keys using the HTTP context for looking up items.
+ ///
+ /// The .
+ /// An ordered containing the base keys to try when looking up items.
+ IEnumerable CreateLookupBaseKey(HttpContext httpContext);
+
+ ///
+ /// Create a vary key using the HTTP context and vary rules for storing items.
///
/// The .
/// The .
/// The created vary key.
- string CreateVaryKey(HttpContext httpContext, VaryRules varyRules);
+ string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules);
+
+ ///
+ /// Create one or more vary keys using the HTTP context and vary rules for looking up items.
+ ///
+ /// The .
+ /// The .
+ /// An ordered containing the vary keys to try when looking up items.
+ IEnumerable CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules);
}
}
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs
index 6ea80b95b0..422f7c9746 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs
@@ -26,9 +26,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
public bool ShouldCacheResponse { get; internal set; }
- public string BaseKey { get; internal set; }
+ public string StorageBaseKey { get; internal set; }
- public string VaryKey { get; internal set; }
+ public string StorageVaryKey { get; internal set; }
public DateTimeOffset ResponseTime { get; internal set; }
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs
index 027d2e3b77..4de7a40b36 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs
@@ -2,6 +2,7 @@
// 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.Linq;
using System.Text;
using Microsoft.AspNetCore.Builder;
@@ -35,9 +36,19 @@ namespace Microsoft.AspNetCore.ResponseCaching
_options = options.Value;
}
+ public virtual IEnumerable CreateLookupBaseKey(HttpContext httpContext)
+ {
+ return new string[] { CreateStorageBaseKey(httpContext) };
+ }
+
+ public virtual IEnumerable CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules)
+ {
+ return new string[] { CreateStorageVaryKey(httpContext, varyRules) };
+ }
+
// GET/PATH
// TODO: Method invariant retrieval? E.g. HEAD after GET to the same resource.
- public virtual string CreateBaseKey(HttpContext httpContext)
+ public virtual string CreateStorageBaseKey(HttpContext httpContext)
{
if (httpContext == null)
{
@@ -63,7 +74,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
}
// BaseKeyHHeaderName=HeaderValueQQueryName=QueryValue
- public virtual string CreateVaryKey(HttpContext httpContext, VaryRules varyRules)
+ public virtual string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules)
{
if (httpContext == null)
{
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs
index 4b9fe410ba..55dc1824ca 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs
@@ -59,84 +59,101 @@ namespace Microsoft.AspNetCore.ResponseCaching
private IHttpSendFileFeature OriginalSendFileFeature { get; set; }
- internal async Task TryServeFromCacheAsync()
+ internal async Task TryServeCachedResponseAsync(CachedResponse cachedResponse)
{
- State.BaseKey = _keyProvider.CreateBaseKey(_httpContext);
- var cacheEntry = _cache.Get(State.BaseKey);
- var responseServed = false;
+ State.CachedResponse = cachedResponse;
+ var cachedResponseHeaders = new ResponseHeaders(State.CachedResponse.Headers);
- if (cacheEntry is CachedVaryRules)
+ 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))
{
- // Request contains vary rules, recompute key and try again
- State.CachedVaryRules = cacheEntry as CachedVaryRules;
- var varyKey = _keyProvider.CreateVaryKey(_httpContext, ((CachedVaryRules)cacheEntry).VaryRules);
- cacheEntry = _cache.Get(varyKey);
- }
-
- if (cacheEntry is CachedResponse)
- {
- State.CachedResponse = cacheEntry as CachedResponse;
- 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 (ConditionalRequestSatisfied(cachedResponseHeaders))
{
- responseServed = true;
-
- // Check conditional request rules
- if (ConditionalRequestSatisfied(cachedResponseHeaders))
- {
- _httpContext.Response.StatusCode = StatusCodes.Status304NotModified;
- }
- else
- {
- 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);
- }
- }
+ _httpContext.Response.StatusCode = StatusCodes.Status304NotModified;
}
else
{
- // TODO: Validate with endpoint instead
+ 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 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 (!responseServed && State.RequestCacheControl.OnlyIfCached)
+
+ if (State.RequestCacheControl.OnlyIfCached)
{
_httpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
- responseServed = true;
+ return true;
}
- return responseServed;
+ return false;
}
internal bool ConditionalRequestSatisfied(ResponseHeaders cachedResponseHeaders)
@@ -174,6 +191,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
if (_cacheabilityValidator.ResponseIsCacheable(_httpContext))
{
State.ShouldCacheResponse = true;
+ State.StorageBaseKey = _keyProvider.CreateStorageBaseKey(_httpContext);
// Create the cache entry now
var response = _httpContext.Response;
@@ -209,10 +227,10 @@ namespace Microsoft.AspNetCore.ResponseCaching
};
State.CachedVaryRules = cachedVaryRules;
- _cache.Set(State.BaseKey, cachedVaryRules, State.CachedResponseValidFor);
+ _cache.Set(State.StorageBaseKey, cachedVaryRules, State.CachedResponseValidFor);
}
- State.VaryKey = _keyProvider.CreateVaryKey(_httpContext, State.CachedVaryRules.VaryRules);
+ State.StorageVaryKey = _keyProvider.CreateStorageVaryKey(_httpContext, State.CachedVaryRules.VaryRules);
}
// Ensure date header is set
@@ -245,12 +263,15 @@ namespace Microsoft.AspNetCore.ResponseCaching
internal void FinalizeCachingBody()
{
- if (State.ShouldCacheResponse && ResponseCacheStream.BufferingEnabled)
+ if (State.ShouldCacheResponse &&
+ ResponseCacheStream.BufferingEnabled &&
+ (State.ResponseHeaders.ContentLength == null ||
+ State.ResponseHeaders.ContentLength == ResponseCacheStream.BufferedStream.Length))
{
if (ResponseCacheStream.BufferedStream.Length >= _options.MinimumSplitBodySize)
{
// Store response and response body separately
- _cache.Set(State.VaryKey ?? State.BaseKey, State.CachedResponse, State.CachedResponseValidFor);
+ _cache.Set(State.StorageVaryKey ?? State.StorageBaseKey, State.CachedResponse, State.CachedResponseValidFor);
var cachedResponseBody = new CachedResponseBody()
{
@@ -263,7 +284,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
{
// Store response and response body together
State.CachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray();
- _cache.Set(State.VaryKey ?? State.BaseKey, State.CachedResponse, State.CachedResponseValidFor);
+ _cache.Set(State.StorageVaryKey ?? State.StorageBaseKey, State.CachedResponse, State.CachedResponseValidFor);
}
}
}
diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs
index 17922f5afa..98e1450b9b 100644
--- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs
+++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs
@@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
};
[Fact]
- public void DefaultKeyProvider_CreateBaseKey_IncludesOnlyNormalizedMethodAndPath()
+ public void DefaultKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodAndPath()
{
var httpContext = CreateDefaultContext();
httpContext.Request.Method = "head";
@@ -30,11 +30,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
httpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b");
var keyProvider = CreateTestKeyProvider();
- Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", keyProvider.CreateBaseKey(httpContext));
+ Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", keyProvider.CreateStorageBaseKey(httpContext));
}
[Fact]
- public void DefaultKeyProvider_CreateBaseKey_CaseInsensitivePath_NormalizesPath()
+ public void DefaultKeyProvider_CreateStorageBaseKey_CaseInsensitivePath_NormalizesPath()
{
var httpContext = CreateDefaultContext();
httpContext.Request.Method = "GET";
@@ -44,11 +44,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
CaseSensitivePaths = false
});
- Assert.Equal($"GET{KeyDelimiter}/PATH", keyProvider.CreateBaseKey(httpContext));
+ Assert.Equal($"GET{KeyDelimiter}/PATH", keyProvider.CreateStorageBaseKey(httpContext));
}
[Fact]
- public void DefaultKeyProvider_CreateBaseKey_CaseSensitivePath_PreservesPathCase()
+ public void DefaultKeyProvider_CreateStorageBaseKey_CaseSensitivePath_PreservesPathCase()
{
var httpContext = CreateDefaultContext();
httpContext.Request.Method = "GET";
@@ -58,21 +58,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
CaseSensitivePaths = true
});
- Assert.Equal($"GET{KeyDelimiter}/Path", keyProvider.CreateBaseKey(httpContext));
+ Assert.Equal($"GET{KeyDelimiter}/Path", keyProvider.CreateStorageBaseKey(httpContext));
}
[Fact]
- public void DefaultKeyProvider_CreateVaryKey_ReturnsCachedVaryGuid_IfVaryRulesIsNullOrEmpty()
+ public void DefaultKeyProvider_CreateStorageVaryKey_ReturnsCachedVaryGuid_IfVaryRulesIsNullOrEmpty()
{
var httpContext = CreateDefaultContext();
var keyProvider = CreateTestKeyProvider();
- Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateVaryKey(httpContext, null));
- Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateVaryKey(httpContext, new VaryRules()));
+ Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateStorageVaryKey(httpContext, null));
+ Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateStorageVaryKey(httpContext, new VaryRules()));
}
[Fact]
- public void DefaultKeyProvider_CreateVaryKey_IncludesListedHeadersOnly()
+ public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly()
{
var httpContext = CreateDefaultContext();
httpContext.Request.Headers["HeaderA"] = "ValueA";
@@ -80,42 +80,42 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var keyProvider = CreateTestKeyProvider();
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null",
- keyProvider.CreateVaryKey(httpContext, new VaryRules()
+ keyProvider.CreateStorageVaryKey(httpContext, new VaryRules()
{
Headers = new string[] { "HeaderA", "HeaderC" }
}));
}
[Fact]
- public void DefaultKeyProvider_CreateVaryKey_IncludesListedParamsOnly()
+ public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedParamsOnly()
{
var httpContext = CreateDefaultContext();
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
var keyProvider = CreateTestKeyProvider();
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null",
- keyProvider.CreateVaryKey(httpContext, new VaryRules()
+ keyProvider.CreateStorageVaryKey(httpContext, new VaryRules()
{
Params = new string[] { "ParamA", "ParamC" }
}));
}
[Fact]
- public void DefaultKeyProvider_CreateVaryKey_IncludesParams_ParamNameCaseInsensitive_UseParamCasing()
+ public void DefaultKeyProvider_CreateStorageVaryKey_IncludesParams_ParamNameCaseInsensitive_UseParamCasing()
{
var httpContext = CreateDefaultContext();
httpContext.Request.QueryString = new QueryString("?parama=ValueA¶mB=ValueB");
var keyProvider = CreateTestKeyProvider();
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null",
- keyProvider.CreateVaryKey(httpContext, new VaryRules()
+ keyProvider.CreateStorageVaryKey(httpContext, new VaryRules()
{
Params = new string[] { "ParamA", "ParamC" }
}));
}
[Fact]
- public void DefaultKeyProvider_CreateVaryKey_IncludesAllQueryParamsGivenAsterisk()
+ public void DefaultKeyProvider_CreateStorageVaryKey_IncludesAllQueryParamsGivenAsterisk()
{
var httpContext = CreateDefaultContext();
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
@@ -124,14 +124,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
// To support case insensitivity, all param keys are converted to upper case.
// Explicit params uses the casing specified in the setting.
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB",
- keyProvider.CreateVaryKey(httpContext, new VaryRules()
+ keyProvider.CreateStorageVaryKey(httpContext, new VaryRules()
{
Params = new string[] { "*" }
}));
}
[Fact]
- public void DefaultKeyProvider_CreateVaryKey_IncludesListedHeadersAndParams()
+ public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndParams()
{
var httpContext = CreateDefaultContext();
httpContext.Request.Headers["HeaderA"] = "ValueA";
@@ -140,7 +140,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var keyProvider = CreateTestKeyProvider();
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null",
- keyProvider.CreateVaryKey(httpContext, new VaryRules()
+ keyProvider.CreateStorageVaryKey(httpContext, new VaryRules()
{
Headers = new string[] { "HeaderA", "HeaderC" },
Params = new string[] { "ParamA", "ParamC" }
diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs
index ec11698d0d..c900f3161e 100644
--- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs
+++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs
@@ -10,16 +10,100 @@ 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;
-using Microsoft.Extensions.Primitives;
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()
@@ -159,7 +243,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
public void FinalizeCachingHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable()
{
var httpContext = new DefaultHttpContext();
- var context = CreateTestContext(httpContext);
+ var context = CreateTestContext(httpContext, cacheabilityValidator: new CacheabilityValidator());
var state = httpContext.GetResponseCachingState();
Assert.False(state.ShouldCacheResponse);
@@ -178,7 +262,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
Public = true
};
- var context = CreateTestContext(httpContext);
+ var context = CreateTestContext(httpContext, cacheabilityValidator: new CacheabilityValidator());
var state = httpContext.GetResponseCachingState();
Assert.False(state.ShouldCacheResponse);
@@ -192,10 +276,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
public void FinalizeCachingHeaders_DefaultResponseValidity_Is10Seconds()
{
var httpContext = new DefaultHttpContext();
- httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
- {
- Public = true
- };
var context = CreateTestContext(httpContext);
context.FinalizeCachingHeaders();
@@ -207,10 +287,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
public void FinalizeCachingHeaders_ResponseValidity_UseExpiryIfAvailable()
{
var httpContext = new DefaultHttpContext();
- httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
- {
- Public = true
- };
var context = CreateTestContext(httpContext);
var state = httpContext.GetResponseCachingState();
@@ -229,7 +305,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var httpContext = new DefaultHttpContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
- Public = true,
MaxAge = TimeSpan.FromSeconds(12)
};
var context = CreateTestContext(httpContext);
@@ -249,7 +324,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var httpContext = new DefaultHttpContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
- Public = true,
MaxAge = TimeSpan.FromSeconds(12),
SharedMaxAge = TimeSpan.FromSeconds(13)
};
@@ -269,16 +343,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
- var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions());
+ 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" });
- httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
- {
- Public = true,
- };
var cachedVaryRules = new CachedVaryRules()
{
VaryRules = new VaryRules()
@@ -291,7 +361,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.FinalizeCachingHeaders();
- Assert.Equal(1, cache.StoredItems);
+ Assert.Equal(1, cache.SetCount);
Assert.NotSame(cachedVaryRules, state.CachedVaryRules);
}
@@ -300,16 +370,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
- var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions());
+ 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" });
- httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
- {
- Public = true,
- };
var cachedVaryRules = new CachedVaryRules()
{
VaryKeyPrefix = FastGuid.NewGuid().IdString,
@@ -323,7 +389,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.FinalizeCachingHeaders();
- Assert.Equal(0, cache.StoredItems);
+ Assert.Equal(0, cache.SetCount);
Assert.Same(cachedVaryRules, state.CachedVaryRules);
}
@@ -331,10 +397,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
public void FinalizeCachingHeaders_DoNotAddDate_IfSpecified()
{
var httpContext = new DefaultHttpContext();
- httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
- {
- Public = true
- };
var context = CreateTestContext(httpContext);
var state = httpContext.GetResponseCachingState();
var utcNow = DateTimeOffset.MinValue;
@@ -351,10 +413,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
public void FinalizeCachingHeaders_AddsDate_IfNoneSpecified()
{
var httpContext = new DefaultHttpContext();
- httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
- {
- Public = true
- };
var context = CreateTestContext(httpContext);
var state = httpContext.GetResponseCachingState();
var utcNow = DateTimeOffset.MinValue;
@@ -372,10 +430,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
public void FinalizeCachingHeaders_StoresCachedResponse_InState()
{
var httpContext = new DefaultHttpContext();
- httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
- {
- Public = true
- };
var context = CreateTestContext(httpContext);
var state = httpContext.GetResponseCachingState();
@@ -391,7 +445,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
- var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions());
+ var context = CreateTestContext(httpContext, cache);
context.ShimResponseStream();
await httpContext.Response.WriteAsync(new string('0', 70 * 1024));
@@ -402,12 +456,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
- state.BaseKey = "BaseKey";
+ state.StorageBaseKey = "BaseKey";
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
context.FinalizeCachingBody();
- Assert.Equal(2, cache.StoredItems);
+ Assert.Equal(2, cache.SetCount);
}
[Fact]
@@ -415,7 +469,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
- var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions());
+ var context = CreateTestContext(httpContext, cache);
context.ShimResponseStream();
await httpContext.Response.WriteAsync(new string('0', 70 * 1024 - 1));
@@ -426,12 +480,113 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
- state.BaseKey = "BaseKey";
+ state.StorageBaseKey = "BaseKey";
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
context.FinalizeCachingBody();
- Assert.Equal(1, cache.StoredItems);
+ 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]
@@ -456,48 +611,30 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Assert.Equal(orderedStrings, normalizedStrings);
}
- private static ResponseCachingContext CreateTestContext(HttpContext httpContext)
- {
- return CreateTestContext(
- httpContext,
- new TestResponseCache(),
- new ResponseCachingOptions(),
- new CacheabilityValidator());
- }
-
- private static ResponseCachingContext CreateTestContext(HttpContext httpContext, ResponseCachingOptions options)
- {
- return CreateTestContext(
- httpContext,
- new TestResponseCache(),
- options,
- new CacheabilityValidator());
- }
-
- private static ResponseCachingContext CreateTestContext(HttpContext httpContext, ICacheabilityValidator cacheabilityValidator)
- {
- return CreateTestContext(
- httpContext,
- new TestResponseCache(),
- new ResponseCachingOptions(),
- cacheabilityValidator);
- }
-
- private static ResponseCachingContext CreateTestContext(HttpContext httpContext, IResponseCache responseCache, ResponseCachingOptions options)
- {
- return CreateTestContext(
- httpContext,
- responseCache,
- options,
- new CacheabilityValidator());
- }
-
private static ResponseCachingContext CreateTestContext(
HttpContext httpContext,
- IResponseCache responseCache,
- ResponseCachingOptions options,
- ICacheabilityValidator cacheabilityValidator)
+ 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(
@@ -505,16 +642,82 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
responseCache,
options,
cacheabilityValidator,
- new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)));
+ 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 CreateLookupBaseKey(HttpContext httpContext) => _baseKey;
+
+
+ public IEnumerable 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
{
- public int StoredItems { get; private set; }
+ private readonly IDictionary _storage = new Dictionary();
+ public int GetCount { get; private set; }
+ public int SetCount { get; private set; }
public object Get(string key)
{
- return null;
+ GetCount++;
+ try
+ {
+ return _storage[key];
+ }
+ catch
+ {
+ return null;
+ }
}
public void Remove(string key)
@@ -523,7 +726,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
public void Set(string key, object entry, TimeSpan validFor)
{
- StoredItems++;
+ SetCount++;
+ _storage[key] = entry;
}
}
@@ -531,7 +735,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
{
- return Task.FromResult(0);
+ return TaskCache.CompletedTask;
}
}
}