Store body and header separately, preliminary sharding

- Add fast id
This commit is contained in:
John Luo 2016-09-07 16:57:49 -07:00
parent 7300d9e936
commit 6a04fe5fb7
16 changed files with 727 additions and 137 deletions

View File

@ -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);
}
}
}

View File

@ -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; }

View File

@ -3,8 +3,8 @@
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
internal class CachedVaryRules
internal class CachedResponseBody
{
internal VaryRules VaryRules;
internal byte[] Body { get; set; }
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -38,6 +38,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
internal CachedResponse CachedResponse { get; set; }
internal CachedVaryRules CachedVaryRules { get; set; }
public RequestHeaders RequestHeaders
{
get

View File

@ -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)

View File

@ -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);
}
}
}
}

View File

@ -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>

View File

@ -2,6 +2,7 @@
"version": "0.1.0-*",
"buildOptions": {
"warningsAsErrors": true,
"allowUnsafe": true,
"keyFile": "../../tools/Key.snk",
"nowarn": [
"CS1591"

View File

@ -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);
}
}
}

View File

@ -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&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" }
@ -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());

View File

@ -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++;
}
}

View File

@ -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();