Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Using chaining to create cached results in ASP.NET Web API v2
public enum Cacheability
{
NoCache,
Private,
Public,
}
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;
}
}
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);
}
}
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));
}
}

Hey Brad,

I have a few of questions about the headers you are returning. I think I have a reasonable grasp on the theory of what the caching headers are supposed to look like but I'm wondering if there are some practical realities that invalidate the theory. Curious as to your experiences.

Why set an Expires header if you are also going to set no-cache? no-cache in a response means a cache must re-validate before returning a stored response, so why set the Expires header at all?

Pragma: no-cache is supposed to be a request header, what does it mean as a response header?

You specify Max-age and Expires, however if max-age is present, it is supposed to override Expires. Why send both?

You set the Date header, is that to try and make the Date more accurate because there may be delay before the host actually generates a Date header?

BTW, I think this might be one of the best uses of IHttpActionResult that I have seen. The chaining mechanism is very clever. Now we just need a similar mechanism for processing the inbound request.

Thanks.

Owner

bradwilson commented Jan 24, 2014

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).

Thanks, that makes sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment