Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implement HTTP Cache (ETag) in ASP.NET Core Web API

I am working on ASP.NET Core (ASP.NET 5) Web API application and have to implement HTTP Caching with the help of Entity Tags. Earlier I used CacheCow for the same but it seems it does not support ASP.NET Core as of now. I also didn't find any other relevant library or framework support details for the same.

I can write custom code for the same but before that I want to see if anything is already available. Kindly share if something is already available and what is the better way to implement that.

like image 282
Brij Avatar asked Feb 17 '16 13:02

Brij


People also ask

How is ETag implemented?

Step by step: we first create and retrieve a Resource – and store the ETag value for further use. then we update the same Resource. send a new GET request, this time with the “If-None-Match” header specifying the ETag that we previously stored.

What is ETag in caching?

The ETag (or entity tag) HTTP response header is an identifier for a specific version of a resource. It lets caches be more efficient and save bandwidth, as a web server does not need to resend a full response if the content was not changed.

What is Iactionresult in Web API?

The IActionResult return type is appropriate when multiple ActionResult return types are possible in an action. The ActionResult types represent various HTTP status codes. Any non-abstract class deriving from ActionResult qualifies as a valid return type.


2 Answers

After a while trying to make it work with middleware I figured out that MVC action filters are actually better suited for this functionality.

public class ETagFilter : Attribute, IActionFilter {     private readonly int[] _statusCodes;      public ETagFilter(params int[] statusCodes)     {         _statusCodes = statusCodes;         if (statusCodes.Length == 0) _statusCodes = new[] { 200 };     }      public void OnActionExecuting(ActionExecutingContext context)     {     }      public void OnActionExecuted(ActionExecutedContext context)     {         if (context.HttpContext.Request.Method == "GET")         {             if (_statusCodes.Contains(context.HttpContext.Response.StatusCode))             {                 //I just serialize the result to JSON, could do something less costly                 var content = JsonConvert.SerializeObject(context.Result);                  var etag = ETagGenerator.GetETag(context.HttpContext.Request.Path.ToString(), Encoding.UTF8.GetBytes(content));                  if (context.HttpContext.Request.Headers.Keys.Contains("If-None-Match") && context.HttpContext.Request.Headers["If-None-Match"].ToString() == etag)                 {                     context.Result = new StatusCodeResult(304);                 }                 context.HttpContext.Response.Headers.Add("ETag", new[] { etag });             }         }     }         }  // Helper class that generates the etag from a key (route) and content (response) public static class ETagGenerator {     public static string GetETag(string key, byte[] contentBytes)     {         var keyBytes = Encoding.UTF8.GetBytes(key);         var combinedBytes = Combine(keyBytes, contentBytes);          return GenerateETag(combinedBytes);     }      private static string GenerateETag(byte[] data)     {         using (var md5 = MD5.Create())         {             var hash = md5.ComputeHash(data);             string hex = BitConverter.ToString(hash);             return hex.Replace("-", "");         }                 }      private static byte[] Combine(byte[] a, byte[] b)     {         byte[] c = new byte[a.Length + b.Length];         Buffer.BlockCopy(a, 0, c, 0, a.Length);         Buffer.BlockCopy(b, 0, c, a.Length, b.Length);         return c;     } } 

And then use it on the actions or controllers you want as an attribute:

[HttpGet("data")] [ETagFilter(200)] public async Task<IActionResult> GetDataFromApi() { } 

The important distinction between Middleware and Filters is that your middleware can run before and after MVC middlware and can only work with HttpContext. Also once MVC starts sending the response back to the client it's too late to make any changes to it.

Filters on the other hand are a part of MVC middleware. They have access to the MVC context, with which in this case it's simpler to implement this functionality. More on Filters and their pipeline in MVC.

like image 96
erikbozic Avatar answered Sep 29 '22 20:09

erikbozic


Building on Eric's answer, I would use an interface that could be implemented on an entity to support entity tagging. In the filter you would only add the ETag if the action is returning a entity with this interface.

This allows you to be more selective about what entities get tagged and allows you have each entity control how its tag is generated. This would be much more efficient than serializing everything and creating a hash. It also eliminates the need to check the status code. It could be safely and easily added as a global filter since you are "opting-in" to the functionality by implementing the interface on your model class.

public interface IGenerateETag {     string GenerateETag(); }  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] public class ETagFilterAttribute : Attribute, IActionFilter {     public void OnActionExecuting(ActionExecutingContext context)     {     }      public void OnActionExecuted(ActionExecutedContext context)     {         var request = context.HttpContext.Request;         var response = context.HttpContext.Response;          if (request.Method == "GET" &&             context.Result is ObjectResult obj &&             obj.Value is IGenerateETag entity)         {             string etag = entity.GenerateETag();              // Value should be in quotes according to the spec             if (!etag.EndsWith("\""))                 etag = "\"" + etag +"\"";              string ifNoneMatch = request.Headers["If-None-Match"];              if (ifNoneMatch == etag)             {                 context.Result = new StatusCodeResult(304);             }              context.HttpContext.Response.Headers.Add("ETag", etag);         }     } } 
like image 27
John Rutherford Avatar answered Sep 29 '22 19:09

John Rutherford