Add option for VaryBy query string params
This commit is contained in:
parent
8c5a5f7394
commit
1d6c5af72c
|
|
@ -8,5 +8,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
|||
internal class CachedVaryBy
|
||||
{
|
||||
internal StringValues Headers { get; set; }
|
||||
internal StringValues Params { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,12 +96,26 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
|||
}
|
||||
|
||||
// Serialization Format
|
||||
// Headers (comma separated string)
|
||||
// Headers count
|
||||
// Headers if count > 0 (comma separated string)
|
||||
// Params count
|
||||
// Params if count > 0 (comma separated string)
|
||||
private static CachedVaryBy ReadCachedVaryBy(BinaryReader reader)
|
||||
{
|
||||
var headers = reader.ReadString().Split(',');
|
||||
var headerCount = reader.ReadInt32();
|
||||
var headers = new string[headerCount];
|
||||
for (var index = 0; index < headerCount; index++)
|
||||
{
|
||||
headers[index] = reader.ReadString();
|
||||
}
|
||||
var paramCount = reader.ReadInt32();
|
||||
var param = new string[paramCount];
|
||||
for (var index = 0; index < paramCount; index++)
|
||||
{
|
||||
param[index] = reader.ReadString();
|
||||
}
|
||||
|
||||
return new CachedVaryBy { Headers = headers };
|
||||
return new CachedVaryBy { Headers = headers, Params = param };
|
||||
}
|
||||
|
||||
// See serialization format above
|
||||
|
|
@ -154,7 +168,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
|||
private static void WriteCachedVaryBy(BinaryWriter writer, CachedVaryBy entry)
|
||||
{
|
||||
writer.Write(nameof(CachedVaryBy));
|
||||
writer.Write(entry.Headers);
|
||||
|
||||
writer.Write(entry.Headers.Count);
|
||||
foreach (var header in entry.Headers)
|
||||
{
|
||||
writer.Write(header);
|
||||
}
|
||||
|
||||
writer.Write(entry.Params.Count);
|
||||
foreach (var param in entry.Params)
|
||||
{
|
||||
writer.Write(param);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
|
@ -19,6 +20,8 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
|||
internal class ResponseCachingContext
|
||||
{
|
||||
private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue();
|
||||
// Use the record separator for delimiting components of the cache key to avoid possible collisions
|
||||
private static readonly char KeyDelimiter = '\x1e';
|
||||
|
||||
private readonly HttpContext _httpContext;
|
||||
private readonly IResponseCache _cache;
|
||||
|
|
@ -150,13 +153,19 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
|||
|
||||
try
|
||||
{
|
||||
// Default key
|
||||
builder
|
||||
.Append(request.Method.ToUpperInvariant())
|
||||
.Append(";")
|
||||
.Append(KeyDelimiter)
|
||||
.Append(request.Path.Value.ToUpperInvariant());
|
||||
|
||||
// Vary by headers
|
||||
if (varyBy?.Headers.Count > 0)
|
||||
{
|
||||
// Append a group separator for the header segment of the cache key
|
||||
builder.Append(KeyDelimiter)
|
||||
.Append('H');
|
||||
|
||||
// TODO: resolve key format and delimiters
|
||||
foreach (var header in varyBy.Headers)
|
||||
{
|
||||
|
|
@ -169,19 +178,62 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
|||
value = "null";
|
||||
}
|
||||
|
||||
builder.Append(";")
|
||||
builder.Append(KeyDelimiter)
|
||||
.Append(header)
|
||||
.Append("=")
|
||||
.Append(value);
|
||||
}
|
||||
}
|
||||
// TODO: Parse querystring params
|
||||
|
||||
// Vary by query params
|
||||
if (varyBy?.Params.Count > 0)
|
||||
{
|
||||
// Append a group separator for the query parameter segment of the cache key
|
||||
builder.Append(KeyDelimiter)
|
||||
.Append('Q');
|
||||
|
||||
if (varyBy.Params.Count == 1 && string.Equals(varyBy.Params[0], "*"))
|
||||
{
|
||||
// Vary by all available query params
|
||||
foreach (var query in _httpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
builder.Append(KeyDelimiter)
|
||||
.Append(query.Key.ToUpperInvariant())
|
||||
.Append("=")
|
||||
.Append(query.Value);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: resolve key format and delimiters
|
||||
foreach (var param in varyBy.Params)
|
||||
{
|
||||
// TODO: Normalization of order, case?
|
||||
var value = _httpContext.Request.Query[param];
|
||||
|
||||
// TODO: How to handle null/empty string?
|
||||
if (StringValues.IsNullOrEmpty(value))
|
||||
{
|
||||
value = "null";
|
||||
}
|
||||
|
||||
builder.Append(KeyDelimiter)
|
||||
.Append(param)
|
||||
.Append("=")
|
||||
.Append(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append custom cache key segment
|
||||
var customKey = _cacheKeySuffixProvider.CreateCustomKeySuffix(_httpContext);
|
||||
if (!string.IsNullOrEmpty(customKey))
|
||||
{
|
||||
builder.Append(";")
|
||||
// Append a group separator for the custom segment of the cache key
|
||||
builder.Append(KeyDelimiter)
|
||||
.Append('C');
|
||||
|
||||
builder.Append(KeyDelimiter)
|
||||
.Append(customKey);
|
||||
}
|
||||
|
||||
|
|
@ -451,6 +503,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
|||
// Create the cache entry now
|
||||
var response = _httpContext.Response;
|
||||
var varyHeaderValue = response.Headers[HeaderNames.Vary];
|
||||
var varyParamsValue = _httpContext.GetResponseCachingFeature().VaryByParams;
|
||||
_cachedResponseValidFor = ResponseCacheControl.SharedMaxAge
|
||||
?? ResponseCacheControl.MaxAge
|
||||
?? (ResponseHeaders.Expires - _responseTime)
|
||||
|
|
@ -458,13 +511,18 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
|||
?? TimeSpan.FromSeconds(10);
|
||||
|
||||
// Check if any VaryBy rules exist
|
||||
if (!StringValues.IsNullOrEmpty(varyHeaderValue))
|
||||
if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue))
|
||||
{
|
||||
if (varyParamsValue.Count > 1)
|
||||
{
|
||||
Array.Sort(varyParamsValue.ToArray(), StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
var cachedVaryBy = new CachedVaryBy
|
||||
{
|
||||
// Only vary by headers for now
|
||||
// TODO: VaryBy Encoding
|
||||
Headers = varyHeaderValue
|
||||
Headers = varyHeaderValue,
|
||||
Params = varyParamsValue
|
||||
};
|
||||
|
||||
// TODO: Overwrite?
|
||||
|
|
@ -536,6 +594,9 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
|||
{
|
||||
_httpContext.Features.Set<IHttpSendFileFeature>(new SendFileFeatureWrapper(OriginalSendFileFeature, ResponseCacheStream));
|
||||
}
|
||||
|
||||
// TODO: Move this temporary interface with endpoint to HttpAbstractions
|
||||
_httpContext.AddResponseCachingFeature();
|
||||
}
|
||||
|
||||
internal void UnshimResponseStream()
|
||||
|
|
@ -545,6 +606,9 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
|||
|
||||
// Unshim IHttpSendFileFeature
|
||||
_httpContext.Features.Set(OriginalSendFileFeature);
|
||||
|
||||
// TODO: Move this temporary interface with endpoint to HttpAbstractions
|
||||
_httpContext.RemoveResponseCachingFeature();
|
||||
}
|
||||
|
||||
private enum ResponseType
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
using System;
|
||||
using Microsoft.AspNetCore.ResponseCaching;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Microsoft.AspNetCore.Builder
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace Microsoft.AspNetCore.ResponseCaching
|
||||
{
|
||||
// TODO: Temporary interface for endpoints to specify options for response caching
|
||||
public class ResponseCachingFeature
|
||||
{
|
||||
public StringValues VaryByParams { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright (c) .NET Foundation. All rights reserved.
|
||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Microsoft.AspNetCore.ResponseCaching
|
||||
{
|
||||
// TODO: Temporary interface for endpoints to specify options for response caching
|
||||
public static class ResponseCachingHttpContextExtensions
|
||||
{
|
||||
public static void AddResponseCachingFeature(this HttpContext httpContext)
|
||||
{
|
||||
httpContext.Features.Set(new ResponseCachingFeature());
|
||||
}
|
||||
|
||||
public static void RemoveResponseCachingFeature(this HttpContext httpContext)
|
||||
{
|
||||
httpContext.Features.Set<ResponseCachingFeature>(null);
|
||||
}
|
||||
|
||||
public static ResponseCachingFeature GetResponseCachingFeature(this HttpContext httpContext)
|
||||
{
|
||||
return httpContext.Features.Get<ResponseCachingFeature>() ?? new ResponseCachingFeature();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,19 +13,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|||
public class DefaultResponseCacheEntrySerializerTests
|
||||
{
|
||||
[Fact]
|
||||
public void SerializeNullObjectThrows()
|
||||
public void Serialize_NullObject_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => DefaultResponseCacheSerializer.Serialize(null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SerializeUnknownObjectThrows()
|
||||
public void Serialize_UnknownObject_Throws()
|
||||
{
|
||||
Assert.Throws<NotSupportedException>(() => DefaultResponseCacheSerializer.Serialize(new object()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTripCachedResponsesSucceeds()
|
||||
public void RoundTrip_CachedResponses_Succeeds()
|
||||
{
|
||||
var headers = new HeaderDictionary();
|
||||
headers["keyA"] = "valueA";
|
||||
|
|
@ -42,7 +42,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTripCachedVaryBySucceeds()
|
||||
public void RoundTrip_Empty_CachedVaryBy_Succeeds()
|
||||
{
|
||||
var cachedVaryBy = new CachedVaryBy();
|
||||
|
||||
AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_HeadersOnly_CachedVaryBy_Succeeds()
|
||||
{
|
||||
var headers = new[] { "headerA", "headerB" };
|
||||
var cachedVaryBy = new CachedVaryBy()
|
||||
|
|
@ -53,9 +61,34 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|||
AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RoundTrip_ParamsOnly_CachedVaryBy_Succeeds()
|
||||
{
|
||||
var param = new[] { "paramA", "paramB" };
|
||||
var cachedVaryBy = new CachedVaryBy()
|
||||
{
|
||||
Params = param
|
||||
};
|
||||
|
||||
AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeserializeInvalidEntriesReturnsNull()
|
||||
public void RoundTrip_HeadersAndParams_CachedVaryBy_Succeeds()
|
||||
{
|
||||
var headers = new[] { "headerA", "headerB" };
|
||||
var param = new[] { "paramA", "paramB" };
|
||||
var cachedVaryBy = new CachedVaryBy()
|
||||
{
|
||||
Headers = headers,
|
||||
Params = param
|
||||
};
|
||||
|
||||
AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_InvalidEntries_ReturnsNull()
|
||||
{
|
||||
var headers = new[] { "headerA", "headerB" };
|
||||
var cachedVaryBy = new CachedVaryBy()
|
||||
|
|
@ -87,6 +120,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|||
Assert.NotNull(actual);
|
||||
Assert.NotNull(expected);
|
||||
Assert.Equal(expected.Headers, actual.Headers);
|
||||
Assert.Equal(expected.Params, actual.Params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|||
{
|
||||
public class ResponseCachingContextTests
|
||||
{
|
||||
private static readonly char KeyDelimiter = '\x1e';
|
||||
|
||||
[Theory]
|
||||
[InlineData("GET")]
|
||||
[InlineData("HEAD")]
|
||||
|
|
@ -171,7 +173,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|||
httpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b");
|
||||
var context = CreateTestContext(httpContext);
|
||||
|
||||
Assert.Equal("HEAD;/PATH/SUBPATH", context.CreateCacheKey());
|
||||
Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", context.CreateCacheKey());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -184,12 +186,77 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|||
httpContext.Request.Headers["HeaderB"] = "ValueB";
|
||||
var context = CreateTestContext(httpContext);
|
||||
|
||||
Assert.Equal("GET;/;HeaderA=ValueA;HeaderC=null", context.CreateCacheKey(new CachedVaryBy()
|
||||
Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null", context.CreateCacheKey(new CachedVaryBy()
|
||||
{
|
||||
Headers = new string[] { "HeaderA", "HeaderC" }
|
||||
}));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateCacheKey_Includes_ListedVaryByParamsOnly()
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Method = "GET";
|
||||
httpContext.Request.Path = "/";
|
||||
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
|
||||
var context = CreateTestContext(httpContext);
|
||||
|
||||
Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", context.CreateCacheKey(new CachedVaryBy()
|
||||
{
|
||||
Params = new string[] { "ParamA", "ParamC" }
|
||||
}));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateCacheKey_Includes_VaryByParams_ParamNameCaseInsensitive_UseVaryByCasing()
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Method = "GET";
|
||||
httpContext.Request.Path = "/";
|
||||
httpContext.Request.QueryString = new QueryString("?parama=ValueA¶mB=ValueB");
|
||||
var context = CreateTestContext(httpContext);
|
||||
|
||||
Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", context.CreateCacheKey(new CachedVaryBy()
|
||||
{
|
||||
Params = new string[] { "ParamA", "ParamC" }
|
||||
}));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateCacheKey_Includes_AllQueryParamsGivenAsterisk()
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Method = "GET";
|
||||
httpContext.Request.Path = "/";
|
||||
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
|
||||
var context = CreateTestContext(httpContext);
|
||||
|
||||
// To support case insensitivity, all param keys are converted to lower case.
|
||||
// Explicit VaryBy uses the casing specified in the setting.
|
||||
Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB", context.CreateCacheKey(new CachedVaryBy()
|
||||
{
|
||||
Params = new string[] { "*" }
|
||||
}));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateCacheKey_Includes_ListedVaryByHeadersAndParams()
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Method = "GET";
|
||||
httpContext.Request.Path = "/";
|
||||
httpContext.Request.Headers["HeaderA"] = "ValueA";
|
||||
httpContext.Request.Headers["HeaderB"] = "ValueB";
|
||||
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
|
||||
var context = CreateTestContext(httpContext);
|
||||
|
||||
Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", context.CreateCacheKey(new CachedVaryBy()
|
||||
{
|
||||
Headers = new string[] { "HeaderA", "HeaderC" },
|
||||
Params = new string[] { "ParamA", "ParamC" }
|
||||
}));
|
||||
}
|
||||
|
||||
private class CustomizeKeySuffixProvider : IResponseCachingCacheKeySuffixProvider
|
||||
{
|
||||
public string CreateCustomKeySuffix(HttpContext httpContext) => "CustomizedKey";
|
||||
|
|
@ -205,7 +272,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|||
httpContext.Request.Headers["HeaderB"] = "ValueB";
|
||||
var responseCachingContext = CreateTestContext(httpContext, new CustomizeKeySuffixProvider());
|
||||
|
||||
Assert.Equal("GET;/;HeaderA=ValueA;HeaderC=null;CustomizedKey", responseCachingContext.CreateCacheKey(new CachedVaryBy()
|
||||
Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null{KeyDelimiter}C{KeyDelimiter}CustomizedKey", responseCachingContext.CreateCacheKey(new CachedVaryBy()
|
||||
{
|
||||
Headers = new string[] { "HeaderA", "HeaderC" }
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
|
|
@ -18,7 +19,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|||
public class ResponseCachingTests
|
||||
{
|
||||
[Fact]
|
||||
public async void ServesCachedContentIfAvailable()
|
||||
public async void ServesCachedContent_IfAvailable()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||
{
|
||||
|
|
@ -40,20 +41,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|||
var initialResponse = await client.GetAsync("");
|
||||
var subsequentResponse = await client.GetAsync("");
|
||||
|
||||
initialResponse.EnsureSuccessStatusCode();
|
||||
subsequentResponse.EnsureSuccessStatusCode();
|
||||
|
||||
foreach (var header in initialResponse.Headers)
|
||||
{
|
||||
Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key));
|
||||
}
|
||||
Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age));
|
||||
Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
|
||||
await AssertResponseCachedAsync(initialResponse, subsequentResponse);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ServesFreshContentIfNotAvailable()
|
||||
public async void ServesFreshContent_IfNotAvailable()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||
{
|
||||
|
|
@ -75,16 +68,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|||
var initialResponse = await client.GetAsync("");
|
||||
var subsequentResponse = await client.GetAsync("/different");
|
||||
|
||||
initialResponse.EnsureSuccessStatusCode();
|
||||
subsequentResponse.EnsureSuccessStatusCode();
|
||||
|
||||
Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age));
|
||||
Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
|
||||
await AssertResponseNotCachedAsync(initialResponse, subsequentResponse);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ServesCachedContentIfVaryByMatches()
|
||||
public async void ServesCachedContent_IfVaryByHeader_Matches()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||
{
|
||||
|
|
@ -108,20 +97,284 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|||
var initialResponse = await client.GetAsync("");
|
||||
var subsequentResponse = await client.GetAsync("");
|
||||
|
||||
initialResponse.EnsureSuccessStatusCode();
|
||||
subsequentResponse.EnsureSuccessStatusCode();
|
||||
|
||||
foreach (var header in initialResponse.Headers)
|
||||
{
|
||||
Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key));
|
||||
}
|
||||
Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age));
|
||||
Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
|
||||
await AssertResponseCachedAsync(initialResponse, subsequentResponse);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ServesFreshContentIfRequestRequirementsNotMet()
|
||||
public async void ServesFreshContent_IfVaryByHeader_Mismatches()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||
{
|
||||
var uniqueId = Guid.NewGuid().ToString();
|
||||
var headers = context.Response.GetTypedHeaders();
|
||||
headers.CacheControl = new CacheControlHeaderValue()
|
||||
{
|
||||
Public = true,
|
||||
MaxAge = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
headers.Date = DateTimeOffset.UtcNow;
|
||||
headers.Headers["X-Value"] = uniqueId;
|
||||
context.Response.Headers[HeaderNames.Vary] = HeaderNames.From;
|
||||
await context.Response.WriteAsync(uniqueId);
|
||||
});
|
||||
|
||||
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 subsequentResponse = await client.GetAsync("");
|
||||
|
||||
await AssertResponseNotCachedAsync(initialResponse, subsequentResponse);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ServesCachedContent_IfVaryByParams_Matches()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||
{
|
||||
var uniqueId = Guid.NewGuid().ToString();
|
||||
var headers = context.Response.GetTypedHeaders();
|
||||
headers.CacheControl = new CacheControlHeaderValue()
|
||||
{
|
||||
Public = true,
|
||||
MaxAge = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
headers.Date = DateTimeOffset.UtcNow;
|
||||
headers.Headers["X-Value"] = uniqueId;
|
||||
context.GetResponseCachingFeature().VaryByParams = "param";
|
||||
await context.Response.WriteAsync(uniqueId);
|
||||
});
|
||||
|
||||
using (var server = new TestServer(builder))
|
||||
{
|
||||
var client = server.CreateClient();
|
||||
var initialResponse = await client.GetAsync("?param=value");
|
||||
var subsequentResponse = await client.GetAsync("?param=value");
|
||||
|
||||
await AssertResponseCachedAsync(initialResponse, subsequentResponse);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ServesCachedContent_IfVaryByParamsExplicit_Matches_ParamNameCaseInsensitive()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(
|
||||
app =>
|
||||
{
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
context.Features.Set<IHttpSendFileFeature>(new DummySendFileFeature());
|
||||
await next.Invoke();
|
||||
});
|
||||
},
|
||||
async (context) =>
|
||||
{
|
||||
var uniqueId = Guid.NewGuid().ToString();
|
||||
var headers = context.Response.GetTypedHeaders();
|
||||
headers.CacheControl = new CacheControlHeaderValue()
|
||||
{
|
||||
Public = true,
|
||||
MaxAge = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
headers.Date = DateTimeOffset.UtcNow;
|
||||
headers.Headers["X-Value"] = uniqueId;
|
||||
context.GetResponseCachingFeature().VaryByParams = new[] { "ParamA", "paramb" };
|
||||
await context.Response.WriteAsync(uniqueId);
|
||||
});
|
||||
|
||||
using (var server = new TestServer(builder))
|
||||
{
|
||||
var client = server.CreateClient();
|
||||
var initialResponse = await client.GetAsync("?parama=valuea¶mb=valueb");
|
||||
var subsequentResponse = await client.GetAsync("?ParamA=valuea&ParamB=valueb");
|
||||
|
||||
await AssertResponseCachedAsync(initialResponse, subsequentResponse);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ServesCachedContent_IfVaryByParamsStar_Matches_ParamNameCaseInsensitive()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(
|
||||
app =>
|
||||
{
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
context.Features.Set<IHttpSendFileFeature>(new DummySendFileFeature());
|
||||
await next.Invoke();
|
||||
});
|
||||
},
|
||||
async (context) =>
|
||||
{
|
||||
var uniqueId = Guid.NewGuid().ToString();
|
||||
var headers = context.Response.GetTypedHeaders();
|
||||
headers.CacheControl = new CacheControlHeaderValue()
|
||||
{
|
||||
Public = true,
|
||||
MaxAge = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
headers.Date = DateTimeOffset.UtcNow;
|
||||
headers.Headers["X-Value"] = uniqueId;
|
||||
context.GetResponseCachingFeature().VaryByParams = new[] { "*" };
|
||||
await context.Response.WriteAsync(uniqueId);
|
||||
});
|
||||
|
||||
using (var server = new TestServer(builder))
|
||||
{
|
||||
var client = server.CreateClient();
|
||||
var initialResponse = await client.GetAsync("?parama=valuea¶mb=valueb");
|
||||
var subsequentResponse = await client.GetAsync("?ParamA=valuea&ParamB=valueb");
|
||||
|
||||
await AssertResponseCachedAsync(initialResponse, subsequentResponse);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ServesCachedContent_IfVaryByParamsExplicit_Matches_OrderInsensitive()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||
{
|
||||
var uniqueId = Guid.NewGuid().ToString();
|
||||
var headers = context.Response.GetTypedHeaders();
|
||||
headers.CacheControl = new CacheControlHeaderValue()
|
||||
{
|
||||
Public = true,
|
||||
MaxAge = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
headers.Date = DateTimeOffset.UtcNow;
|
||||
headers.Headers["X-Value"] = uniqueId;
|
||||
context.GetResponseCachingFeature().VaryByParams = new[] { "ParamB", "ParamA" };
|
||||
await context.Response.WriteAsync(uniqueId);
|
||||
});
|
||||
|
||||
using (var server = new TestServer(builder))
|
||||
{
|
||||
var client = server.CreateClient();
|
||||
var initialResponse = await client.GetAsync("?ParamA=ValueA&ParamB=ValueB");
|
||||
var subsequentResponse = await client.GetAsync("?ParamB=ValueB&ParamA=ValueA");
|
||||
|
||||
await AssertResponseCachedAsync(initialResponse, subsequentResponse);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ServesCachedContent_IfVaryByParamsStar_Matches_OrderInsensitive()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||
{
|
||||
var uniqueId = Guid.NewGuid().ToString();
|
||||
var headers = context.Response.GetTypedHeaders();
|
||||
headers.CacheControl = new CacheControlHeaderValue()
|
||||
{
|
||||
Public = true,
|
||||
MaxAge = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
headers.Date = DateTimeOffset.UtcNow;
|
||||
headers.Headers["X-Value"] = uniqueId;
|
||||
context.GetResponseCachingFeature().VaryByParams = new[] { "*" };
|
||||
await context.Response.WriteAsync(uniqueId);
|
||||
});
|
||||
|
||||
using (var server = new TestServer(builder))
|
||||
{
|
||||
var client = server.CreateClient();
|
||||
var initialResponse = await client.GetAsync("?ParamA=ValueA&ParamB=ValueB");
|
||||
var subsequentResponse = await client.GetAsync("?ParamB=ValueB&ParamA=ValueA");
|
||||
|
||||
await AssertResponseCachedAsync(initialResponse, subsequentResponse);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ServesFreshContent_IfVaryByParams_Mismatches()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||
{
|
||||
var uniqueId = Guid.NewGuid().ToString();
|
||||
var headers = context.Response.GetTypedHeaders();
|
||||
headers.CacheControl = new CacheControlHeaderValue()
|
||||
{
|
||||
Public = true,
|
||||
MaxAge = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
headers.Date = DateTimeOffset.UtcNow;
|
||||
headers.Headers["X-Value"] = uniqueId;
|
||||
context.GetResponseCachingFeature().VaryByParams = "param";
|
||||
await context.Response.WriteAsync(uniqueId);
|
||||
});
|
||||
|
||||
using (var server = new TestServer(builder))
|
||||
{
|
||||
var client = server.CreateClient();
|
||||
var initialResponse = await client.GetAsync("?param=value");
|
||||
var subsequentResponse = await client.GetAsync("?param=value2");
|
||||
|
||||
await AssertResponseNotCachedAsync(initialResponse, subsequentResponse);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ServesFreshContent_IfVaryByParamsExplicit_Mismatch_ParamValueCaseSensitive()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||
{
|
||||
var uniqueId = Guid.NewGuid().ToString();
|
||||
var headers = context.Response.GetTypedHeaders();
|
||||
headers.CacheControl = new CacheControlHeaderValue()
|
||||
{
|
||||
Public = true,
|
||||
MaxAge = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
headers.Date = DateTimeOffset.UtcNow;
|
||||
headers.Headers["X-Value"] = uniqueId;
|
||||
context.GetResponseCachingFeature().VaryByParams = new[] { "ParamA", "ParamB" };
|
||||
await context.Response.WriteAsync(uniqueId);
|
||||
});
|
||||
|
||||
using (var server = new TestServer(builder))
|
||||
{
|
||||
var client = server.CreateClient();
|
||||
var initialResponse = await client.GetAsync("?parama=valuea¶mb=valueb");
|
||||
var subsequentResponse = await client.GetAsync("?parama=ValueA¶mb=ValueB");
|
||||
|
||||
await AssertResponseNotCachedAsync(initialResponse, subsequentResponse);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ServesFreshContent_IfVaryByParamsStar_Mismatch_ParamValueCaseSensitive()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||
{
|
||||
var uniqueId = Guid.NewGuid().ToString();
|
||||
var headers = context.Response.GetTypedHeaders();
|
||||
headers.CacheControl = new CacheControlHeaderValue()
|
||||
{
|
||||
Public = true,
|
||||
MaxAge = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
headers.Date = DateTimeOffset.UtcNow;
|
||||
headers.Headers["X-Value"] = uniqueId;
|
||||
context.GetResponseCachingFeature().VaryByParams = new[] { "*" };
|
||||
await context.Response.WriteAsync(uniqueId);
|
||||
});
|
||||
|
||||
using (var server = new TestServer(builder))
|
||||
{
|
||||
var client = server.CreateClient();
|
||||
var initialResponse = await client.GetAsync("?parama=valuea¶mb=valueb");
|
||||
var subsequentResponse = await client.GetAsync("?parama=ValueA¶mb=ValueB");
|
||||
|
||||
await AssertResponseNotCachedAsync(initialResponse, subsequentResponse);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ServesFreshContent_IfRequestRequirements_NotMet()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||
{
|
||||
|
|
@ -147,50 +400,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|||
};
|
||||
var subsequentResponse = await client.GetAsync("");
|
||||
|
||||
initialResponse.EnsureSuccessStatusCode();
|
||||
subsequentResponse.EnsureSuccessStatusCode();
|
||||
|
||||
Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age));
|
||||
Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
|
||||
await AssertResponseNotCachedAsync(initialResponse, subsequentResponse);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ServesFreshContentIfVaryByMismatches()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||
{
|
||||
var uniqueId = Guid.NewGuid().ToString();
|
||||
var headers = context.Response.GetTypedHeaders();
|
||||
headers.CacheControl = new CacheControlHeaderValue()
|
||||
{
|
||||
Public = true,
|
||||
MaxAge = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
headers.Date = DateTimeOffset.UtcNow;
|
||||
headers.Headers["X-Value"] = uniqueId;
|
||||
context.Response.Headers[HeaderNames.Vary] = HeaderNames.From;
|
||||
await context.Response.WriteAsync(uniqueId);
|
||||
});
|
||||
|
||||
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 subsequentResponse = await client.GetAsync("");
|
||||
|
||||
initialResponse.EnsureSuccessStatusCode();
|
||||
subsequentResponse.EnsureSuccessStatusCode();
|
||||
|
||||
Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age));
|
||||
Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void Serves504IfOnlyIfCachedHeaderIsSpecified()
|
||||
public async void Serves504_IfOnlyIfCachedHeader_IsSpecified()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||
{
|
||||
|
|
@ -222,7 +437,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async void ServesCachedContentWithoutSetCookie()
|
||||
public async void ServesCachedContent_WithoutSetCookie()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(async (context) =>
|
||||
{
|
||||
|
|
@ -263,7 +478,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async void ServesCachedContentIfIHttpSendFileFeatureNotUsed()
|
||||
public async void ServesCachedContent_IfIHttpSendFileFeature_NotUsed()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(
|
||||
app =>
|
||||
|
|
@ -294,20 +509,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|||
var initialResponse = await client.GetAsync("");
|
||||
var subsequentResponse = await client.GetAsync("");
|
||||
|
||||
initialResponse.EnsureSuccessStatusCode();
|
||||
subsequentResponse.EnsureSuccessStatusCode();
|
||||
|
||||
foreach (var header in initialResponse.Headers)
|
||||
{
|
||||
Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key));
|
||||
}
|
||||
Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age));
|
||||
Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
|
||||
await AssertResponseCachedAsync(initialResponse, subsequentResponse);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ServesFreshContentIfIHttpSendFileFeatureUsed()
|
||||
public async void ServesFreshContent_IfIHttpSendFileFeature_Used()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(
|
||||
app =>
|
||||
|
|
@ -339,16 +546,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|||
var initialResponse = await client.GetAsync("");
|
||||
var subsequentResponse = await client.GetAsync("");
|
||||
|
||||
initialResponse.EnsureSuccessStatusCode();
|
||||
subsequentResponse.EnsureSuccessStatusCode();
|
||||
|
||||
Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age));
|
||||
Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
|
||||
await AssertResponseNotCachedAsync(initialResponse, subsequentResponse);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ServesCachedContentIfSubsequentRequestContainsNoStore()
|
||||
public async void ServesCachedContent_IfSubsequentRequest_ContainsNoStore()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(
|
||||
async (context) =>
|
||||
|
|
@ -375,20 +578,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|||
};
|
||||
var subsequentResponse = await client.GetAsync("");
|
||||
|
||||
initialResponse.EnsureSuccessStatusCode();
|
||||
subsequentResponse.EnsureSuccessStatusCode();
|
||||
|
||||
foreach (var header in initialResponse.Headers)
|
||||
{
|
||||
Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key));
|
||||
}
|
||||
Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age));
|
||||
Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
|
||||
await AssertResponseCachedAsync(initialResponse, subsequentResponse);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void ServesFreshContentIfInitialRequestContainsNoStore()
|
||||
public async void ServesFreshContent_IfInitialRequestContains_NoStore()
|
||||
{
|
||||
var builder = CreateBuilderWithResponseCaching(
|
||||
async (context) =>
|
||||
|
|
@ -415,14 +610,32 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
|||
var initialResponse = await client.GetAsync("");
|
||||
var subsequentResponse = await client.GetAsync("");
|
||||
|
||||
initialResponse.EnsureSuccessStatusCode();
|
||||
subsequentResponse.EnsureSuccessStatusCode();
|
||||
|
||||
Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age));
|
||||
Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
|
||||
await AssertResponseNotCachedAsync(initialResponse, subsequentResponse);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task AssertResponseCachedAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse)
|
||||
{
|
||||
initialResponse.EnsureSuccessStatusCode();
|
||||
subsequentResponse.EnsureSuccessStatusCode();
|
||||
|
||||
foreach (var header in initialResponse.Headers)
|
||||
{
|
||||
Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key));
|
||||
}
|
||||
Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age));
|
||||
Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
private static async Task AssertResponseNotCachedAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse)
|
||||
{
|
||||
initialResponse.EnsureSuccessStatusCode();
|
||||
subsequentResponse.EnsureSuccessStatusCode();
|
||||
|
||||
Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age));
|
||||
Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
private static IWebHostBuilder CreateBuilderWithResponseCaching(RequestDelegate requestDelegate) =>
|
||||
CreateBuilderWithResponseCaching(app => { }, requestDelegate);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue