Add option for VaryBy query string params

This commit is contained in:
John Luo 2016-08-26 12:39:27 -07:00
parent 8c5a5f7394
commit 1d6c5af72c
9 changed files with 562 additions and 120 deletions

View File

@ -8,5 +8,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
internal class CachedVaryBy
{
internal StringValues Headers { get; set; }
internal StringValues Params { get; set; }
}
}

View File

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

View File

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

View File

@ -3,7 +3,6 @@
using System;
using Microsoft.AspNetCore.ResponseCaching;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Builder
{

View File

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

View File

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

View File

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

View File

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

View File

@ -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&paramb=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&paramb=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&paramb=valueb");
var subsequentResponse = await client.GetAsync("?parama=ValueA&paramb=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&paramb=valueb");
var subsequentResponse = await client.GetAsync("?parama=ValueA&paramb=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);