Building a conditional caching middleware instead of an attribute in ASP.NET Core 3.1
Introduction
It was not that long ago that I had to manage the cache to get informations that rarely change. My goal was to avoid making HTTP hits to the server, so I had to eliminate the distributed cache and the memory cache. So I only had as solution the HTTP response cache by attributes because client side caching. But something bothered me, I wanted to have the possibility of benefiting from a greater flexibility in parameters management, for example by using dependency injection to inject parameters rather than using caching profiles. So I had the idea of building a caching middleware, conditional on the route used, in order to have the same behavior as an action or controller attribute. In this article I wanted to explain to you how I did it.
Caching scenarii
My scenari were like this:
- Expose an endpoint that takes optionally a query string parameter (a method that retrieves all my configuration + the possibility to filter it)
- Expose an endpoint that returns a configuration for a specified route filter.
If we take a look of all possible urls to manage, we have
- /configuration/all endpoint
- /configuration/all?siteId={siteId}
- /configuration/{siteId}
The third scenario is different from the first two, because the route parameter, there are as many urls as possible siteId while others are the same url but only the query parameter changes.
Building the caching middleware
Let’s start by configuring option values, in my demo I will need two options:
- Define caching duration
- Define caching location (in my case client side)
Here we go:
The value “Location” is set to “Client” because I want to cache it client only, then “Cache-Control” header will be set to “Private”, “Public” if I choose “Any” if I want to set it Client side or in any Proxy.
The caching middleware has to be able to manage query string parameters (siteId) so we need to implement IResponseCachingFeature. This feature is activated when you configure your application with services.AddResponseCaching() and app.UseResponseCaching() in your Startup.cs.
Here is the implementation of the middleware:
Note that how its easy to access to the configuration from options we defined juste before (ConfigurationCachingOptions).
Our caching middleware is ready to be used, but we need to define on what endpoint it has to be activated, because for now it’s activated on any endpoint in within the app and manage siteId parameter in every endpoints within the app as well.
According to Microsoft documentation here are conditions of caching:
- The request must result in a server response with a 200 (OK) status code.
- The request method must be GET or HEAD.
- In
Startup.Configure
, Response Caching Middleware must be placed before middleware that require caching. For more information, see ASP.NET Core Middleware. - The
Authorization
header must not be present. Cache-Control
header parameters must be valid, and the response must be markedpublic
and not markedprivate
.- The
Pragma: no-cache
header must not be present if theCache-Control
header isn’t present, as theCache-Control
header overrides thePragma
header when present. - The
Set-Cookie
header must not be present. Vary
header parameters must be valid and not equal to*
.- The
Content-Length
header value (if set) must match the size of the response body. - The IHttpSendFileFeature isn’t used.
- The response must not be stale as specified by the
Expires
header and themax-age
ands-maxage
cache directives. - Response buffering must be successful. The size of the response must be smaller than the configured or default SizeLimit. The body size of the response must be smaller than the configured or default MaximumBodySize.
- The response must be cacheable according to the RFC 7234 specifications. For example, the
no-store
directive must not exist in request or response header fields. See Section 3: Storing Responses in Caches of RFC 7234 for details.
Activate the caching middleware by condition
To use our middleware with a condition, we need to use the method .UseWhen() and our condition is the route name: configuration
Now we are able to activate our middleware on every endpoint that match the request path configuration.
These urls will now be cached independently from each others:
If we take a look of all possible urls to manage, we have
- /configuration/all
- /configuration/all?siteId=1
- /configuration/all?siteId=2
- /configuration/1
- /configuration/2
/configuration/1 and /configuration/2 are two differents Uri, so any conflict will appear, but all Uri that starts with /configuration/all may be in conflict if the parameter siteId is not well managed with IResponseCachingFeature and VaryByQueryKeys options:
var responseCachingFeature = context.Features.Get<IResponseCachingFeature>(); if (responseCachingFeature != null) { responseCachingFeature.VaryByQueryKeys = new[] { "siteId" }; }
Demo:
Conclusion
This demo showed you how to build a conditional Response caching middleware. If you prefer using attributes, it’s definitely not a bad option!
If you want to learn more about Response caching in a middleware you can go here Response caching in ASP.NET Core and here Response Caching Middleware in ASP.NET Core.
Hope you liked this article 😉 and if you have any comment to make, please make it I will be glad to read it :).