Skip to content
This repository was archived by the owner on Nov 22, 2018. It is now read-only.

Commit 763b45f

Browse files
committed
Add configurable options for ResponseCaching
Override RequestIsCacheable Override ResponseIsCacheable Append customized cache key
1 parent 4f61c65 commit 763b45f

11 files changed

+402
-83
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Http;
5+
6+
namespace Microsoft.AspNetCore.ResponseCaching
7+
{
8+
public interface IResponseCachingCacheKeySuffixProvider
9+
{
10+
/// <summary>
11+
/// Create a key segment that is appended to the default cache key.
12+
/// </summary>
13+
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
14+
/// <returns>The key segment that will be appended to the default cache key.</returns>
15+
string CreateCustomKeySuffix(HttpContext httpContext);
16+
}
17+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Http;
5+
6+
namespace Microsoft.AspNetCore.ResponseCaching
7+
{
8+
public interface IResponseCachingCacheabilityValidator
9+
{
10+
/// <summary>
11+
/// Override default behavior for determining cacheability of an HTTP request.
12+
/// </summary>
13+
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
14+
/// <returns>The <see cref="OverrideResult"/>.</returns>
15+
OverrideResult RequestIsCacheableOverride(HttpContext httpContext);
16+
17+
/// <summary>
18+
/// Override default behavior for determining cacheability of an HTTP response.
19+
/// </summary>
20+
/// <param name="httpContext">The <see cref="HttpContext"/>.</param>
21+
/// <returns>The <see cref="OverrideResult"/>.</returns>
22+
OverrideResult ResponseIsCacheableOverride(HttpContext httpContext);
23+
}
24+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Http;
5+
6+
namespace Microsoft.AspNetCore.ResponseCaching.Internal
7+
{
8+
public class NoopCacheKeySuffixProvider : IResponseCachingCacheKeySuffixProvider
9+
{
10+
public string CreateCustomKeySuffix(HttpContext httpContext) => null;
11+
}
12+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Http;
6+
7+
namespace Microsoft.AspNetCore.ResponseCaching.Internal
8+
{
9+
public class NoopCacheabilityValidator : IResponseCachingCacheabilityValidator
10+
{
11+
public OverrideResult RequestIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic;
12+
13+
public OverrideResult ResponseIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic;
14+
}
15+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.ResponseCaching
5+
{
6+
public enum OverrideResult
7+
{
8+
/// <summary>
9+
/// Use the default logic for determining cacheability.
10+
/// </summary>
11+
UseDefaultLogic,
12+
13+
/// <summary>
14+
/// Ignore default logic and do not cache.
15+
/// </summary>
16+
DoNotCache,
17+
18+
/// <summary>
19+
/// Ignore default logic and cache.
20+
/// </summary>
21+
Cache
22+
}
23+
}

src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs

Lines changed: 71 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -28,31 +28,29 @@ public class ResponseCachingContext
2828
private CachedResponse _cachedResponse;
2929
private TimeSpan _cachedResponseValidFor;
3030
internal DateTimeOffset _responseTime;
31-
32-
public ResponseCachingContext(HttpContext httpContext, IResponseCache cache)
33-
: this(httpContext, cache, new SystemClock())
31+
32+
public ResponseCachingContext(
33+
HttpContext httpContext,
34+
IResponseCache cache,
35+
IResponseCachingCacheabilityValidator cacheabilityValidator,
36+
IResponseCachingCacheKeySuffixProvider cacheKeySuffixProvider)
37+
: this(httpContext, cache, new SystemClock(), cacheabilityValidator, cacheKeySuffixProvider)
3438
{
3539
}
3640

3741
// Internal for testing
38-
internal ResponseCachingContext(HttpContext httpContext, IResponseCache cache, ISystemClock clock)
42+
internal ResponseCachingContext(
43+
HttpContext httpContext,
44+
IResponseCache cache,
45+
ISystemClock clock,
46+
IResponseCachingCacheabilityValidator cacheabilityValidator,
47+
IResponseCachingCacheKeySuffixProvider cacheKeySuffixProvider)
3948
{
40-
if (cache == null)
41-
{
42-
throw new ArgumentNullException(nameof(cache));
43-
}
44-
if (httpContext == null)
45-
{
46-
throw new ArgumentNullException(nameof(httpContext));
47-
}
48-
if (clock == null)
49-
{
50-
throw new ArgumentNullException(nameof(clock));
51-
}
52-
5349
HttpContext = httpContext;
5450
Cache = cache;
5551
Clock = clock;
52+
CacheabilityValidator = cacheabilityValidator;
53+
CacheKeySuffixProvider = cacheKeySuffixProvider;
5654
}
5755

5856
internal bool CacheResponse
@@ -72,12 +70,16 @@ internal bool CacheResponse
7270

7371
internal bool ResponseStarted { get; set; }
7472

75-
private ISystemClock Clock { get; }
76-
7773
private HttpContext HttpContext { get; }
7874

7975
private IResponseCache Cache { get; }
8076

77+
private ISystemClock Clock { get; }
78+
79+
private IResponseCachingCacheabilityValidator CacheabilityValidator { get; }
80+
81+
private IResponseCachingCacheKeySuffixProvider CacheKeySuffixProvider { get; }
82+
8183
private Stream OriginalResponseStream { get; set; }
8284

8385
private ResponseCacheStream ResponseCacheStream { get; set; }
@@ -145,46 +147,56 @@ internal string CreateCacheKey(CachedVaryBy varyBy)
145147
var builder = new StringBuilder()
146148
.Append(request.Method.ToUpperInvariant())
147149
.Append(";")
148-
.Append(request.Path.Value.ToUpperInvariant())
149-
.Append(CreateVaryByCacheKey(varyBy));
150+
.Append(request.Path.Value.ToUpperInvariant());
150151

151-
return builder.ToString();
152-
}
153-
154-
private string CreateVaryByCacheKey(CachedVaryBy varyBy)
155-
{
156-
// TODO: resolve key format and delimiters
157-
if (varyBy == null || varyBy.Headers.Count == 0)
152+
if (varyBy?.Headers.Count > 0)
158153
{
159-
return string.Empty;
160-
}
161-
162-
var builder = new StringBuilder(";");
154+
// TODO: resolve key format and delimiters
155+
foreach (var header in varyBy.Headers)
156+
{
157+
// TODO: Normalization of order, case?
158+
var value = HttpContext.Request.Headers[header];
163159

164-
foreach (var header in varyBy.Headers)
165-
{
166-
// TODO: Normalization of order, case?
167-
var value = HttpContext.Request.Headers[header].ToString();
160+
// TODO: How to handle null/empty string?
161+
if (StringValues.IsNullOrEmpty(value))
162+
{
163+
value = "null";
164+
}
168165

169-
// TODO: How to handle null/empty string?
170-
if (string.IsNullOrEmpty(value))
171-
{
172-
value = "null";
166+
builder.Append(";")
167+
.Append(header)
168+
.Append("=")
169+
.Append(value);
173170
}
174-
175-
builder.Append(header)
176-
.Append("=")
177-
.Append(value)
178-
.Append(";");
179171
}
172+
// TODO: Parse querystring params
180173

181-
// Parse querystring params
174+
// Append custom cache key segment
175+
var customKey = CacheKeySuffixProvider.CreateCustomKeySuffix(HttpContext);
176+
if (!string.IsNullOrEmpty(customKey))
177+
{
178+
builder.Append(";")
179+
.Append(customKey);
180+
}
182181

183182
return builder.ToString();
184183
}
185184

186185
internal bool RequestIsCacheable()
187186
{
187+
// Use optional override if specified by user
188+
switch(CacheabilityValidator.RequestIsCacheableOverride(HttpContext))
189+
{
190+
case OverrideResult.UseDefaultLogic:
191+
break;
192+
case OverrideResult.DoNotCache:
193+
return false;
194+
case OverrideResult.Cache:
195+
return true;
196+
default:
197+
throw new NotSupportedException($"Unrecognized result from {nameof(CacheabilityValidator.RequestIsCacheableOverride)}.");
198+
}
199+
188200
// Verify the method
189201
// TODO: RFC lists POST as a cacheable method when explicit freshness information is provided, but this is not widely implemented. Will revisit.
190202
var request = HttpContext.Request;
@@ -236,6 +248,19 @@ internal bool RequestIsCacheable()
236248

237249
internal bool ResponseIsCacheable()
238250
{
251+
// Use optional override if specified by user
252+
switch (CacheabilityValidator.ResponseIsCacheableOverride(HttpContext))
253+
{
254+
case OverrideResult.UseDefaultLogic:
255+
break;
256+
case OverrideResult.DoNotCache:
257+
return false;
258+
case OverrideResult.Cache:
259+
return true;
260+
default:
261+
throw new NotSupportedException($"Unrecognized result from {nameof(CacheabilityValidator.ResponseIsCacheableOverride)}.");
262+
}
263+
239264
// Only cache pages explicitly marked with public
240265
// TODO: Consider caching responses that are not marked as public but otherwise cacheable?
241266
if (!ResponseCacheControl.Public)

src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
45
using Microsoft.AspNetCore.ResponseCaching;
6+
using Microsoft.Extensions.Options;
57

68
namespace Microsoft.AspNetCore.Builder
79
{
810
public static class ResponseCachingExtensions
911
{
1012
public static IApplicationBuilder UseResponseCaching(this IApplicationBuilder app)
1113
{
14+
if (app == null)
15+
{
16+
throw new ArgumentNullException(nameof(app));
17+
}
18+
1219
return app.UseMiddleware<ResponseCachingMiddleware>();
1320
}
1421
}

src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@
44
using System;
55
using System.Threading.Tasks;
66
using Microsoft.AspNetCore.Http;
7-
using Microsoft.AspNetCore.Http.Features;
87

98
namespace Microsoft.AspNetCore.ResponseCaching
109
{
11-
// http://tools.ietf.org/html/rfc7234
1210
public class ResponseCachingMiddleware
1311
{
1412
private static readonly Func<object, Task> OnStartingCallback = state =>
@@ -19,26 +17,45 @@ public class ResponseCachingMiddleware
1917

2018
private readonly RequestDelegate _next;
2119
private readonly IResponseCache _cache;
20+
IResponseCachingCacheabilityValidator _cacheabilityValidator;
21+
IResponseCachingCacheKeySuffixProvider _cacheKeySuffixProvider;
2222

23-
public ResponseCachingMiddleware(RequestDelegate next, IResponseCache cache)
23+
public ResponseCachingMiddleware(
24+
RequestDelegate next,
25+
IResponseCache cache,
26+
IResponseCachingCacheabilityValidator cacheabilityValidator,
27+
IResponseCachingCacheKeySuffixProvider cacheKeySuffixProvider)
2428
{
2529
if (cache == null)
2630
{
2731
throw new ArgumentNullException(nameof(cache));
2832
}
29-
3033
if (next == null)
3134
{
3235
throw new ArgumentNullException(nameof(next));
3336
}
37+
if (cacheabilityValidator == null)
38+
{
39+
throw new ArgumentNullException(nameof(cacheabilityValidator));
40+
}
41+
if (cacheKeySuffixProvider == null)
42+
{
43+
throw new ArgumentNullException(nameof(cacheKeySuffixProvider));
44+
}
3445

3546
_next = next;
3647
_cache = cache;
48+
_cacheabilityValidator = cacheabilityValidator;
49+
_cacheKeySuffixProvider = cacheKeySuffixProvider;
3750
}
3851

3952
public async Task Invoke(HttpContext context)
4053
{
41-
var cachingContext = new ResponseCachingContext(context, _cache);
54+
var cachingContext = new ResponseCachingContext(
55+
context,
56+
_cache,
57+
_cacheabilityValidator,
58+
_cacheKeySuffixProvider);
4259

4360
// Should we attempt any caching logic?
4461
if (cachingContext.RequestIsCacheable())

src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServiceCollectionExtensions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public static IServiceCollection AddMemoryResponseCache(this IServiceCollection
1818
}
1919

2020
services.AddMemoryCache();
21+
services.AddResponseCachingServices();
2122
services.TryAdd(ServiceDescriptor.Singleton<IResponseCache, MemoryResponseCache>());
2223

2324
return services;
@@ -31,9 +32,18 @@ public static IServiceCollection AddDistributedResponseCache(this IServiceCollec
3132
}
3233

3334
services.AddDistributedMemoryCache();
35+
services.AddResponseCachingServices();
3436
services.TryAdd(ServiceDescriptor.Singleton<IResponseCache, DistributedResponseCache>());
3537

3638
return services;
3739
}
40+
41+
private static IServiceCollection AddResponseCachingServices(this IServiceCollection services)
42+
{
43+
services.TryAdd(ServiceDescriptor.Singleton<IResponseCachingCacheKeySuffixProvider, NoopCacheKeySuffixProvider>());
44+
services.TryAdd(ServiceDescriptor.Singleton<IResponseCachingCacheabilityValidator, NoopCacheabilityValidator>());
45+
46+
return services;
47+
}
3848
}
3949
}

0 commit comments

Comments
 (0)