ASP.NET Core doesn’t add an ETag header automatically to HTTP responses from MVC action methods or Razor Pages. We have to implement that ourselves to provide the users with true Conditional GET support that honors the If-None-Match request header.

Wouldn’t it be nice if all we had to do was to register it using app.UseETagger() like this?

// Add "app.UseETagger();" to "Configure" method in Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    // 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?}"); }); }

We can if we use this little middleware class that handles the ETag and If-None-Match headers.


Comment by Muhammad Rehan Saeed

It's worth mentioning, that this code buffers the response in memory, so you'll have to take a performance hit. I'm not sure there is a way to avoid that with E-Tags.

One alternative method of caching that can avoid buffering is to use Last-Modified and If-Not-Modified-Since HTTP headers to check a timestamp of the resource you want to cache. You usually already know the last modified date for a file or database record, so it's a simple matter of returning that date.

Comment by Tomasz Pęczek

The above observation regarding response buffering is quite important, especially as from framework perspective the fact of buffering the response should be represented by implementation of IHttpBufferingFeature (for example like in Response Compression middleware - github.com/.../BodyWrapperStream.cs) in which case call to IHttpBufferingFeature.DisableResponseBuffering() should disable to whole thing. Of course this would complicate this middleware a little bit.

Comment by Brian Kuhn

The approach I have taken is to use an IAsyncResultFilter to generate the ETag and Last-Modified response headers. Resources returned by an API typically implement an IConcurrentResource interface that defines ConcurrencyToken and LastModifedOn properties. The result filter determines if the result is a IConcurrentResource; and if true uses the interface properties to set the headers. While this is not a perfectly 'generic' approach it avoids the cost of calculating a checksum and also allows for expose of the actual concurrency token (e.g. RowVersion in SQL or CAS in Couchbase).

Brian Kuhn