Skip to content

Instantly share code, notes, and snippets.

@petevb
Forked from madskristensen/ETagMiddleware.cs
Last active July 21, 2020 09:40
Show Gist options
  • Save petevb/5de0bfb625d619ed3b1562ed3452c4c1 to your computer and use it in GitHub Desktop.
Save petevb/5de0bfb625d619ed3b1562ed3452c4c1 to your computer and use it in GitHub Desktop.
ASP.NET Core ETAg middleware
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Net.Http.Headers;
using System.IO;
using System.Security.Cryptography;
using System.Threading.Tasks;
public class ETagMiddleware
{
private readonly RequestDelegate _next;
public ETagMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var response = context.Response;
var originalStream = response.Body;
using (var ms = new MemoryStream())
{
response.Body = ms;
await _next(context);
if (IsEtagSupported(response))
{
string checksum = CalculateChecksum(ms);
response.Headers[HeaderNames.ETag] = checksum;
if (context.Request.Headers.TryGetValue(HeaderNames.IfNoneMatch, out var etag) && checksum == etag)
{
response.StatusCode = StatusCodes.Status304NotModified;
return;
}
}
ms.Position = 0;
await ms.CopyToAsync(originalStream);
}
}
private static bool IsEtagSupported(HttpResponse response)
{
if (response.StatusCode != StatusCodes.Status200OK)
return false;
// The 20kb length limit is not based in science. Feel free to change
if (response.Body.Length > 20 * 1024)
return false;
if (response.Headers.ContainsKey(HeaderNames.ETag))
return false;
return true;
}
private static string CalculateChecksum(MemoryStream ms)
{
string checksum = "";
using (var algo = SHA1.Create())
{
ms.Position = 0;
byte[] bytes = algo.ComputeHash(ms);
checksum = $"\"{WebEncoders.Base64UrlEncode(bytes)}\"";
}
return checksum;
}
}
public static class ApplicationBuilderExtensions
{
public static void UseETagger(this IApplicationBuilder app)
{
app.UseMiddleware<ETagMiddleware>();
}
}
/// <returns>
/// Enough settings to start the app.
/// </returns>
[HttpGet]
[AllowAnonymous]
[Route("bootstrap")]
[ResponseCache(CacheProfileName = "OneHourWithOverride")]
public IActionResult Bootstrap()
{
if (Request?.Headers != null && Request.Headers.ContainsKey("If-Modified-Since"))
{
var result = DateTime.TryParse(Request.Headers["If-Modified-Since"], out var when);
if (result)
{
// It's very rare for app settings to change; they're environment-specific.
// But, if you're testing you may want to override the default 3600 (in appSettings.Development.json).
var seconds = this.configuration.GetValue<int?>("OneHourWithOverrideCacheDuration") ?? 3600;
if (when.AddSeconds(seconds).CompareTo(DateTime.Now) > 0)
{
Response.StatusCode = 304;
Response.ContentLength = 0;
return Content(string.Empty);
}
}
}
...
#pragma warning disable CA1305 // Specify IFormatProvider: "O" is invariant
this.HttpContext.Response.Headers.Add("Last-Modified", DateTime.UtcNow.ToString("O"));
#pragma warning restore CA1305 // Specify IFormatProvider
return this.Ok(/* app settings */);
}
// Add "app.UseETagger();" to "Configure" method in Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseStaticFiles();
// Add this after static files but before MVC in order to provide ETags to MVC Views and Razor Pages.
app.UseETagger();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment