Allow lookup of multiple keys

- Do not cache if content-length mismatches with the length of the response body
This commit is contained in:
John Luo 2016-09-08 17:16:32 -07:00
parent 6a04fe5fb7
commit 65b89668bb
6 changed files with 435 additions and 183 deletions

View File

@ -1,6 +1,7 @@
// 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.Collections.Generic;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.ResponseCaching
@ -8,18 +9,33 @@ namespace Microsoft.AspNetCore.ResponseCaching
public interface IKeyProvider
{
/// <summary>
/// Create a base key using the HTTP request.
/// Create a base key using the HTTP context for storing items.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
/// <returns>The created base key.</returns>
string CreateBaseKey(HttpContext httpContext);
string CreateStorageBaseKey(HttpContext httpContext);
/// <summary>
/// Create a vary key using the HTTP context and vary rules.
/// Create one or more base keys using the HTTP context for looking up items.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
/// <returns>An ordered <see cref="IEnumerable{T}"/> containing the base keys to try when looking up items.</returns>
IEnumerable<string> CreateLookupBaseKey(HttpContext httpContext);
/// <summary>
/// Create a vary key using the HTTP context and vary rules for storing items.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
/// <param name="varyRules">The <see cref="VaryRules"/>.</param>
/// <returns>The created vary key.</returns>
string CreateVaryKey(HttpContext httpContext, VaryRules varyRules);
string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules);
/// <summary>
/// Create one or more vary keys using the HTTP context and vary rules for looking up items.
/// </summary>
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
/// <param name="varyRules">The <see cref="VaryRules"/>.</param>
/// <returns>An ordered <see cref="IEnumerable{T}"/> containing the vary keys to try when looking up items.</returns>
IEnumerable<string> CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules);
}
}

View File

