diff --git a/src/Microsoft.AspNetCore.ResponseCaching/IResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCache.cs
similarity index 100%
rename from src/Microsoft.AspNetCore.ResponseCaching/IResponseCache.cs
rename to src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCache.cs
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheKeySuffixProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheKeySuffixProvider.cs
new file mode 100644
index 0000000..7514fa4
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheKeySuffixProvider.cs
@@ -0,0 +1,17 @@
+// 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
+{
+ public interface IResponseCachingCacheKeySuffixProvider
+ {
+ ///
+ /// Create a key segment that is appended to the default cache key.
+ ///
+ /// The .
+ /// The key segment that will be appended to the default cache key.
+ string CreateCustomKeySuffix(HttpContext httpContext);
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheabilityValidator.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheabilityValidator.cs
new file mode 100644
index 0000000..11b4656
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheabilityValidator.cs
@@ -0,0 +1,24 @@
+// 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
+{
+ public interface IResponseCachingCacheabilityValidator
+ {
+ ///
+ /// Override default behavior for determining cacheability of an HTTP request.
+ ///
+ /// The .
+ /// The .
+ OverrideResult RequestIsCacheableOverride(HttpContext httpContext);
+
+ ///
+ /// Override default behavior for determining cacheability of an HTTP response.
+ ///
+ /// The .
+ /// The .
+ OverrideResult ResponseIsCacheableOverride(HttpContext httpContext);
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheKeySuffixProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheKeySuffixProvider.cs
new file mode 100644
index 0000000..43daeb5
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheKeySuffixProvider.cs
@@ -0,0 +1,12 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.ResponseCaching.Internal
+{
+ internal class NoopCacheKeySuffixProvider : IResponseCachingCacheKeySuffixProvider
+ {
+ public string CreateCustomKeySuffix(HttpContext httpContext) => null;
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheabilityValidator.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheabilityValidator.cs
new file mode 100644
index 0000000..309e900
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheabilityValidator.cs
@@ -0,0 +1,14 @@
+// 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.Internal
+{
+ internal class NoopCacheabilityValidator : IResponseCachingCacheabilityValidator
+ {
+ public OverrideResult RequestIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic;
+
+ public OverrideResult ResponseIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic;
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/OverrideResult.cs b/src/Microsoft.AspNetCore.ResponseCaching/OverrideResult.cs
new file mode 100644
index 0000000..e2a168a
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCaching/OverrideResult.cs
@@ -0,0 +1,23 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.ResponseCaching
+{
+ public enum OverrideResult
+ {
+ ///
+ /// Use the default logic for determining cacheability.
+ ///
+ UseDefaultLogic,
+
+ ///
+ /// Ignore default logic and do not cache.
+ ///
+ DoNotCache,
+
+ ///
+ /// Ignore default logic and cache.
+ ///
+ Cache
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs
index 54f6384..2ba747d 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs
@@ -10,14 +10,23 @@
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.AspNetCore.ResponseCaching.Internal;
+using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.ResponseCaching
{
- public class ResponseCachingContext
+ internal class ResponseCachingContext
{
private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue();
+
+ private readonly HttpContext _httpContext;
+ private readonly IResponseCache _cache;
+ private readonly ISystemClock _clock;
+ private readonly ObjectPool _builderPool;
+ private readonly IResponseCachingCacheabilityValidator _cacheabilityValidator;
+ private readonly IResponseCachingCacheKeySuffixProvider _cacheKeySuffixProvider;
+
private string _cacheKey;
private ResponseType? _responseType;
private RequestHeaders _requestHeaders;
@@ -28,31 +37,32 @@ public class ResponseCachingContext
private CachedResponse _cachedResponse;
private TimeSpan _cachedResponseValidFor;
internal DateTimeOffset _responseTime;
-
- public ResponseCachingContext(HttpContext httpContext, IResponseCache cache)
- : this(httpContext, cache, new SystemClock())
+
+ internal ResponseCachingContext(
+ HttpContext httpContext,
+ IResponseCache cache,
+ ObjectPool builderPool,
+ IResponseCachingCacheabilityValidator cacheabilityValidator,
+ IResponseCachingCacheKeySuffixProvider cacheKeySuffixProvider)
+ : this(httpContext, cache, new SystemClock(), builderPool, cacheabilityValidator, cacheKeySuffixProvider)
{
}
// Internal for testing
- internal ResponseCachingContext(HttpContext httpContext, IResponseCache cache, ISystemClock clock)
+ internal ResponseCachingContext(
+ HttpContext httpContext,
+ IResponseCache cache,
+ ISystemClock clock,
+ ObjectPool builderPool,
+ IResponseCachingCacheabilityValidator cacheabilityValidator,
+ IResponseCachingCacheKeySuffixProvider cacheKeySuffixProvider)
{
- if (cache == null)
- {
- throw new ArgumentNullException(nameof(cache));
- }
- if (httpContext == null)
- {
- throw new ArgumentNullException(nameof(httpContext));
- }
- if (clock == null)
- {
- throw new ArgumentNullException(nameof(clock));
- }
-
- HttpContext = httpContext;
- Cache = cache;
- Clock = clock;
+ _httpContext = httpContext;
+ _cache = cache;
+ _clock = clock;
+ _builderPool = builderPool;
+ _cacheabilityValidator = cacheabilityValidator;
+ _cacheKeySuffixProvider = cacheKeySuffixProvider;
}
internal bool CacheResponse
@@ -72,12 +82,6 @@ internal bool CacheResponse
internal bool ResponseStarted { get; set; }
- private ISystemClock Clock { get; }
-
- private HttpContext HttpContext { get; }
-
- private IResponseCache Cache { get; }
-
private Stream OriginalResponseStream { get; set; }
private ResponseCacheStream ResponseCacheStream { get; set; }
@@ -90,7 +94,7 @@ private RequestHeaders RequestHeaders
{
if (_requestHeaders == null)
{
- _requestHeaders = HttpContext.Request.GetTypedHeaders();
+ _requestHeaders = _httpContext.Request.GetTypedHeaders();
}
return _requestHeaders;
}
@@ -102,7 +106,7 @@ private ResponseHeaders ResponseHeaders
{
if (_responseHeaders == null)
{
- _responseHeaders = HttpContext.Response.GetTypedHeaders();
+ _responseHeaders = _httpContext.Response.GetTypedHeaders();
}
return _responseHeaders;
}
@@ -141,53 +145,75 @@ internal string CreateCacheKey()
internal string CreateCacheKey(CachedVaryBy varyBy)
{
- var request = HttpContext.Request;
- var builder = new StringBuilder()
- .Append(request.Method.ToUpperInvariant())
- .Append(";")
- .Append(request.Path.Value.ToUpperInvariant())
- .Append(CreateVaryByCacheKey(varyBy));
-
- return builder.ToString();
- }
+ var request = _httpContext.Request;
+ var builder = _builderPool?.Get() ?? new StringBuilder();
- private string CreateVaryByCacheKey(CachedVaryBy varyBy)
- {
- // TODO: resolve key format and delimiters
- if (varyBy == null || varyBy.Headers.Count == 0)
+ try
{
- return string.Empty;
- }
+ builder
+ .Append(request.Method.ToUpperInvariant())
+ .Append(";")
+ .Append(request.Path.Value.ToUpperInvariant());
- var builder = new StringBuilder(";");
+ if (varyBy?.Headers.Count > 0)
+ {
+ // TODO: resolve key format and delimiters
+ foreach (var header in varyBy.Headers)
+ {
+ // TODO: Normalization of order, case?
+ var value = _httpContext.Request.Headers[header];
- foreach (var header in varyBy.Headers)
- {
- // TODO: Normalization of order, case?
- var value = HttpContext.Request.Headers[header].ToString();
+ // TODO: How to handle null/empty string?
+ if (StringValues.IsNullOrEmpty(value))
+ {
+ value = "null";
+ }
+
+ builder.Append(";")
+ .Append(header)
+ .Append("=")
+ .Append(value);
+ }
+ }
+ // TODO: Parse querystring params
- // TODO: How to handle null/empty string?
- if (string.IsNullOrEmpty(value))
+ // Append custom cache key segment
+ var customKey = _cacheKeySuffixProvider.CreateCustomKeySuffix(_httpContext);
+ if (!string.IsNullOrEmpty(customKey))
{
- value = "null";
+ builder.Append(";")
+ .Append(customKey);
}
- builder.Append(header)
- .Append("=")
- .Append(value)
- .Append(";");
+ return builder.ToString();
+ }
+ finally
+ {
+ if (_builderPool != null)
+ {
+ _builderPool.Return(builder);
+ }
}
-
- // Parse querystring params
-
- return builder.ToString();
}
internal bool RequestIsCacheable()
{
+ // Use optional override if specified by user
+ switch(_cacheabilityValidator.RequestIsCacheableOverride(_httpContext))
+ {
+ case OverrideResult.UseDefaultLogic:
+ break;
+ case OverrideResult.DoNotCache:
+ return false;
+ case OverrideResult.Cache:
+ return true;
+ default:
+ throw new NotSupportedException($"Unrecognized result from {nameof(_cacheabilityValidator.RequestIsCacheableOverride)}.");
+ }
+
// Verify the method
// TODO: RFC lists POST as a cacheable method when explicit freshness information is provided, but this is not widely implemented. Will revisit.
- var request = HttpContext.Request;
+ var request = _httpContext.Request;
if (string.Equals("GET", request.Method, StringComparison.OrdinalIgnoreCase))
{
_responseType = ResponseType.FullReponse;
@@ -236,6 +262,19 @@ internal bool RequestIsCacheable()
internal bool ResponseIsCacheable()
{
+ // Use optional override if specified by user
+ switch (_cacheabilityValidator.ResponseIsCacheableOverride(_httpContext))
+ {
+ case OverrideResult.UseDefaultLogic:
+ break;
+ case OverrideResult.DoNotCache:
+ return false;
+ case OverrideResult.Cache:
+ return true;
+ default:
+ throw new NotSupportedException($"Unrecognized result from {nameof(_cacheabilityValidator.ResponseIsCacheableOverride)}.");
+ }
+
// Only cache pages explicitly marked with public
// TODO: Consider caching responses that are not marked as public but otherwise cacheable?
if (!ResponseCacheControl.Public)
@@ -256,7 +295,7 @@ internal bool ResponseIsCacheable()
return false;
}
- var response = HttpContext.Response;
+ var response = _httpContext.Response;
// Do not cache responses varying by *
if (string.Equals(response.Headers[HeaderNames.Vary], "*", StringComparison.OrdinalIgnoreCase))
@@ -334,14 +373,14 @@ internal bool EntryIsFresh(ResponseHeaders responseHeaders, TimeSpan age, bool v
internal async Task TryServeFromCacheAsync()
{
_cacheKey = CreateCacheKey();
- var cacheEntry = Cache.Get(_cacheKey);
+ var cacheEntry = _cache.Get(_cacheKey);
var responseServed = false;
if (cacheEntry is CachedVaryBy)
{
// Request contains VaryBy rules, recompute key and try again
_cacheKey = CreateCacheKey(cacheEntry as CachedVaryBy);
- cacheEntry = Cache.Get(_cacheKey);
+ cacheEntry = _cache.Get(_cacheKey);
}
if (cacheEntry is CachedResponse)
@@ -349,13 +388,13 @@ internal async Task TryServeFromCacheAsync()
var cachedResponse = cacheEntry as CachedResponse;
var cachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers);
- _responseTime = Clock.UtcNow;
+ _responseTime = _clock.UtcNow;
var age = _responseTime - cachedResponse.Created;
age = age > TimeSpan.Zero ? age : TimeSpan.Zero;
if (EntryIsFresh(cachedResponseHeaders, age, verifyAgainstRequest: true))
{
- var response = HttpContext.Response;
+ var response = _httpContext.Response;
// Copy the cached status code and response headers
response.StatusCode = cachedResponse.StatusCode;
foreach (var header in cachedResponse.Headers)
@@ -400,7 +439,7 @@ internal async Task TryServeFromCacheAsync()
if (!responseServed && RequestCacheControl.OnlyIfCached)
{
- HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
+ _httpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
responseServed = true;
}
@@ -413,7 +452,7 @@ internal void FinalizeCachingHeaders()
if (CacheResponse)
{
// Create the cache entry now
- var response = HttpContext.Response;
+ var response = _httpContext.Response;
var varyHeaderValue = response.Headers[HeaderNames.Vary];
_cachedResponseValidFor = ResponseCacheControl.SharedMaxAge
?? ResponseCacheControl.MaxAge
@@ -432,7 +471,7 @@ internal void FinalizeCachingHeaders()
};
// TODO: Overwrite?
- Cache.Set(_cacheKey, cachedVaryBy, _cachedResponseValidFor);
+ _cache.Set(_cacheKey, cachedVaryBy, _cachedResponseValidFor);
_cacheKey = CreateCacheKey(cachedVaryBy);
}
@@ -446,7 +485,7 @@ internal void FinalizeCachingHeaders()
_cachedResponse = new CachedResponse
{
Created = ResponseHeaders.Date.Value,
- StatusCode = HttpContext.Response.StatusCode
+ StatusCode = _httpContext.Response.StatusCode
};
foreach (var header in ResponseHeaders.Headers)
@@ -470,7 +509,7 @@ internal void FinalizeCachingBody()
{
_cachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray();
- Cache.Set(_cacheKey, _cachedResponse, _cachedResponseValidFor);
+ _cache.Set(_cacheKey, _cachedResponse, _cachedResponseValidFor);
}
}
@@ -479,7 +518,7 @@ internal void OnResponseStarting()
if (!ResponseStarted)
{
ResponseStarted = true;
- _responseTime = Clock.UtcNow;
+ _responseTime = _clock.UtcNow;
FinalizeCachingHeaders();
}
@@ -490,25 +529,25 @@ internal void ShimResponseStream()
// TODO: Consider caching large responses on disk and serving them from there.
// Shim response stream
- OriginalResponseStream = HttpContext.Response.Body;
+ OriginalResponseStream = _httpContext.Response.Body;
ResponseCacheStream = new ResponseCacheStream(OriginalResponseStream);
- HttpContext.Response.Body = ResponseCacheStream;
+ _httpContext.Response.Body = ResponseCacheStream;
// Shim IHttpSendFileFeature
- OriginalSendFileFeature = HttpContext.Features.Get();
+ OriginalSendFileFeature = _httpContext.Features.Get();
if (OriginalSendFileFeature != null)
{
- HttpContext.Features.Set(new SendFileFeatureWrapper(OriginalSendFileFeature, ResponseCacheStream));
+ _httpContext.Features.Set(new SendFileFeatureWrapper(OriginalSendFileFeature, ResponseCacheStream));
}
}
internal void UnshimResponseStream()
{
// Unshim response stream
- HttpContext.Response.Body = OriginalResponseStream;
+ _httpContext.Response.Body = OriginalResponseStream;
// Unshim IHttpSendFileFeature
- HttpContext.Features.Set(OriginalSendFileFeature);
+ _httpContext.Features.Set(OriginalSendFileFeature);
}
private enum ResponseType
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs
index 60817a6..76b81db 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs
@@ -1,7 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
using Microsoft.AspNetCore.ResponseCaching;
+using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Builder
{
@@ -9,6 +11,11 @@ public static class ResponseCachingExtensions
{
public static IApplicationBuilder UseResponseCaching(this IApplicationBuilder app)
{
+ if (app == null)
+ {
+ throw new ArgumentNullException(nameof(app));
+ }
+
return app.UseMiddleware();
}
}
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs
index 5c43ff1..9c6a922 100644
--- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs
+++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs
@@ -2,13 +2,14 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.ObjectPool;
+using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.ResponseCaching
{
- // http://tools.ietf.org/html/rfc7234
public class ResponseCachingMiddleware
{
private static readonly Func