Skip to content

Commit f1583b0

Browse files
Merge pull request #19 from ThaDaVos/cache-control
Better control of caching logic by following headers and a manual option to turn of caching for a request
2 parents 09457ae + 24c175e commit f1583b0

File tree

3 files changed

+189
-94
lines changed

3 files changed

+189
-94
lines changed

HttpClient.Caching/InMemory/InMemoryCacheHandler.cs

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Net;
44
using System.Net.Http;
5+
using System.Net.Http.Headers;
56
using System.Threading;
67
using System.Threading.Tasks;
78
using Microsoft.Extensions.Caching.Abstractions;
@@ -14,7 +15,27 @@ namespace Microsoft.Extensions.Caching.InMemory
1415
/// </summary>
1516
public class InMemoryCacheHandler : DelegatingHandler
1617
{
17-
private static HashSet<HttpMethod> CachedHttpMethods = new HashSet<HttpMethod>
18+
#if NET5_0_OR_GREATER
19+
/// <summary>
20+
/// The key to use to store the UseCache value in the HttpRequestMessage.Options dictionary.
21+
/// This key is used to determine if the cache should be checked for the request.
22+
/// If the key is not present, the cache will be checked.
23+
/// If the key is present and the value is false, the cache will not be checked.
24+
/// If the key is present and the value is true, the cache will be checked.
25+
/// </summary>
26+
public static readonly HttpRequestOptionsKey<bool> UseCache = new HttpRequestOptionsKey<bool>(nameof(UseCache));
27+
#else
28+
/// <summary>
29+
/// The key to use to store the UseCache value in the HttpRequestMessage.Properties dictionary.
30+
/// This key is used to determine if the cache should be checked for the request.
31+
/// If the key is not present, the cache will be checked.
32+
/// If the key is present and the value is false, the cache will not be checked.
33+
/// If the key is present and the value is true, the cache will be checked.
34+
/// </summary>
35+
public const string UseCache = nameof(UseCache);
36+
#endif
37+
38+
private static readonly HashSet<HttpMethod> CachedHttpMethods = new HashSet<HttpMethod>
1839
{
1940
HttpMethod.Get,
2041
HttpMethod.Head
@@ -50,10 +71,10 @@ public InMemoryCacheHandler(HttpMessageHandler innerHandler = null,
5071
IStatsProvider statsProvider = null,
5172
ICacheKeysProvider cacheKeysProvider = null)
5273
: this(innerHandler,
53-
cacheExpirationPerHttpResponseCode,
54-
statsProvider,
55-
new MemoryCache(new MemoryCacheOptions()),
56-
cacheKeysProvider)
74+
cacheExpirationPerHttpResponseCode,
75+
statsProvider,
76+
new MemoryCache(new MemoryCacheOptions()),
77+
cacheKeysProvider)
5778
{
5879
}
5980

@@ -104,6 +125,40 @@ public void InvalidateCache(Uri uri, HttpMethod httpMethod = null)
104125
}
105126
}
106127

128+
/// <summary>
129+
/// Determines if the cache should be checked for the request.
130+
/// </summary>
131+
/// <param name="request"></param>
132+
/// <returns>A bool representing if the cache should be cached or not</returns>
133+
private static bool ShouldTheCacheBeChecked(HttpRequestMessage request)
134+
{
135+
#if NET5_0_OR_GREATER
136+
var useCacheOption = request.Options.TryGetValue(UseCache, out var useCache) == false || useCache == true;
137+
#else
138+
var useCacheOption = request.Properties.TryGetValue(UseCache, out var useCache) == false || (bool)useCache == true;
139+
#endif
140+
141+
return useCacheOption && request.Headers.CacheControl?.NoCache != true;
142+
}
143+
144+
/// <summary>
145+
/// Determines if the response should be cached.
146+
/// </summary>
147+
/// <param name="response"></param>
148+
/// <returns>A bool representing if the response should be cached or not</returns>
149+
private static bool ShouldCacheResponse(HttpResponseMessage response)
150+
{
151+
if (response.Headers.CacheControl is CacheControlHeaderValue cacheControl)
152+
{
153+
return
154+
cacheControl.NoStore == false &&
155+
cacheControl.NoCache == false &&
156+
response.StatusCode != HttpStatusCode.NotModified;
157+
}
158+
159+
return response.StatusCode != HttpStatusCode.NotModified;
160+
}
161+
107162
/// <summary>
108163
/// Tries to get the value from the cache, and only calls the delegating handler on cache misses.
109164
/// </summary>
@@ -114,7 +169,10 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
114169

