Created
September 30, 2016 17:51
-
-
Save psantiago/808bf6fa0ab141dab9015fd851d1420f to your computer and use it in GitHub Desktop.
fancy api caching action filter (for etags and max age)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <summary> | |
/// Enables caching by setting cache-control to private (instead of no-cache). | |
/// By default also enables etags, and sets cache-control max-age to 60 seconds. | |
/// </summary> | |
public class EnableApiCaching : ActionFilterAttribute | |
{ | |
private readonly TimeSpan _maxAge; | |
private readonly bool _enableEntityTagCaching; | |
/// <summary> | |
/// Enables caching by setting cache-control to private (instead of no-cache). | |
/// By default also enables etags, and sets cache-control max-age to 60 seconds. | |
/// </summary> | |
/// <param name="enableEntityTagCaching">A boolean indicating whether or not to enable entity tag based caching.</param> | |
/// <param name="maxAgeInSeconds"> | |
/// The maximum age in seconds that the cached response is valid. | |
/// NOTE: this will be overridden by a request's x-requested-max-age header if provided. | |
/// </param> | |
public EnableApiCaching(bool enableEntityTagCaching = true, int maxAgeInSeconds = 60) | |
{ | |
_enableEntityTagCaching = enableEntityTagCaching; | |
_maxAge = new TimeSpan(0, 0, maxAgeInSeconds); | |
} | |
public override void OnActionExecuted(HttpActionExecutedContext context) | |
{ | |
if (context.Request.Method.Method.ToUpperInvariant() != "GET" || context.Response == null) return; | |
//we need public or private (not no-cache) for etags to work. | |
//We also tell it not to bother the server with a real request for maxAgeInSeconds. | |
var ageToUse = _maxAge; | |
if (context.Request.Headers.Contains("X-Requested-Max-Cache-Age")) | |
{ | |
int requestedMaxCacheAge; | |
if (int.TryParse(context.Request.Headers.GetValues("X-Requested-Max-Cache-Age").FirstOrDefault(), out requestedMaxCacheAge)) | |
{ | |
ageToUse = new TimeSpan(0, 0, requestedMaxCacheAge); | |
} | |
} | |
context.Response.Headers.CacheControl = new CacheControlHeaderValue { Private = true, MaxAge = ageToUse }; | |
//while competent HTTP caches such as the ones used in chrome and firefox will update cache entries when receiving 304 (Not Modified) | |
//internet explorer/System.Net.Cache/WinInet won't (even though it spits out logs like it actually is). | |
//Until this is resolved, HttpClient has have significantly worse performance after the initially cached response expires | |
//compared to just using the cache control headers, as the cache is then never refreshed. | |
//this wasted a ton of my time trying to figure out. | |
//see http://stackoverflow.com/q/20925934/957829 | |
//if you're confident your api won't be consumed by the http client, you can probably safely use http client. | |
//we use this fancy header to indicate not to send back etags, since httpclient can't handle these properly, but must other clients can. | |
if (context.Request.Headers.Contains("X-Requested-No-ETag")) return; | |
if (!_enableEntityTagCaching) return; | |
if (context.Response.Content == null) return; | |
var byteArrayTask = context.Response.Content.ReadAsByteArrayAsync(); | |
byteArrayTask.ConfigureAwait(false); | |
byteArrayTask.Wait(); | |
var etag = BitConverter.ToString(MD5.Create().ComputeHash(byteArrayTask.Result)).Replace("-", ""); | |
context.Response.Headers.ETag = new EntityTagHeaderValue("\"" + etag + "\""); | |
if (context.Request.Headers.IfNoneMatch.Any() && | |
context.Request.Headers.IfNoneMatch.First().Equals(context.Response.Headers.ETag)) | |
{ | |
context.Response.StatusCode = HttpStatusCode.NotModified; | |
context.Response.Content = null; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment