Applying Caching to your API – Part 2

In my previous post I talk about the general overview of caching with Microservices and their Client. In this post I explore how the Api, the owner of the data, communicates to the Client if the data can be cached and how it can be cached.

Pushing Caching onto the Client

Even though you are planning to have fast caching in your api often it is beneficial for the client to also cache the data, reducing the amount of trips over the wire and the serialization cost that occurs.

There are two mechanisms for this

Response Caching Headers

This method is defined by the HTTP 1.1 Caching Specification and it is essentially returning information back to the client on how to cache the data.
The primary header is the Cache-Control and can be accompanied with additional information such as Age, Expires, Pragma and Vary.

There is a good description on it by Mozilla

How to use it in your ASP.NET Core Api

Using these headers I am able to tell the client if they are allowed to cache the data and how.

This is a good start but the problem I have with this (and it is common in all cache implementations) is data is ok to stay in the cache for a certain period of time. Whether the cache sits on the server or the client there is often an expiry time associated with it and this is only good if we know when the data maybe updated, for example by a scheduled mechanism.

I find that I am often building applications that they are event driven and the time data changes vary. We may also have reference data that could change several times by a user and then may not be touched for a week.

ETags

The Entity Tag is used to provide a version of the cached query. It tends to be a string and is returned in the response headers when the resources is requested.

Future requests by the client can then contain the etag with this version. The api will check this version and if there are no changes, return 304 Not Modified. However if there is a change then 200 ok is returned along with a new version and the payload.

There is no official implementation of this in Asp.net core and I am aware of CacheCow but I want to look at how I can implement it to give myself more flexibility and customization. Most implementations seem to involve putting an Attribute on the Controller method and using a generic Action Filter to provide a central method for generating the e-tag. If I have a large response I find the generating of this hash value for the e-tag a big overhead.

Implementing the Initial Response

First thing to do is create a new ActionResult which I can return from by Controller. I want it to behave like just like the OkObjectAction but add the additional header

public class OkObjectWithETagResult : OkObjectResult
{
    private readonly string _version;
    public OkObjectWithETagResult(object value, string version) : base(value)
    {
        _version = version;
    }
    public override void OnFormatting(ActionContext context)
    {
        base.OnFormatting(context);
        context.HttpContext.Response.Headers.Add("ETag", _version);
    }
}
Code language: C# (cs)

Then from my Controller, when I return the result I can add the Version.

[HttpGet("{id:int}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<TEntity>> GetById(int id, CancellationToken cancellationToken)
{
    if (id < 1) return BadRequest();
    var book = await _queryProcessor.ProcessAsync(new GetById<TEntity>(id), cancellationToken);
    if (book == null) return NotFound();
    return  new OkObjectWithETagResult(book, Guid.NewGuid().ToString());
}

Code language: C# (cs)

Here I am just generating a Guid but in practice I can generate the version from the object itself.

The advantage of doing it this way is I am reducing the time taken for the Api to generate the version. Other implementations will get the payload and create the version each time. Because it’s a common method implementations often convert the payload to Json and generate the version of that. If it’s a large payload there is still an overhead on generating that version. What I can do is add this version to my local cache and quickly look it up and compare it later.

Handling a Request with a Etag

Now I need to had a request with the ETag in the header. I am going to use Middleware for this and the IMemoryCache. When the request comes in I am going to check to see if the header exists and if the value is in the Cache. If it is then I immediately stop processing and return 304 Not Modified. If it doesn’t exist then I carry on processing and check the response on the way back. If the ETag header exists then I will put it in the Cache.

public class ETagMiddleware : IMiddleware
{
    private readonly IMemoryCache _cache;

    public ETagMiddleware(IMemoryCache cache)
    {
        _cache = cache;
    }

    const string ETAG_HEADER = "ETag";

    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        var key = new ETagCacheKey(context.Request);
       
        if (context.Request.Headers.TryGetValue(ETAG_HEADER, out var requestValue) &&
            _cache.TryGetValue(key, out var cacheValue) &&
            requestValue == cacheValue)
        {
            context.Response.StatusCode = 304;
            return;
        }

        await next(context);

        if (context.Response.Headers.TryGetValue(ETAG_HEADER, out var responseValue))
        {
            _cache.Set(key, responseValue);
        }
    }
}
Code language: C# (cs)

This now gives me the basics of ETag Caching allowing me to communicate with the client on the version of the data and reduces the amount of data being passed over the wire.


Next I will look at how to cache my data on the Api to reduce the amount of communication with the database.

Tags: