Last active
July 25, 2018 14:33
-
-
Save sandcastle/404d61a43abc1476821906159c8d697d to your computer and use it in GitHub Desktop.
Some helpers for dealing with caching in ASP.Net Core
This file contains hidden or 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> | |
| /// 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)); | |
| } | |
| } |
This file contains hidden or 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 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; | |
| } | |
| } |
This file contains hidden or 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 interface IAuditable : IReadOnlyAuditable | |
| { | |
| DateTime LastModified { get; set; } | |
| Guid LastModifiedBy { get; set; } | |
| } |
This file contains hidden or 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 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