Created
January 23, 2014 20:53
-
-
Save bradwilson/8586562 to your computer and use it in GitHub Desktop.
Using chaining to create cached results in ASP.NET Web API v2
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
public enum Cacheability | |
{ | |
NoCache, | |
Private, | |
Public, | |
} |
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
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Net.Http; | |
using System.Net.Http.Headers; | |
using System.Threading; | |
using System.Threading.Tasks; | |
using System.Web.Http; | |
public class CachedResult<T> : IHttpActionResult | |
where T : IHttpActionResult | |
{ | |
public CachedResult( | |
T innerResult, | |
Cacheability cacheability, | |
string eTag, | |
DateTimeOffset? expires, | |
DateTimeOffset? lastModified, | |
TimeSpan? maxAge, | |
bool? noStore) | |
{ | |
Cacheability = cacheability; | |
ETag = eTag; | |
Expires = expires; | |
InnerResult = innerResult; | |
LastModified = lastModified; | |
MaxAge = maxAge; | |
NoStore = noStore; | |
} | |
public Cacheability Cacheability { get; private set; } | |
public string ETag { get; private set; } | |
public DateTimeOffset? Expires { get; private set; } | |
public T InnerResult { get; private set; } | |
public DateTimeOffset? LastModified { get; private set; } | |
public TimeSpan? MaxAge { get; private set; } | |
public bool? NoStore { get; private set; } | |
public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken) | |
{ | |
var response = await InnerResult.ExecuteAsync(cancellationToken); | |
if (!response.Headers.Date.HasValue) | |
response.Headers.Date = DateTimeOffset.UtcNow; | |
response.Headers.CacheControl = new CacheControlHeaderValue | |
{ | |
NoCache = Cacheability == Cacheability.NoCache, | |
Private = Cacheability == Cacheability.Private, | |
Public = Cacheability == Cacheability.Public | |
}; | |
if (response.Headers.CacheControl.NoCache) | |
{ | |
response.Headers.Pragma.TryParseAdd("no-cache"); | |
response.Content.Headers.Expires = response.Headers.Date; | |
return response; // None of the other headers are valid | |
} | |
response.Content.Headers.Expires = Expires; | |
response.Content.Headers.LastModified = LastModified; | |
response.Headers.CacheControl.MaxAge = MaxAge; | |
if (!String.IsNullOrWhiteSpace(ETag)) | |
response.Headers.ETag = new EntityTagHeaderValue(String.Format("\"{0}\"", ETag)); | |
if (NoStore.HasValue) | |
response.Headers.CacheControl.NoStore = NoStore.Value; | |
return response; | |
} | |
} |
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
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Web.Http; | |
public static class HttpActionResultExtensions | |
{ | |
public static CachedResult<T> Cached<T>( | |
this T actionResult, | |
Cacheability cacheability = Cacheability.Private, | |
string eTag = null, | |
DateTimeOffset? expires = null, | |
DateTimeOffset? lastModified = null, | |
TimeSpan? maxAge = null, | |
bool? noStore = null) where T : IHttpActionResult | |
{ | |
return new CachedResult<T>(actionResult, cacheability, eTag, expires, lastModified, maxAge, noStore); | |
} | |
} |
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
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Web.Http; | |
public SampleController : ApiController | |
{ | |
public IHttpActionResult GetExample(string name) | |
{ | |
return Ok("Hello, " + name).Cached(Cacheability.Public, maxAge: TimeSpan.FromMinutes(15)); | |
} | |
} |
Thanks, that makes sense.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I was mostly trying to emulate the behavior that ASP.NET does when setting the comparable headers. For NoCache, it sets an illegal Expires header value ("-1"), so I followed the spec which says "if Date and Expires are the same value, then the data must be considered to be already expired." Since the Date header does not get set until much later, I just go ahead and set it so that I can have the equal values.
Pragma: no-cache is the same thing: it's implemented by ASP.NET, so I did so as well. The HTTP spec says that HTTP/1.0 clients likely don't implement Cache-Control, and while I'm pretty sure that all browsers will be 1.1, the proxies in-between are more questionable, so it seemed reasonable to continue the practice.
As for Expires vs. MaxAge, I don't necessarily expect people to set all the values, but rather the pick and choose one or two (you could argue that ETag and LastModified do the same thing, at the end of the day).