@ -26,9 +26,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
public bool ShouldCacheResponse { get; internal set; }
public string BaseKey { get; internal set; }
public string StorageBaseKey { get; internal set; }
public string VaryKey { get; internal set; }
public string StorageVaryKey { get; internal set; }
public DateTimeOffset ResponseTime { get; internal set; }

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.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Builder;
@ -35,9 +36,19 @@ namespace Microsoft.AspNetCore.ResponseCaching
_options = options.Value;
}
public virtual IEnumerable<string> CreateLookupBaseKey(HttpContext httpContext)
{
return new string[] { CreateStorageBaseKey(httpContext) };
}
public virtual IEnumerable<string> CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules)
{
return new string[] { CreateStorageVaryKey(httpContext, varyRules) };
}
// GET<delimiter>/PATH
// TODO: Method invariant retrieval? E.g. HEAD after GET to the same resource.
public virtual string CreateBaseKey(HttpContext httpContext)
public virtual string CreateStorageBaseKey(HttpContext httpContext)
{
if (httpContext == null)
{
@ -63,7 +74,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
}
// BaseKey<delimiter>H<delimiter>HeaderName=HeaderValue<delimiter>Q<delimiter>QueryName=QueryValue
public virtual string CreateVaryKey(HttpContext httpContext, VaryRules varyRules)
public virtual string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules)
{
if (httpContext == null)
{

View File

@ -59,84 +59,101 @@ namespace Microsoft.AspNetCore.ResponseCaching
private IHttpSendFileFeature OriginalSendFileFeature { get; set; }
internal async Task<bool> TryServeFromCacheAsync()
internal async Task<bool> TryServeCachedResponseAsync(CachedResponse cachedResponse)
{
State.BaseKey = _keyProvider.CreateBaseKey(_httpContext);
var cacheEntry = _cache.Get(State.BaseKey);
var responseServed = false;
State.CachedResponse = cachedResponse;
var cachedResponseHeaders = new ResponseHeaders(State.CachedResponse.Headers);
if (cacheEntry is CachedVaryRules)
State.ResponseTime = _options.SystemClock.UtcNow;
var cachedEntryAge = State.ResponseTime - State.CachedResponse.Created;
State.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero;
if (_cacheabilityValidator.CachedEntryIsFresh(_httpContext, cachedResponseHeaders))
{
// 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)
{
State.CachedResponse = cacheEntry as CachedResponse;
var cachedResponseHeaders = new ResponseHeaders(State.CachedResponse.Headers);
State.ResponseTime = _options.SystemClock.UtcNow;
var cachedEntryAge = State.ResponseTime - State.CachedResponse.Created;
State.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero;
if (_cacheabilityValidator.CachedEntryIsFresh(_httpContext, cachedResponseHeaders))
// Check conditional request rules
if (ConditionalRequestSatisfied(cachedResponseHeaders))
{
responseServed = true;
// Check conditional request rules
if (ConditionalRequestSatisfied(cachedResponseHeaders))
{
_httpContext.Response.StatusCode = StatusCodes.Status304NotModified;
}
else
{
var response = _httpContext.Response;
// Copy the cached status code and response 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;
// If the body is not found, something went wrong.
if (body == null)
{
return false;
}
// Copy the cached response body
if (body.Length > 0)
{
// Add a content-length if required
if (response.ContentLength == null && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding]))
{
response.ContentLength = body.Length;
}
await response.Body.WriteAsync(body, 0, body.Length);
}
}
_httpContext.Response.StatusCode = StatusCodes.Status304NotModified;
}
else
{
// TODO: Validate with endpoint instead
var response = _httpContext.Response;
// Copy the cached status code and response 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;
// If the body is not found, something went wrong.
if (body == null)
{
return false;
}
// Copy the cached response body
if (body.Length > 0)
{
// Add a content-length if required
if (response.ContentLength == null && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding]))
{
response.ContentLength = body.Length;
}
await response.Body.WriteAsync(body, 0, body.Length);
}
}
return true;
}
else
{
// TODO: Validate with endpoint instead
}
return false;
}
internal async Task<bool> TryServeFromCacheAsync()
{
foreach (var baseKey in _keyProvider.CreateLookupBaseKey(_httpContext))
{
var cacheEntry = _cache.Get(baseKey);
if (cacheEntry is CachedVaryRules)
{
// Request contains vary rules, recompute key(s) and try again
State.CachedVaryRules = cacheEntry as CachedVaryRules;
foreach (var varyKey in _keyProvider.CreateLookupVaryKey(_httpContext, State.CachedVaryRules.VaryRules))
{
cacheEntry = _cache.Get(varyKey);
if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(cacheEntry as CachedResponse))
{
return true;
}
}
}
if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(cacheEntry as CachedResponse))
{
return true;
}
}
if (!responseServed && State.RequestCacheControl.OnlyIfCached)
if (State.RequestCacheControl.OnlyIfCached)
{
_httpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
responseServed = true;
return true;
}
return responseServed;
return false;
}
internal bool ConditionalRequestSatisfied(ResponseHeaders cachedResponseHeaders)
@ -174,6 +191,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
if (_cacheabilityValidator.ResponseIsCacheable(_httpContext))
{
State.ShouldCacheResponse = true;
State.StorageBaseKey = _keyProvider.CreateStorageBaseKey(_httpContext);
// Create the cache entry now
var response = _httpContext.Response;
@ -209,10 +227,10 @@ namespace Microsoft.AspNetCore.ResponseCaching
};
State.CachedVaryRules = cachedVaryRules;
_cache.Set(State.BaseKey, cachedVaryRules, State.CachedResponseValidFor);
_cache.Set(State.StorageBaseKey, cachedVaryRules, State.CachedResponseValidFor);
}
State.VaryKey = _keyProvider.CreateVaryKey(_httpContext, State.CachedVaryRules.VaryRules);
State.StorageVaryKey = _keyProvider.CreateStorageVaryKey(_httpContext, State.CachedVaryRules.VaryRules);
}
// Ensure date header is set
@ -245,12 +263,15 @@ namespace Microsoft.AspNetCore.ResponseCaching
internal void FinalizeCachingBody()
{
if (State.ShouldCacheResponse && ResponseCacheStream.BufferingEnabled)
if (State.ShouldCacheResponse &&
ResponseCacheStream.BufferingEnabled &&
(State.ResponseHeaders.ContentLength == null ||
State.ResponseHeaders.ContentLength == ResponseCacheStream.BufferedStream.Length))
{
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.StorageVaryKey ?? State.StorageBaseKey, State.CachedResponse, State.CachedResponseValidFor);
var cachedResponseBody = new CachedResponseBody()
{
@ -263,7 +284,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
{
// Store response and response body together
State.CachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray();
_cache.Set(State.VaryKey ?? State.BaseKey, State.CachedResponse, State.CachedResponseValidFor);
_cache.Set(State.StorageVaryKey ?? State.StorageBaseKey, State.CachedResponse, State.CachedResponseValidFor);
}
}
}

View File

@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
};
[Fact]
public void DefaultKeyProvider_CreateBaseKey_IncludesOnlyNormalizedMethodAndPath()
public void DefaultKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodAndPath()
{
var httpContext = CreateDefaultContext();
httpContext.Request.Method = "head";
@ -30,11 +30,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
httpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b");
var keyProvider = CreateTestKeyProvider();
Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", keyProvider.CreateBaseKey(httpContext));
Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", keyProvider.CreateStorageBaseKey(httpContext));
}
[Fact]
public void DefaultKeyProvider_CreateBaseKey_CaseInsensitivePath_NormalizesPath()
public void DefaultKeyProvider_CreateStorageBaseKey_CaseInsensitivePath_NormalizesPath()
{
var httpContext = CreateDefaultContext();
httpContext.Request.Method = "GET";
@ -44,11 +44,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
CaseSensitivePaths = false
});
Assert.Equal($"GET{KeyDelimiter}/PATH", keyProvider.CreateBaseKey(httpContext));
Assert.Equal($"GET{KeyDelimiter}/PATH", keyProvider.CreateStorageBaseKey(httpContext));
}
[Fact]
public void DefaultKeyProvider_CreateBaseKey_CaseSensitivePath_PreservesPathCase()
public void DefaultKeyProvider_CreateStorageBaseKey_CaseSensitivePath_PreservesPathCase()
{
var httpContext = CreateDefaultContext();
httpContext.Request.Method = "GET";
@ -58,21 +58,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
CaseSensitivePaths = true
});
Assert.Equal($"GET{KeyDelimiter}/Path", keyProvider.CreateBaseKey(httpContext));
Assert.Equal($"GET{KeyDelimiter}/Path", keyProvider.CreateStorageBaseKey(httpContext));
}
[Fact]
public void DefaultKeyProvider_CreateVaryKey_ReturnsCachedVaryGuid_IfVaryRulesIsNullOrEmpty()
public void DefaultKeyProvider_CreateStorageVaryKey_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()));
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateStorageVaryKey(httpContext, null));
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateStorageVaryKey(httpContext, new VaryRules()));
}
[Fact]
public void DefaultKeyProvider_CreateVaryKey_IncludesListedHeadersOnly()
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly()
{
var httpContext = CreateDefaultContext();
httpContext.Request.Headers["HeaderA"] = "ValueA";
@ -80,42 +80,42 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var keyProvider = CreateTestKeyProvider();
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null",
keyProvider.CreateVaryKey(httpContext, new VaryRules()
keyProvider.CreateStorageVaryKey(httpContext, new VaryRules()
{
Headers = new string[] { "HeaderA", "HeaderC" }
}));
}
[Fact]
public void DefaultKeyProvider_CreateVaryKey_IncludesListedParamsOnly()
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedParamsOnly()
{
var httpContext = CreateDefaultContext();
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
var keyProvider = CreateTestKeyProvider();
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null",
keyProvider.CreateVaryKey(httpContext, new VaryRules()
keyProvider.CreateStorageVaryKey(httpContext, new VaryRules()
{
Params = new string[] { "ParamA", "ParamC" }
}));
}
[Fact]
public void DefaultKeyProvider_CreateVaryKey_IncludesParams_ParamNameCaseInsensitive_UseParamCasing()
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesParams_ParamNameCaseInsensitive_UseParamCasing()
{
var httpContext = CreateDefaultContext();
httpContext.Request.QueryString = new QueryString("?parama=ValueA&paramB=ValueB");
var keyProvider = CreateTestKeyProvider();
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null",
keyProvider.CreateVaryKey(httpContext, new VaryRules()
keyProvider.CreateStorageVaryKey(httpContext, new VaryRules()
{
Params = new string[] { "ParamA", "ParamC" }
}));
}
[Fact]
public void DefaultKeyProvider_CreateVaryKey_IncludesAllQueryParamsGivenAsterisk()
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesAllQueryParamsGivenAsterisk()
{
var httpContext = CreateDefaultContext();
httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB");
@ -124,14 +124,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
// To support case insensitivity, all param keys are converted to upper case.
// Explicit params uses the casing specified in the setting.
Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB",
keyProvider.CreateVaryKey(httpContext, new VaryRules()
keyProvider.CreateStorageVaryKey(httpContext, new VaryRules()
{
Params = new string[] { "*" }
}));
}
[Fact]
public void DefaultKeyProvider_CreateVaryKey_IncludesListedHeadersAndParams()
public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndParams()
{
var httpContext = CreateDefaultContext();
httpContext.Request.Headers["HeaderA"] = "ValueA";
@ -140,7 +140,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var keyProvider = CreateTestKeyProvider();
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()
keyProvider.CreateStorageVaryKey(httpContext, new VaryRules()
{
Headers = new string[] { "HeaderA", "HeaderC" },
Params = new string[] { "ParamA", "ParamC" }

View File

@ -10,16 +10,100 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Xunit;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
public class ResponseCachingContextTests
{
[Fact]
public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504()
{
var cache = new TestResponseCache();
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider());
httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
OnlyIfCached = true
};
Assert.True(await context.TryServeFromCacheAsync());
Assert.Equal(StatusCodes.Status504GatewayTimeout, httpContext.Response.StatusCode);
}
[Fact]
public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails()
{
var cache = new TestResponseCache();
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }));
Assert.False(await context.TryServeFromCacheAsync());
Assert.Equal(2, cache.GetCount);
}
[Fact]
public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds()
{
var cache = new TestResponseCache();
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }));
cache.Set(
"BaseKey2",
new CachedResponse()
{
Body = new byte[0]
},
TimeSpan.Zero);
Assert.True(await context.TryServeFromCacheAsync());
Assert.Equal(2, cache.GetCount);
}
[Fact]
public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseNotFound_Fails()
{
var cache = new TestResponseCache();
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }));
cache.Set(
"BaseKey2",
new CachedVaryRules(),
TimeSpan.Zero);
Assert.False(await context.TryServeFromCacheAsync());
Assert.Equal(2, cache.GetCount);
}
[Fact]
public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseFound_Succeeds()
{
var cache = new TestResponseCache();
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }, new[] { "VaryKey", "VaryKey2" }));
cache.Set(
"BaseKey2",
new CachedVaryRules(),
TimeSpan.Zero);
cache.Set(
"BaseKey2VaryKey2",
new CachedResponse()
{
Body = new byte[0]
},
TimeSpan.Zero);
Assert.True(await context.TryServeFromCacheAsync());
Assert.Equal(6, cache.GetCount);
}
[Fact]
public void ConditionalRequestSatisfied_NotConditionalRequest_Fails()
@ -159,7 +243,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
public void FinalizeCachingHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable()
{
var httpContext = new DefaultHttpContext();
var context = CreateTestContext(httpContext);
var context = CreateTestContext(httpContext, cacheabilityValidator: new CacheabilityValidator());
var state = httpContext.GetResponseCachingState();
Assert.False(state.ShouldCacheResponse);
@ -178,7 +262,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
Public = true
};
var context = CreateTestContext(httpContext);
var context = CreateTestContext(httpContext, cacheabilityValidator: new CacheabilityValidator());
var state = httpContext.GetResponseCachingState();
Assert.False(state.ShouldCacheResponse);
@ -192,10 +276,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
public void FinalizeCachingHeaders_DefaultResponseValidity_Is10Seconds()
{
var httpContext = new DefaultHttpContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true
};
var context = CreateTestContext(httpContext);
context.FinalizeCachingHeaders();
@ -207,10 +287,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
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();
@ -229,7 +305,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var httpContext = new DefaultHttpContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true,
MaxAge = TimeSpan.FromSeconds(12)
};
var context = CreateTestContext(httpContext);
@ -249,7 +324,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
var httpContext = new DefaultHttpContext();
httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true,
MaxAge = TimeSpan.FromSeconds(12),
SharedMaxAge = TimeSpan.FromSeconds(13)
};
@ -269,16 +343,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions());
var context = CreateTestContext(httpContext, cache);
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()
@ -291,7 +361,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.FinalizeCachingHeaders();
Assert.Equal(1, cache.StoredItems);
Assert.Equal(1, cache.SetCount);
Assert.NotSame(cachedVaryRules, state.CachedVaryRules);
}
@ -300,16 +370,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions());
var context = CreateTestContext(httpContext, cache);
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,
@ -323,7 +389,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
context.FinalizeCachingHeaders();
Assert.Equal(0, cache.StoredItems);
Assert.Equal(0, cache.SetCount);
Assert.Same(cachedVaryRules, state.CachedVaryRules);
}
@ -331,10 +397,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
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;
@ -351,10 +413,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
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;
@ -372,10 +430,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
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();
@ -391,7 +445,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions());
var context = CreateTestContext(httpContext, cache);
context.ShimResponseStream();
await httpContext.Response.WriteAsync(new string('0', 70 * 1024));
@ -402,12 +456,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
state.BaseKey = "BaseKey";
state.StorageBaseKey = "BaseKey";
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
context.FinalizeCachingBody();
Assert.Equal(2, cache.StoredItems);
Assert.Equal(2, cache.SetCount);
}
[Fact]
@ -415,7 +469,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions());
var context = CreateTestContext(httpContext, cache);
context.ShimResponseStream();
await httpContext.Response.WriteAsync(new string('0', 70 * 1024 - 1));
@ -426,12 +480,113 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
state.BaseKey = "BaseKey";
state.StorageBaseKey = "BaseKey";
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
context.FinalizeCachingBody();
Assert.Equal(1, cache.StoredItems);
Assert.Equal(1, cache.SetCount);
}
[Fact]
public async Task FinalizeCachingBody_StoreResponseBodySeparately_LimitIsConfigurable()
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions()
{
MinimumSplitBodySize = 2048
});
context.ShimResponseStream();
await httpContext.Response.WriteAsync(new string('0', 1024));
var state = httpContext.GetResponseCachingState();
state.ShouldCacheResponse = true;
state.CachedResponse = new CachedResponse()
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
state.StorageBaseKey = "BaseKey";
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
context.FinalizeCachingBody();
Assert.Equal(1, cache.SetCount);
}
[Fact]
public async Task FinalizeCachingBody_Cache_IfContentLengthMatches()
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
var context = CreateTestContext(httpContext, cache);
context.ShimResponseStream();
httpContext.Response.ContentLength = 10;
await httpContext.Response.WriteAsync(new string('0', 10));
var state = httpContext.GetResponseCachingState();
state.ShouldCacheResponse = true;
state.CachedResponse = new CachedResponse()
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
state.StorageBaseKey = "BaseKey";
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
context.FinalizeCachingBody();
Assert.Equal(1, cache.SetCount);
}
[Fact]
public async Task FinalizeCachingBody_DoNotCache_IfContentLengthMismatches()
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
var context = CreateTestContext(httpContext, cache);
context.ShimResponseStream();
httpContext.Response.ContentLength = 9;
await httpContext.Response.WriteAsync(new string('0', 10));
var state = httpContext.GetResponseCachingState();
state.ShouldCacheResponse = true;
state.CachedResponse = new CachedResponse()
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
state.StorageBaseKey = "BaseKey";
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
context.FinalizeCachingBody();
Assert.Equal(0, cache.SetCount);
}
[Fact]
public async Task FinalizeCachingBody_Cache_IfContentLengthAbsent()
{
var httpContext = new DefaultHttpContext();
var cache = new TestResponseCache();
var context = CreateTestContext(httpContext, cache);
context.ShimResponseStream();
await httpContext.Response.WriteAsync(new string('0', 10));
var state = httpContext.GetResponseCachingState();
state.ShouldCacheResponse = true;
state.CachedResponse = new CachedResponse()
{
BodyKeyPrefix = FastGuid.NewGuid().IdString
};
state.StorageBaseKey = "BaseKey";
state.CachedResponseValidFor = TimeSpan.FromSeconds(10);
context.FinalizeCachingBody();
Assert.Equal(1, cache.SetCount);
}
[Fact]
@ -456,48 +611,30 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
Assert.Equal(orderedStrings, normalizedStrings);
}
private static ResponseCachingContext CreateTestContext(HttpContext httpContext)
{
return CreateTestContext(
httpContext,
new TestResponseCache(),
new ResponseCachingOptions(),
new CacheabilityValidator());
}
private static ResponseCachingContext CreateTestContext(HttpContext httpContext, ResponseCachingOptions options)
{
return CreateTestContext(
httpContext,
new TestResponseCache(),
options,
new CacheabilityValidator());
}
private static ResponseCachingContext CreateTestContext(HttpContext httpContext, ICacheabilityValidator cacheabilityValidator)
{
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)
IResponseCache responseCache = null,
ResponseCachingOptions options = null,
IKeyProvider keyProvider = null,
ICacheabilityValidator cacheabilityValidator = null)
{
if (responseCache == null)
{
responseCache = new TestResponseCache();
}
if (options == null)
{
options = new ResponseCachingOptions();
}
if (keyProvider == null)
{
keyProvider = new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options));
}
if (cacheabilityValidator == null)
{
cacheabilityValidator = new TestCacheabilityValidator();
}
httpContext.AddResponseCachingState();
return new ResponseCachingContext(
@ -505,16 +642,82 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
responseCache,
options,
cacheabilityValidator,
new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)));
keyProvider);
}
private class TestCacheabilityValidator : ICacheabilityValidator
{
public bool CachedEntryIsFresh(HttpContext httpContext, ResponseHeaders cachedResponseHeaders) => true;
public bool RequestIsCacheable(HttpContext httpContext) => true;
public bool ResponseIsCacheable(HttpContext httpContext) => true;
}
private class TestKeyProvider : IKeyProvider
{
private readonly StringValues _baseKey;
private readonly StringValues _varyKey;
public TestKeyProvider(StringValues? lookupBaseKey = null, StringValues? lookupVaryKey = null)
{
if (lookupBaseKey.HasValue)
{
_baseKey = lookupBaseKey.Value;
}
if (lookupVaryKey.HasValue)
{
_varyKey = lookupVaryKey.Value;
}
}
public IEnumerable<string> CreateLookupBaseKey(HttpContext httpContext) => _baseKey;
public IEnumerable<string> CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules)
{
foreach (var baseKey in _baseKey)
{
foreach (var varyKey in _varyKey)
{
yield return baseKey + varyKey;
}
}
}
public string CreateBodyKey(HttpContext httpContext)
{
throw new NotImplementedException();
}
public string CreateStorageBaseKey(HttpContext httpContext)
{
throw new NotImplementedException();
}
public string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules)
{
throw new NotImplementedException();
}
}
private class TestResponseCache : IResponseCache
{
public int StoredItems { get; private set; }
private readonly IDictionary<string, object> _storage = new Dictionary<string, object>();
public int GetCount { get; private set; }
public int SetCount { get; private set; }
public object Get(string key)
{
return null;
GetCount++;
try
{
return _storage[key];
}
catch
{
return null;
}
}
public void Remove(string key)
@ -523,7 +726,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
public void Set(string key, object entry, TimeSpan validFor)
{
StoredItems++;
SetCount++;
_storage[key] = entry;
}
}
@ -531,7 +735,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
{
return Task.FromResult(0);
return TaskCache.CompletedTask;
}
}
}