diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntrySerializer.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs
similarity index 66%
rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntrySerializer.cs
rename to src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs
index 0f4f3f5a37..48d28e319f 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntrySerializer.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs
@@ -13,6 +13,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
public static object Deserialize(byte[] serializedEntry)
{
+ if (serializedEntry == null)
+ {
+ return null;
+ }
+
using (var memory = new MemoryStream(serializedEntry))
{
using (var reader = new BinaryReader(memory))
@@ -37,7 +42,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
// Serialization Format
// Format version (int)
- // Type (char: 'R' for CachedResponse, 'V' for CachedVaryRules)
+ // Type (char: 'B' for CachedResponseBody, 'R' for CachedResponse, 'V' for CachedVaryRules)
// Type-dependent data (see CachedResponse and CachedVaryRules)
public static object Read(BinaryReader reader)
{
@@ -53,15 +58,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
var type = reader.ReadChar();
- if (type == 'R')
+ if (type == 'B')
{
- var cachedResponse = ReadCachedResponse(reader);
- return cachedResponse;
+ return ReadCachedResponseBody(reader);
+ }
+ else if (type == 'R')
+ {
+ return ReadCachedResponse(reader);
}
else if (type == 'V')
{
- var cachedVaryRules = ReadCachedVaryRules(reader);
- return cachedVaryRules;
+ return ReadCachedVaryRules(reader);
}
// Unable to read as CachedResponse or CachedVaryRules
@@ -69,16 +76,30 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
}
// Serialization Format
+ // Body length (int)
+ // Body (byte[])
+ private static CachedResponseBody ReadCachedResponseBody(BinaryReader reader)
+ {
+ var bodyLength = reader.ReadInt32();
+ var body = reader.ReadBytes(bodyLength);
+
+ return new CachedResponseBody() { Body = body };
+ }
+
+ // Serialization Format
+ // BodyKeyPrefix (string)
// Creation time - UtcTicks (long)
// Status code (int)
// Header count (int)
// Header(s)
// Key (string)
// Value (string)
- // Body length (int)
- // Body (byte[])
+ // ContainsBody (bool)
+ // Body length (int)
+ // Body (byte[])
private static CachedResponse ReadCachedResponse(BinaryReader reader)
{
+ var bodyKeyPrefix = reader.ReadString();
var created = new DateTimeOffset(reader.ReadInt64(), TimeSpan.Zero);
var statusCode = reader.ReadInt32();
var headerCount = reader.ReadInt32();
@@ -89,25 +110,28 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
var value = reader.ReadString();
headers[key] = value;
}
- var bodyLength = reader.ReadInt32();
- var body = reader.ReadBytes(bodyLength);
- return new CachedResponse { Created = created, StatusCode = statusCode, Headers = headers, Body = body };
+ var containsBody = reader.ReadBoolean();
+ int bodyLength;
+ byte[] body = null;
+ if (containsBody)
+ {
+ bodyLength = reader.ReadInt32();
+ body = reader.ReadBytes(bodyLength);
+ }
+
+ return new CachedResponse { BodyKeyPrefix = bodyKeyPrefix, Created = created, StatusCode = statusCode, Headers = headers, Body = body };
}
// Serialization Format
- // ContainsVaryRules (bool)
- // If containing vary rules:
- // Headers count
- // Headers if count > 0 (comma separated string)
- // Params count
- // Params if count > 0 (comma separated string)
+ // Guid (long)
+ // Headers count
+ // Header(s) (comma separated string)
+ // Params count
+ // Param(s) (comma separated string)
private static CachedVaryRules ReadCachedVaryRules(BinaryReader reader)
{
- if (!reader.ReadBoolean())
- {
- return new CachedVaryRules();
- }
+ var varyKeyPrefix = reader.ReadString();
var headerCount = reader.ReadInt32();
var headers = new string[headerCount];
@@ -122,7 +146,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
param[index] = reader.ReadString();
}
- return new CachedVaryRules { VaryRules = new VaryRules() { Headers = headers, Params = param } };
+ return new CachedVaryRules { VaryKeyPrefix = varyKeyPrefix, VaryRules = new VaryRules() { Headers = headers, Params = param } };
}
// See serialization format above
@@ -140,7 +164,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
writer.Write(FormatVersion);
- if (entry is CachedResponse)
+ if (entry is CachedResponseBody)
+ {
+ writer.Write('B');
+ WriteCachedResponseBody(writer, entry as CachedResponseBody);
+ }
+ else if (entry is CachedResponse)
{
writer.Write('R');
WriteCachedResponse(writer, entry as CachedResponse);
@@ -156,9 +185,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
}
}
+ // See serialization format above
+ private static void WriteCachedResponseBody(BinaryWriter writer, CachedResponseBody entry)
+ {
+ writer.Write(entry.Body.Length);
+ writer.Write(entry.Body);
+ }
+
// See serialization format above
private static void WriteCachedResponse(BinaryWriter writer, CachedResponse entry)
{
+ writer.Write(entry.BodyKeyPrefix);
writer.Write(entry.Created.UtcTicks);
writer.Write(entry.StatusCode);
writer.Write(entry.Headers.Count);
@@ -168,31 +205,33 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
writer.Write(header.Value);
}
- writer.Write(entry.Body.Length);
- writer.Write(entry.Body);
- }
-
- // See serialization format above
- private static void WriteCachedVaryRules(BinaryWriter writer, CachedVaryRules varyRules)
- {
- if (varyRules.VaryRules == null)
+ if (entry.Body == null)
{
writer.Write(false);
}
else
{
writer.Write(true);
- writer.Write(varyRules.VaryRules.Headers.Count);
- foreach (var header in varyRules.VaryRules.Headers)
- {
- writer.Write(header);
- }
+ writer.Write(entry.Body.Length);
+ writer.Write(entry.Body);
+ }
+ }
- writer.Write(varyRules.VaryRules.Params.Count);
- foreach (var param in varyRules.VaryRules.Params)
- {
- writer.Write(param);
- }
+ // See serialization format above
+ private static void WriteCachedVaryRules(BinaryWriter writer, CachedVaryRules varyRules)
+ {
+ writer.Write(varyRules.VaryKeyPrefix);
+
+ writer.Write(varyRules.VaryRules.Headers.Count);
+ foreach (var header in varyRules.VaryRules.Headers)
+ {
+ writer.Write(header);
+ }
+
+ writer.Write(varyRules.VaryRules.Params.Count);
+ foreach (var param in varyRules.VaryRules.Params)
+ {
+ writer.Write(param);
}
}
}
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs
similarity index 91%
rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs
rename to src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs
index 4743d370b5..d4413bf4d1 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs
@@ -8,6 +8,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
internal class CachedResponse
{
+ internal string BodyKeyPrefix { get; set; }
+
internal DateTimeOffset Created { get; set; }
internal int StatusCode { get; set; }
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryRules.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs
similarity index 75%
rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryRules.cs
rename to src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs
index bd74a08c8c..643fac449c 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryRules.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs
@@ -3,8 +3,8 @@
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
- internal class CachedVaryRules
+ internal class CachedResponseBody
{
- internal VaryRules VaryRules;
+ internal byte[] Body { get; set; }
}
}
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryRules.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryRules.cs
new file mode 100644
index 0000000000..de3355e761
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryRules.cs
@@ -0,0 +1,12 @@
+// 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.
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ internal class CachedVaryRules
+ {
+ internal string VaryKeyPrefix { get; set; }
+
+ internal VaryRules VaryRules { get; set; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs
index 888498f908..972b9917fe 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs
@@ -8,18 +8,18 @@ namespace Microsoft.AspNetCore.ResponseCaching
public interface IKeyProvider
{
///
- /// Create a key using the HTTP request.
+ /// Create a base key using the HTTP request.
///
/// The .
/// The created base key.
string CreateBaseKey(HttpContext httpContext);
///
- /// Create a key using the HTTP context and vary rules.
+ /// Create a vary key using the HTTP context and vary rules.
///
/// The .
/// The .
- /// The created base key.
+ /// The created vary key.
string CreateVaryKey(HttpContext httpContext, VaryRules varyRules);
}
}
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs
new file mode 100644
index 0000000000..075403f35c
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs
@@ -0,0 +1,80 @@
+// 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.Threading;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ internal class FastGuid
+ {
+ // Base32 encoding - in ascii sort order for easy text based sorting
+ private static readonly string _encode32Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUV";
+ // Global ID
+ private static long NextId;
+
+ // Instance components
+ private string _idString;
+ internal long IdValue { get; private set; }
+
+ internal string IdString
+ {
+ get
+ {
+ if (_idString == null)
+ {
+ _idString = GenerateGuidString(this);
+ }
+ return _idString;
+ }
+ }
+
+ // Static constructor to initialize global components
+ static FastGuid()
+ {
+ var guidBytes = Guid.NewGuid().ToByteArray();
+
+ // Use the first 4 bytes from the Guid to initialize global ID
+ NextId =
+ guidBytes[0] << 32 |
+ guidBytes[1] << 40 |
+ guidBytes[2] << 48 |
+ guidBytes[3] << 56;
+ }
+
+ internal FastGuid(long id)
+ {
+ IdValue = id;
+ }
+
+ internal static FastGuid NewGuid()
+ {
+ return new FastGuid(Interlocked.Increment(ref NextId));
+ }
+
+ private static unsafe string GenerateGuidString(FastGuid guid)
+ {
+
+ // stackalloc to allocate array on stack rather than heap
+ char* charBuffer = stackalloc char[13];
+
+ // ID
+ charBuffer[0] = _encode32Chars[(int)(guid.IdValue >> 60) & 31];
+ charBuffer[1] = _encode32Chars[(int)(guid.IdValue >> 55) & 31];
+ charBuffer[2] = _encode32Chars[(int)(guid.IdValue >> 50) & 31];
+ charBuffer[3] = _encode32Chars[(int)(guid.IdValue >> 45) & 31];
+ charBuffer[4] = _encode32Chars[(int)(guid.IdValue >> 40) & 31];
+ charBuffer[5] = _encode32Chars[(int)(guid.IdValue >> 35) & 31];
+ charBuffer[6] = _encode32Chars[(int)(guid.IdValue >> 30) & 31];
+ charBuffer[7] = _encode32Chars[(int)(guid.IdValue >> 25) & 31];
+ charBuffer[8] = _encode32Chars[(int)(guid.IdValue >> 20) & 31];
+ charBuffer[9] = _encode32Chars[(int)(guid.IdValue >> 15) & 31];
+ charBuffer[10] = _encode32Chars[(int)(guid.IdValue >> 10) & 31];
+ charBuffer[11] = _encode32Chars[(int)(guid.IdValue >> 5) & 31];
+ charBuffer[12] = _encode32Chars[(int)guid.IdValue & 31];
+
+ // string ctor overload that takes char*
+ return new string(charBuffer, 0, 13);
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs
index 117d112db9..d5296c93e2 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs
@@ -33,8 +33,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
public void Set(string key, object entry, TimeSpan validFor)
{
_cache.Set(
- key,
- entry,
+ key,
+ entry,
new MemoryCacheEntryOptions()
{
AbsoluteExpirationRelativeToNow = validFor
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs
index 3ff862a502..6ea80b95b0 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs
@@ -38,6 +38,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
internal CachedResponse CachedResponse { get; set; }
+ internal CachedVaryRules CachedVaryRules { get; set; }
+
public RequestHeaders RequestHeaders
{
get
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs
index 6eac525b33..027d2e3b77 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs
@@ -6,7 +6,6 @@ using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
@@ -70,10 +69,9 @@ namespace Microsoft.AspNetCore.ResponseCaching
{
throw new ArgumentNullException(nameof(httpContext));
}
- if (varyRules == null)
+ if (varyRules == null || (StringValues.IsNullOrEmpty(varyRules.Headers) && StringValues.IsNullOrEmpty(varyRules.Params)))
{
- // TODO: replace this with a GUID
- return httpContext.GetResponseCachingState()?.BaseKey ?? CreateBaseKey(httpContext);
+ return httpContext.GetResponseCachingState().CachedVaryRules.VaryKeyPrefix;
}
var request = httpContext.Request;
@@ -81,8 +79,8 @@ namespace Microsoft.AspNetCore.ResponseCaching
try
{
- // TODO: replace this with a GUID
- builder.Append(httpContext.GetResponseCachingState()?.BaseKey ?? CreateBaseKey(httpContext));
+ // Prepend with the Guid of the CachedVaryRules
+ builder.Append(httpContext.GetResponseCachingState().CachedVaryRules.VaryKeyPrefix);
// Vary by headers
if (varyRules?.Headers.Count > 0)
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs
index cd34857e84..4b9fe410ba 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs
@@ -68,17 +68,18 @@ namespace Microsoft.AspNetCore.ResponseCaching
if (cacheEntry is CachedVaryRules)
{
// 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)
{
- var cachedResponse = cacheEntry as CachedResponse;
- var cachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers);
+ State.CachedResponse = cacheEntry as CachedResponse;
+ var cachedResponseHeaders = new ResponseHeaders(State.CachedResponse.Headers);
State.ResponseTime = _options.SystemClock.UtcNow;
- var cachedEntryAge = State.ResponseTime - cachedResponse.Created;
+ var cachedEntryAge = State.ResponseTime - State.CachedResponse.Created;
State.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero;
if (_cacheabilityValidator.CachedEntryIsFresh(_httpContext, cachedResponseHeaders))
@@ -94,16 +95,22 @@ namespace Microsoft.AspNetCore.ResponseCaching
{
var response = _httpContext.Response;
// Copy the cached status code and response headers
- response.StatusCode = cachedResponse.StatusCode;
- foreach (var header in cachedResponse.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;
- var body = cachedResponse.Body;
+ // If the body is not found, something went wrong.
+ if (body == null)
+ {
+ return false;
+ }
// Copy the cached response body
if (body.Length > 0)
@@ -181,19 +188,31 @@ namespace Microsoft.AspNetCore.ResponseCaching
// Check if any vary rules exist
if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue))
{
- var cachedVaryRules = new CachedVaryRules
- {
- VaryRules = new VaryRules()
- {
- // TODO: Vary Encoding
- Headers = varyHeaderValue,
- Params = varyParamsValue
- }
- };
+ // Normalize order and casing of vary by rules
+ var normalizedVaryHeaderValue = GetNormalizedStringValues(varyHeaderValue);
+ var normalizedVaryParamsValue = GetNormalizedStringValues(varyParamsValue);
- // TODO: Overwrite?
- _cache.Set(State.BaseKey, cachedVaryRules, State.CachedResponseValidFor);
- State.VaryKey = _keyProvider.CreateVaryKey(_httpContext, cachedVaryRules.VaryRules);
+ // 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.BaseKey, cachedVaryRules, State.CachedResponseValidFor);
+ }
+
+ State.VaryKey = _keyProvider.CreateVaryKey(_httpContext, State.CachedVaryRules.VaryRules);
}
// Ensure date header is set
@@ -202,9 +221,10 @@ namespace Microsoft.AspNetCore.ResponseCaching
State.ResponseHeaders.Date = State.ResponseTime;
}
- // Store the response to cache
+ // Store the response on the state
State.CachedResponse = new CachedResponse
{
+ BodyKeyPrefix = FastGuid.NewGuid().IdString,
Created = State.ResponseHeaders.Date.Value,
StatusCode = _httpContext.Response.StatusCode
};
@@ -227,9 +247,24 @@ namespace Microsoft.AspNetCore.ResponseCaching
{
if (State.ShouldCacheResponse && ResponseCacheStream.BufferingEnabled)
{
- State.CachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray();
+ 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.VaryKey ?? State.BaseKey, State.CachedResponse, State.CachedResponseValidFor);
+ var cachedResponseBody = new CachedResponseBody()
+ {
+ Body = ResponseCacheStream.BufferedStream.ToArray()
+ };
+
+ _cache.Set(State.CachedResponse.BodyKeyPrefix, cachedResponseBody, State.CachedResponseValidFor);
+ }
+ else
+ {
+ // Store response and response body together
+ State.CachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray();
+ _cache.Set(State.VaryKey ?? State.BaseKey, State.CachedResponse, State.CachedResponseValidFor);
+ }
}
}
@@ -275,5 +310,29 @@ namespace Microsoft.AspNetCore.ResponseCaching
// 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();
+ }
+
+ // Since the casing has already been normalized, use Ordinal comparison
+ Array.Sort(newArray, StringComparer.Ordinal);
+
+ return new StringValues(newArray);
+ }
+ }
}
}
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs
index fefe65c4e2..7630237ec5 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs
@@ -18,6 +18,11 @@ namespace Microsoft.AspNetCore.Builder
///
public bool CaseSensitivePaths { get; set; } = false;
+ ///
+ /// The smallest size in bytes for which the headers and body of the response will be stored separately. The default is set to 70 KB.
+ ///
+ public long MinimumSplitBodySize { get; set; } = 70 * 1024;
+
///
/// For testing purposes only.
///
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/project.json b/src/Microsoft.AspNetCore.ResponseCaching/project.json
index 70fa887b15..d2521465b9 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/project.json
+++ b/src/Microsoft.AspNetCore.ResponseCaching/project.json
@@ -2,6 +2,7 @@
"version": "0.1.0-*",
"buildOptions": {
"warningsAsErrors": true,
+ "allowUnsafe": true,
"keyFile": "../../tools/Key.snk",
"nowarn": [
"CS1591"
diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs
index e85dabe9f6..bd2ad03bb1 100644
--- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs
+++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs
@@ -25,78 +25,109 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
[Fact]
- public void RoundTrip_CachedResponses_Succeeds()
+ public void Deserialize_NullObject_ReturnsNull()
+ {
+ Assert.Null(CacheEntrySerializer.Deserialize(null));
+ }
+
+ [Fact]
+ public void RoundTrip_CachedResponseBody_Succeeds()
+ {
+ var cachedResponseBody = new CachedResponseBody()
+ {
+ Body = Encoding.ASCII.GetBytes("Hello world"),
+ };
+
+ AssertCachedResponseBodyEqual(cachedResponseBody, (CachedResponseBody)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedResponseBody)));
+ }
+
+ [Fact]
+ public void RoundTrip_CachedResponseWithoutBody_Succeeds()
{
var headers = new HeaderDictionary();
headers["keyA"] = "valueA";
headers["keyB"] = "valueB";
- var cachedEntry = new CachedResponse()
+ var cachedResponse = new CachedResponse()
{
+ BodyKeyPrefix = FastGuid.NewGuid().IdString,
+ Created = DateTimeOffset.UtcNow,
+ StatusCode = StatusCodes.Status200OK,
+ Headers = headers
+ };
+
+ AssertCachedResponseEqual(cachedResponse, (CachedResponse)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedResponse)));
+ }
+
+ [Fact]
+ public void RoundTrip_CachedResponseWithBody_Succeeds()
+ {
+ var headers = new HeaderDictionary();
+ headers["keyA"] = "valueA";
+ headers["keyB"] = "valueB";
+ var cachedResponse = new CachedResponse()
+ {
+ BodyKeyPrefix = FastGuid.NewGuid().IdString,
Created = DateTimeOffset.UtcNow,
StatusCode = StatusCodes.Status200OK,
Body = Encoding.ASCII.GetBytes("Hello world"),
Headers = headers
};
- AssertCachedResponsesEqual(cachedEntry, (CachedResponse)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedEntry)));
+ AssertCachedResponseEqual(cachedResponse, (CachedResponse)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedResponse)));
}
[Fact]
- public void RoundTrip_Empty_CachedVaryRules_Succeeds()
+ public void RoundTrip_CachedVaryRule_EmptyRules_Succeeds()
{
- var cachedVaryRules = new CachedVaryRules();
-
- AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules)));
- }
-
- [Fact]
- public void RoundTrip_CachedVaryRules_EmptyRules_Succeeds()
- {
- var cachedVaryRules = new CachedVaryRules()
+ var cachedVaryRule = new CachedVaryRules()
{
+ VaryKeyPrefix = FastGuid.NewGuid().IdString,
VaryRules = new VaryRules()
};
- AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules)));
+ AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
}
[Fact]
- public void RoundTrip_HeadersOnly_CachedVaryRules_Succeeds()
+ public void RoundTrip_CachedVaryRule_HeadersOnly_Succeeds()
{
var headers = new[] { "headerA", "headerB" };
- var cachedVaryRules = new CachedVaryRules()
+ var cachedVaryRule = new CachedVaryRules()
{
+ VaryKeyPrefix = FastGuid.NewGuid().IdString,
VaryRules = new VaryRules()
{
Headers = headers
}
};
- AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules)));
+ AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
}
[Fact]
- public void RoundTrip_ParamsOnly_CachedVaryRules_Succeeds()
+ public void RoundTrip_CachedVaryRule_ParamsOnly_Succeeds()
{
var param = new[] { "paramA", "paramB" };
- var cachedVaryRules = new CachedVaryRules()
+ var cachedVaryRule = new CachedVaryRules()
{
+ VaryKeyPrefix = FastGuid.NewGuid().IdString,
VaryRules = new VaryRules()
{
Params = param
}
};
- AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules)));
+ AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
}
[Fact]
- public void RoundTrip_HeadersAndParams_CachedVaryRules_Succeeds()
+ public void RoundTrip_CachedVaryRule_HeadersAndParams_Succeeds()
{
var headers = new[] { "headerA", "headerB" };
var param = new[] { "paramA", "paramB" };
- var cachedVaryRules = new CachedVaryRules()
+ var cachedVaryRule = new CachedVaryRules()
{
+ VaryKeyPrefix = FastGuid.NewGuid().IdString,
VaryRules = new VaryRules()
{
Headers = headers,
@@ -104,30 +135,37 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
};
- AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules)));
+ AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule)));
}
[Fact]
public void Deserialize_InvalidEntries_ReturnsNull()
{
var headers = new[] { "headerA", "headerB" };
- var cachedVaryRules = new CachedVaryRules()
+ var cachedVaryRule = new CachedVaryRules()
{
+ VaryKeyPrefix = FastGuid.NewGuid().IdString,
VaryRules = new VaryRules()
{
Headers = headers
}
};
- var serializedEntry = CacheEntrySerializer.Serialize(cachedVaryRules);
+ var serializedEntry = CacheEntrySerializer.Serialize(cachedVaryRule);
Array.Reverse(serializedEntry);
Assert.Null(CacheEntrySerializer.Deserialize(serializedEntry));
}
- private static void AssertCachedResponsesEqual(CachedResponse expected, CachedResponse actual)
+ private static void AssertCachedResponseBodyEqual(CachedResponseBody expected, CachedResponseBody actual)
+ {
+ Assert.True(expected.Body.SequenceEqual(actual.Body));
+ }
+
+ private static void AssertCachedResponseEqual(CachedResponse expected, CachedResponse actual)
{
Assert.NotNull(actual);
Assert.NotNull(expected);
+ Assert.Equal(expected.BodyKeyPrefix, actual.BodyKeyPrefix);
Assert.Equal(expected.Created, actual.Created);
Assert.Equal(expected.StatusCode, actual.StatusCode);
Assert.Equal(expected.Headers.Count, actual.Headers.Count);
@@ -135,23 +173,23 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
Assert.Equal(expectedHeader.Value, actual.Headers[expectedHeader.Key]);
}
- Assert.True(expected.Body.SequenceEqual(actual.Body));
- }
-
- private static void AssertCachedVaryRulesEqual(CachedVaryRules expected, CachedVaryRules actual)
- {
- Assert.NotNull(actual);
- Assert.NotNull(expected);
- if (expected.VaryRules == null)
+ if (expected.Body == null)
{
- Assert.Null(actual.VaryRules);
+ Assert.Null(actual.Body);
}
else
{
- Assert.NotNull(actual.VaryRules);
- Assert.Equal(expected.VaryRules.Headers, actual.VaryRules.Headers);
- Assert.Equal(expected.VaryRules.Params, actual.VaryRules.Params);
+ Assert.True(expected.Body.SequenceEqual(actual.Body));
}
}
+
+ private static void AssertCachedVaryRuleEqual(CachedVaryRules expected, CachedVaryRules actual)
+ {
+ Assert.NotNull(actual);
+ Assert.NotNull(expected);
+ Assert.Equal(expected.VaryKeyPrefix, actual.VaryKeyPrefix);
+ Assert.Equal(expected.VaryRules.Headers, actual.VaryRules.Headers);
+ Assert.Equal(expected.VaryRules.Params, actual.VaryRules.Params);
+ }
}
}
diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs
index 8f96278fc3..17922f5afa 100644
--- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs
+++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs
@@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Xunit;
@@ -12,11 +13,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
public class DefaultKeyProviderTests
{
private static readonly char KeyDelimiter = '\x1e';
+ private static readonly CachedVaryRules TestVaryRules = new CachedVaryRules()
+ {
+ VaryKeyPrefix = FastGuid.NewGuid().IdString
+ };
[Fact]
public void DefaultKeyProvider_CreateBaseKey_IncludesOnlyNormalizedMethodAndPath()
{
- var httpContext = new DefaultHttpContext();
+ var httpContext = CreateDefaultContext();
httpContext.Request.Method = "head";
httpContext.Request.Path = "/path/subpath";
httpContext.Request.Scheme = "https";
@@ -31,7 +36,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact]
public void DefaultKeyProvider_CreateBaseKey_CaseInsensitivePath_NormalizesPath()
{
- var httpContext = new DefaultHttpContext();
+ var httpContext = CreateDefaultContext();
httpContext.Request.Method = "GET";
httpContext.Request.Path = "/Path";
var keyProvider = CreateTestKeyProvider(new ResponseCachingOptions()
@@ -45,7 +50,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact]
public void DefaultKeyProvider_CreateBaseKey_CaseSensitivePath_PreservesPathCase()
{
- var httpContext = new DefaultHttpContext();
+ var httpContext = CreateDefaultContext();
httpContext.Request.Method = "GET";
httpContext.Request.Path = "/Path";
var keyProvider = CreateTestKeyProvider(new ResponseCachingOptions()
@@ -56,17 +61,25 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Assert.Equal($"GET{KeyDelimiter}/Path", keyProvider.CreateBaseKey(httpContext));
}
+ [Fact]
+ public void DefaultKeyProvider_CreateVaryKey_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()));
+ }
+
[Fact]
public void DefaultKeyProvider_CreateVaryKey_IncludesListedHeadersOnly()
{
- var httpContext = new DefaultHttpContext();
- httpContext.Request.Method = "GET";
- httpContext.Request.Path = "/";
+ var httpContext = CreateDefaultContext();
httpContext.Request.Headers["HeaderA"] = "ValueA";
httpContext.Request.Headers["HeaderB"] = "ValueB";
var keyProvider = CreateTestKeyProvider();
- Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null",
+ Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null",
keyProvider.CreateVaryKey(httpContext, new VaryRules()
{
Headers = new string[] { "HeaderA", "HeaderC" }
@@ -76,13 +89,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact]
public void DefaultKeyProvider_CreateVaryKey_IncludesListedParamsOnly()
{
- var httpContext = new DefaultHttpContext();
- httpContext.Request.Method = "GET";
- httpContext.Request.Path = "/";
+ var httpContext = CreateDefaultContext();
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
var keyProvider = CreateTestKeyProvider();
- Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null",
+ Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null",
keyProvider.CreateVaryKey(httpContext, new VaryRules()
{
Params = new string[] { "ParamA", "ParamC" }
@@ -92,13 +103,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact]
public void DefaultKeyProvider_CreateVaryKey_IncludesParams_ParamNameCaseInsensitive_UseParamCasing()
{
- var httpContext = new DefaultHttpContext();
- httpContext.Request.Method = "GET";
- httpContext.Request.Path = "/";
+ var httpContext = CreateDefaultContext();
httpContext.Request.QueryString = new QueryString("?parama=ValueA¶mB=ValueB");
var keyProvider = CreateTestKeyProvider();
- Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null",
+ Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null",
keyProvider.CreateVaryKey(httpContext, new VaryRules()
{
Params = new string[] { "ParamA", "ParamC" }
@@ -108,15 +117,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact]
public void DefaultKeyProvider_CreateVaryKey_IncludesAllQueryParamsGivenAsterisk()
{
- var httpContext = new DefaultHttpContext();
- httpContext.Request.Method = "GET";
- httpContext.Request.Path = "/";
+ var httpContext = CreateDefaultContext();
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
var keyProvider = CreateTestKeyProvider();
// To support case insensitivity, all param keys are converted to upper case.
// Explicit params uses the casing specified in the setting.
- Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB",
+ Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB",
keyProvider.CreateVaryKey(httpContext, new VaryRules()
{
Params = new string[] { "*" }
@@ -126,15 +133,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
[Fact]
public void DefaultKeyProvider_CreateVaryKey_IncludesListedHeadersAndParams()
{
- var httpContext = new DefaultHttpContext();
- httpContext.Request.Method = "GET";
- httpContext.Request.Path = "/";
+ var httpContext = CreateDefaultContext();
httpContext.Request.Headers["HeaderA"] = "ValueA";
httpContext.Request.Headers["HeaderB"] = "ValueB";
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
var keyProvider = CreateTestKeyProvider();
- Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null",
+ 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()
{
Headers = new string[] { "HeaderA", "HeaderC" },
@@ -142,6 +147,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}));
}
+ private static HttpContext CreateDefaultContext()
+ {
+ var context = new DefaultHttpContext();
+ context.AddResponseCachingState();
+ context.GetResponseCachingState().CachedVaryRules = TestVaryRules;
+ return context;
+ }
+
private static IKeyProvider CreateTestKeyProvider()
{
return CreateTestKeyProvider(new ResponseCachingOptions());
diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs
index c4caca269b..ec11698d0d 100644
--- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs
+++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs
@@ -14,6 +14,7 @@ using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Xunit;
+using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
@@ -154,10 +155,312 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Assert.False(context.ConditionalRequestSatisfied(cachedHeaders));
}
+ [Fact]
+ public void FinalizeCachingHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable()
+ {
+ var httpContext = new DefaultHttpContext();
+ var context = CreateTestContext(httpContext);
+ 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);
+ var state = httpContext.GetResponseCachingState();
+
+ Assert.False(state.ShouldCacheResponse);
+
+ context.FinalizeCachingHeaders();
+
+ Assert.True(state.ShouldCacheResponse);
+ }
+
+ [Fact]
+ public void FinalizeCachingHeaders_DefaultResponseValidity_Is10Seconds()
+ {
+ var httpContext = new DefaultHttpContext();
+ httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
+ {
+ Public = true
+ };
+ var context = CreateTestContext(httpContext);
+
+ context.FinalizeCachingHeaders();
+
+ Assert.Equal(TimeSpan.FromSeconds(10), httpContext.GetResponseCachingState().CachedResponseValidFor);
+ }
+
+ [Fact]
+ 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();
+ 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()
+ {
+ Public = true,
+ 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()
+ {
+ Public = true,
+ 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, new ResponseCachingOptions());
+ 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()
+ {
+ Headers = new StringValues(new[] { "HeaderA", "HeaderB" }),
+ Params = new StringValues(new[] { "ParamA", "ParamB" })
+ }
+ };
+ state.CachedVaryRules = cachedVaryRules;
+
+ context.FinalizeCachingHeaders();
+
+ Assert.Equal(1, cache.StoredItems);
+ Assert.NotSame(cachedVaryRules, state.CachedVaryRules);
+ }
+
+ [Fact]
+ public void FinalizeCachingHeaders_DoNotUpdateCachedVaryRules_IfEquivalentToPrevious()
+ {
+ var httpContext = new DefaultHttpContext();
+ var cache = new TestResponseCache();
+ var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions());
+ 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,
+ VaryRules = new VaryRules()
+ {
+ Headers = new StringValues(new[] { "HEADERA", "HEADERB" }),
+ Params = new StringValues(new[] { "PARAMA", "PARAMB" })
+ }
+ };
+ state.CachedVaryRules = cachedVaryRules;
+
+ context.FinalizeCachingHeaders();
+
+ Assert.Equal(0, cache.StoredItems);
+ Assert.Same(cachedVaryRules, state.CachedVaryRules);
+ }
+
+ [Fact]
+ 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;
+ 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();
+ httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
+ {
+ Public = true
+ };
+ 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();
+ httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
+ {
+ Public = true
+ };
+ 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, new ResponseCachingOptions());
+
+ 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.BaseKey = "BaseKey";
+ state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
+
+ context.FinalizeCachingBody();
+
+ Assert.Equal(2, cache.StoredItems);
+ }
+
+ [Fact]
+ public async Task FinalizeCachingBody_StoreResponseBodyInCachedResponse_IfSmallerThanLimit()
+ {
+ var httpContext = new DefaultHttpContext();
+ var cache = new TestResponseCache();
+ var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions());
+
+ 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.BaseKey = "BaseKey";
+ state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
+
+ context.FinalizeCachingBody();
+
+ Assert.Equal(1, cache.StoredItems);
+ }
+
+ [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)
{
return CreateTestContext(
httpContext,
+ new TestResponseCache(),
new ResponseCachingOptions(),
new CacheabilityValidator());
}
@@ -166,6 +469,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
return CreateTestContext(
httpContext,
+ new TestResponseCache(),
options,
new CacheabilityValidator());
}
@@ -174,12 +478,23 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
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)
{
@@ -187,7 +502,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
return new ResponseCachingContext(
httpContext,
- new TestResponseCache(),
+ responseCache,
options,
cacheabilityValidator,
new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)));
@@ -195,6 +510,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
private class TestResponseCache : IResponseCache
{
+ public int StoredItems { get; private set; }
+
public object Get(string key)
{
return null;
@@ -206,6 +523,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
public void Set(string key, object entry, TimeSpan validFor)
{
+ StoredItems++;
}
}
diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs
index c28c31e8c1..d37d76e30e 100644
--- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs
+++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs
@@ -497,6 +497,29 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
}
}
+ [Fact]
+ public async void ServesCachedContent_WithoutReplacingCachedVaryBy_OnCacheMiss()
+ {
+ var builder = CreateBuilderWithResponseCaching(async (context) =>
+ {
+ context.Response.Headers[HeaderNames.Vary] = HeaderNames.From;
+ await DefaultRequestDelegate(context);
+ });
+
+ using (var server = new TestServer(builder))
+ {
+ var client = server.CreateClient();
+ client.DefaultRequestHeaders.From = "user@example.com";
+ var initialResponse = await client.GetAsync("");
+ client.DefaultRequestHeaders.From = "user2@example.com";
+ var otherResponse = await client.GetAsync("");
+ client.DefaultRequestHeaders.From = "user@example.com";
+ var subsequentResponse = await client.GetAsync("");
+
+ await AssertResponseCachedAsync(initialResponse, subsequentResponse);
+ }
+ }
+
private static async Task AssertResponseCachedAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse)
{
initialResponse.EnsureSuccessStatusCode();