115170
// Gets the data from cache, and returns the data if it's a cache hit
116171
var isCachedHttpMethod = CachedHttpMethods.Contains(request.Method);
117-
if (isCachedHttpMethod)
172+
173+
// Check if the cache should be checked
174+
var shouldCheckCache = ShouldTheCacheBeChecked(request);
175+
if (shouldCheckCache && isCachedHttpMethod)
118176
{
119177
key = this.CacheKeysProvider.GetKey(request);
120178

@@ -131,13 +189,17 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
131189
if (isCachedHttpMethod)
132190
{
133191
var absoluteExpirationRelativeToNow = response.StatusCode.GetAbsoluteExpirationRelativeToNow(this.cacheExpirationPerHttpResponseCode);
192+
var maxAgeHeader = response.Headers.CacheControl?.MaxAge ?? TimeSpan.MaxValue;
193+
194+
// If the server sends a Cache-Control header with a max-age that is less than the configured expiration time, use the max-age value
195+
var maxCacheTime = (maxAgeHeader < absoluteExpirationRelativeToNow) ? maxAgeHeader : absoluteExpirationRelativeToNow;
134196

135197
this.StatsProvider.ReportCacheMiss(response.StatusCode);
136198

137-
if (TimeSpan.Zero != absoluteExpirationRelativeToNow)
199+
if (ShouldCacheResponse(response) && TimeSpan.Zero != maxCacheTime)
138200
{
139201
var entry = await response.ToCacheEntryAsync();
140-
await this.responseCache.TrySetAsync(key, entry, absoluteExpirationRelativeToNow);
202+
await this.responseCache.TrySetAsync(key, entry, maxCacheTime);
141203
return request.PrepareCachedEntry(entry);
142204
}
143205
}
@@ -153,7 +215,10 @@ protected override HttpResponseMessage Send(HttpRequestMessage request, Cancella
153215

154216
// Gets the data from cache, and returns the data if it's a cache hit
155217
var isCachedHttpMethod = CachedHttpMethods.Contains(request.Method);
156-
if (isCachedHttpMethod)
218+
219+
// Check if the cache should be checked
220+
var shouldCheckCache = ShouldTheCacheBeChecked(request);
221+
if (shouldCheckCache && isCachedHttpMethod)
157222
{
158223
key = this.CacheKeysProvider.GetKey(request);
159224

@@ -169,13 +234,17 @@ protected override HttpResponseMessage Send(HttpRequestMessage request, Cancella
169234
if (isCachedHttpMethod)
170235
{
171236
var absoluteExpirationRelativeToNow = response.StatusCode.GetAbsoluteExpirationRelativeToNow(this.cacheExpirationPerHttpResponseCode);
237+
var maxAgeHeader = response.Headers.CacheControl?.MaxAge ?? TimeSpan.MaxValue;
238+
239+
// If the server sends a Cache-Control header with a max-age that is less than the configured expiration time, use the max-age value
240+
var maxCacheTime = (maxAgeHeader < absoluteExpirationRelativeToNow) ? maxAgeHeader : absoluteExpirationRelativeToNow;
172241

173242
this.StatsProvider.ReportCacheMiss(response.StatusCode);
174243

175-
if (TimeSpan.Zero != absoluteExpirationRelativeToNow)
244+
if (ShouldCacheResponse(response) && TimeSpan.Zero != maxCacheTime)
176245
{
177246
var cacheData = response.ToCacheEntry();
178-
this.responseCache.TrySetCacheData(key, cacheData, absoluteExpirationRelativeToNow);
247+
this.responseCache.TrySetCacheData(key, cacheData, maxCacheTime);
179248
return request.PrepareCachedEntry(cacheData);
180249
}
181250
}

0 commit comments

Comments
 (0)