Store body and header separately, preliminary sharding
- Add fast id
This commit is contained in:
parent
7300d9e936
commit
6a04fe5fb7
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
|
|
@ -3,8 +3,8 @@
|
|||
|
||||
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||
{
|
||||
internal class CachedVaryRules
|
||||
internal class CachedResponseBody
|
||||
{
|
||||
internal VaryRules VaryRules;
|
||||
internal byte[] Body { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -8,18 +8,18 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
|||
public interface IKeyProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a key using the HTTP request.
|
||||
/// Create a base key using the HTTP request.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
|
||||
/// <returns>The created base key.</returns>
|
||||
string CreateBaseKey(HttpContext httpContext);
|
||||
|
||||
/// <summary>
|
||||
/// Create a key using the HTTP context and vary rules.
|
||||
/// Create a vary key using the HTTP context and vary rules.
|
||||
/// </summary>
|
||||
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
|
||||
/// <param name="varyRules">The <see cref="VaryRules"/>.</param>
|
||||
/// <returns>The created base key.</returns>
|
||||
/// <returns>The created vary key.</returns>
|
||||
string CreateVaryKey(HttpContext httpContext, VaryRules varyRules);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
|||
|
||||
internal CachedResponse CachedResponse { get; set; }
|
||||
|
||||
internal CachedVaryRules CachedVaryRules { get; set; }
|
||||
|
||||
public RequestHeaders RequestHeaders
|
||||
{
|
||||
get
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ namespace Microsoft.AspNetCore.Builder
|
|||
/// </summary>
|
||||
public bool CaseSensitivePaths { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public long MinimumSplitBodySize { get; set; } = 70 * 1024;
|
||||
|
||||
/// <summary>
|
||||
/// For testing purposes only.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"version": "0.1.0-*",
|
||||
"buildOptions": {
|
||||
"warningsAsErrors": true,
|
||||
"allowUnsafe": true,
|
||||
"keyFile": "../../tools/Key.snk",
|
||||
"nowarn": [
|
||||
"CS1591"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue