Skip to content

Instantly share code, notes, and snippets.

@sandcastle
Last active July 25, 2018 14:33
Show Gist options
  • Select an option

  • Save sandcastle/404d61a43abc1476821906159c8d697d to your computer and use it in GitHub Desktop.

Select an option

Save sandcastle/404d61a43abc1476821906159c8d697d to your computer and use it in GitHub Desktop.
Some helpers for dealing with caching in ASP.Net Core
/// <summary>
/// More information on cache headers can be found here:
///
/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
///
/// The important ones:
///
/// no-store:
///
/// The cache should not store anything about the client request
/// or server response. A request is sent to the server and a
/// full response is downloaded each and every time.
///
/// no-cache:
///
/// A cache will send the request to the origin server for validation
/// before releasing a cached copy.
///
/// must-revalidate:
///
/// When using the "must-revalidate" directive, the cache must
/// verify the status of the stale resources before using it
/// and expired ones should not be used.
///
/// </summary>
public static class CacheExtensions
{
/// <summary>
/// Sets the response Cache-Control header to "no-cache, no-store, must-revalidate"
/// and the Expires header to "0" and the Pragma header to "no-cache" to ensure
/// that the response is not cached by any proxy or client.
/// </summary>
/// <param name="controller">The controller to apply this to.</param>
public static void SetCacheNoStore(this ControllerBase controller)
{
var responseHeaders = controller.Response.GetTypedHeaders();
// HTTP 1.1
responseHeaders.CacheControl = new CacheControlHeaderValue
{
MustRevalidate = true,
NoCache = true,
NoStore = true
};
// HTTP 1.0
controller.Response.Headers.Add("Pragma", "no-cache");
// Proxies
controller.Response.Headers.Add("Expires", "0");
}
/// <summary>
/// Sets the response Cache-Control header to "no-cache, must-revalidate" which forces
/// the client to consider the response to be stale, which forces subsequent requests
/// to issue a conditional request if there is a LastModified or ETag header present.
///
/// This should be used when you want to take advantage of conditional requests with
/// headers like LastModified and Etag, but don't want the response to be automatically
/// assumed to be fresh by default, by browser heuristics (see RFC 7234 section 4.2.2).
/// </summary>
/// <param name="controller">The controller to apply this to.</param>
public static void SetCacheStale(this ControllerBase controller)
{
var responseHeaders = controller.Response.GetTypedHeaders();
// HTTP 1.1
responseHeaders.CacheControl = new CacheControlHeaderValue
{
NoCache = true,
MustRevalidate = true
};
// HTTP 1.0
controller.Response.Headers.Add("Pragma", "no-cache");
}
/// <summary>
/// Sets the response Cache-Control header to "private, must-revalidate, max-age={maxAge}".
///
/// This should be used when the response can be cached by only the client (not proxies)
/// and you want to item to be re-validated after its expired.
/// </summary>
/// <param name="controller">The controller to apply this to.</param>
/// <param name="maxAge">The max-age before the response is stale.</param>
public static void SetCachePrivateAllowStale(this ControllerBase controller, TimeSpan maxAge)
{
var value = new CacheControlHeaderValue
{
Private = true,
MaxAge = maxAge,
MustRevalidate = true
};
var responseHeaders = controller.Response.GetTypedHeaders();
responseHeaders.CacheControl = value;
}
/// <summary>
/// Sets the response Cache-Control header to "private, must-revalidate, max-age={maxAge}".
///
/// This should be used when the response can be cached by only the client (not proxies)
/// and you want to item to be re-validated after its expired.
/// </summary>
/// <param name="controller">The controller to apply this to.</param>
/// <param name="maxAge">The max-age before the response is stale.</param>
public static void SetCachePrivateNoStale(this ControllerBase controller, TimeSpan maxAge)
{
var value = new CacheControlHeaderValue
{
Private = true,
MaxAge = maxAge,
MustRevalidate = true
};
var responseHeaders = controller.Response.GetTypedHeaders();
responseHeaders.CacheControl = value;
}
/// <summary>
/// Sets the response Cache-Control header to "public, must-revalidate, max-age={maxAge}".
///
/// This should be used when the response can be cached by both the client and also proxies
/// and you want to item to be re-validated after its expired.
/// </summary>
/// <param name="controller">The controller to apply this to.</param>
/// <param name="maxAge">The max-age before the response is stale.</param>
public static void SetCachePublic(this ControllerBase controller, TimeSpan maxAge)
{
var value = new CacheControlHeaderValue
{
Public = true,
MaxAge = maxAge
};
var responseHeaders = controller.Response.GetTypedHeaders();
responseHeaders.CacheControl = value;
}
/// <summary>
/// Sets the response Cache-Control header to "public, must-revalidate, max-age={maxAge}".
///
/// This should be used when the response can be cached by both the client and also proxies
/// and you want to item to be re-validated when stale.
/// </summary>
/// <param name="controller">The controller to apply this to.</param>
/// <param name="maxAge">The max-age before the response is stale.</param>
public static void SetCachePublicNoStale(this ControllerBase controller, TimeSpan maxAge)
{
var value = new CacheControlHeaderValue
{
Public = true,
MaxAge = maxAge,
MustRevalidate = true
};
var responseHeaders = controller.Response.GetTypedHeaders();
responseHeaders.CacheControl = value;
}
/// <summary>
/// Returns the specified audited item with LastModified header.
///
/// Important: If a browser sees the LastModified header without
/// a Cache-Control header, then it will use heuristics to determine
/// how long it should treat the item as fresh for before checking
/// if the version is stale. This can trip you up as it will not even
/// send back conditional headers to check if the response has changed
/// - almost like you had set a "public, max-age={XX}" header.
///
/// You should use one of the cache extension methods from this class
/// to ensure the appropriate Cache-Control header is set when using this
/// method.
///
/// The default rule that most browsers use is:
///
/// Cache for = (current time - last modified time) / 10%
///
/// See RFC 7234 Section 4 and Section 4.2.2 for more info:
///
/// https://tools.ietf.org/html/rfc7234#section-4.2
///
/// </summary>
/// <param name="controller">The current controller</param>
/// <param name="audited">The audited item to return.</param>
/// <returns>The action result.</returns>
public static IActionResult AsCached(this ControllerBase controller, IAuditable audited)
{
controller.Response.SetLastModified(audited);
return controller.Ok(audited);
}
/// <summary>
/// Checks if the object has been modified since last being requested.
/// </summary>
/// <param name="request">The current request.</param>
/// <param name="audited">The audited item to check.</param>
/// <returns>True if not modified, else false.</returns>
public static bool IsNotModified(this HttpRequest request, IAuditable audited)
{
return IsNotModified(request, audited.LastModified);
}
/// <summary>
/// Checks if the object has been modified since last being requested.
/// </summary>
/// <param name="request">The current request.</param>
/// <param name="lastModified">The audited item to check.</param>
/// <returns>True if not modified, else false.</returns>
public static bool IsNotModified(this HttpRequest request, DateTime lastModified)
{
var requestHeaders = request?.GetTypedHeaders();
// The LastModified header uses RFC 1123 (Sun, 15 Jun 2008 21:15:07 GMT)
// which does not include milliseconds, we need to ensure that we truncate
// the milliseconds off the lastModified value before comparing with the
// header otherwise most of the time they will not match when they should
return requestHeaders?.IfModifiedSince != null &&
requestHeaders.IfModifiedSince.Value.UtcDateTime >= lastModified.Truncate(TimeSpan.FromSeconds(1));
}
/// <summary>
/// Returns a not modified response, using both the ETag and Last Modified headers.
/// </summary>
/// <param name="response">The current response.</param>
/// <param name="audited">The audited item.</param>
/// <returns></returns>
public static void SetLastModified(this HttpResponse response, IAuditable audited)
{
SetLastModified(response, audited.LastModified);
}
/// <summary>
/// Returns a not modified response, using both the ETag and Last Modified headers.
/// </summary>
/// <param name="response">The current response.</param>
/// <param name="lastModified">The last modified date time.</param>
/// <returns></returns>
public static void SetLastModified(this HttpResponse response, DateTime lastModified)
{
response.Headers[HeaderNames.LastModified] = lastModified.ToString("R");
}
/// <summary>
/// Returns a not modified response, using both the ETag and Last Modified headers.
/// </summary>
/// <param name="audited">The audited item.</param>
/// <returns>The not modified result.</returns>
public static DateNotModifiedResult AsNotModified(IAuditable audited)
{
return AsNotModified(audited.LastModified);
}
/// <summary>
/// Returns a not modified response, using both the ETag and Last Modified headers.
/// </summary>
/// <param name="lastModified">The last modified date time.</param>
/// <returns>The not modified result.</returns>
static DateNotModifiedResult AsNotModified(DateTime lastModified)
{
return new DateNotModifiedResult(lastModified);
}
/// <summary>
/// Truncates the specified timespan off the date time.
///
/// This is useful when comparing RFC 1123 dates, by truncating the
/// milliseconds:
///
/// lastModified.Truncate(TimeSpan.FromSeconds(1))
///
/// </summary>
/// <param name="dateTime">The original date.</param>
/// <param name="timeSpan">The timespan to truncate.</param>
/// <returns></returns>
static DateTime Truncate(this DateTime dateTime, TimeSpan timeSpan)
{
return timeSpan == TimeSpan.Zero
? dateTime
: dateTime.AddTicks(-(dateTime.Ticks % timeSpan.Ticks));
}
}
public class DateNotModifiedResult : ActionResult
{
readonly DateTime _lastModified;
public DateNotModifiedResult(IAuditable auditable)
: this(auditable.LastModified) { }
public DateNotModifiedResult(DateTime lastModified)
{
_lastModified = lastModified;
}
[UsedImplicitly]
public override void ExecuteResult(ActionContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof (context));
}
context.HttpContext.Response.Headers[HeaderNames.LastModified] = _lastModified.ToString("R");
context.HttpContext.Response.Headers[HeaderNames.ContentLength] = "0";
context.HttpContext.Response.StatusCode = (int)HttpStatusCode.NotModified;
context.HttpContext.Response.ContentType = null;
context.HttpContext.Response.Body = null;
}
}
public interface IAuditable : IReadOnlyAuditable
{
DateTime LastModified { get; set; }
Guid LastModifiedBy { get; set; }
}
public interface IReadOnlyAuditable
{
DateTime Created { get; set; }
Guid CreatedBy { get; set; }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment