I'm using API Management with an external Azure Cache for Redis to cache responses that reach out to external services. I need to provide a mechanism for invalidating cached responses by refetching data from the server - in turn updating the cache with the new responses.
My current policy (shown below) receives up-to-date data from the server when the cached response expires or a new request with a Cache-Control: must-revalidate
header is received. Again, when the header is received, I'd like to update the cached response with the new response. Am I missing anything either conceptually or in my policy?
<policies>
<inbound>
<base />
<set-backend-service id="apim-generated-policy" backend-id="func-myapp-dev-001" />
<set-variable name="mustRevalidate" value="@(context.Request.Headers.GetValueOrDefault("Cache-Control","").Contains("must-revalidate"))" />
<choose>
<when condition="@(context.Variables.GetValueOrDefault<bool>("mustRevalidate") == false)">
<cache-lookup vary-by-developer="false" vary-by-developer-groups="false" allow-private-response-caching="true" />
</when>
</choose>
</inbound>
<backend>
<base />
</backend>
<outbound>
<base />
<set-header name="Cached-At" exists-action="override">
<value>@(System.DateTime.Now.ToString())</value>
</set-header>
<cache-store duration="360" />
</outbound>
<on-error>
<base />
</on-error>
</policies>
The problem is that <cache-store>
cannot live without <cache-lookup>
. <cache-lookup>
not only gets cached value from the cache but it is also kind of configuration for <cache-store>
. And you can see that for yourself in the trace. When you call your API with Control-Cache: must-revalidate
header, <cache-lookup>
won't be triggered because of your <when>
policy but also <cache-store>
won't be triggered, unfortunately. In the logs you will see message:
cache-store (0.019 ms) "Corresponding cache-lookup policy was not applied. Skipping cache-store."
So there you have it. Skipping <cache-lookup>
will get you fresh response from the backend but won't cache it afterwards for future requests.
As for how to achieve what you want.
I've checked it twice but unfortunately <cache-lookup>
does not have option to skip lookup based on parameter.
Also was thinking about different approach. I think this could work with use of <cache-lookup-value>
policies. At the very beginning, based on your must-revalidate
parameter if it is set to true you could remove your cached value. And then proceed normally. The hardest part would be to think of the mechanism that could generate the cache key based on the request. The same thing APIM is doing in cache-store
. The key must be unique between different request but the same for the same request - exactly like caching works. The pattern that APIM applies for the cache keys, is something like this (you can check in the trace):
"cacheKey": "{apimName}.{apiId};{revision};{backendurl with all query parameters}"
example from my API:
"cacheKey": "my-apim.azure-api.net.1_weatherbit-io;rev=1.2432_get-current-city_id-city_id-key-key_4_https_api.weatherbit.io_443_/v2.0/current?city_id=4487042"
Once you have some smart generation of keys I think you are good to go, the final solution could look something like this:
<policies>
<inbound>
<set-variable name="cacheKey" value="@{
return ""; // here implement some smart key generation based on the request parameters from context.
}" />
<set-variable name="mustRevalidate" value="@(context.Request.Headers.GetValueOrDefault("Cache-Control","").Contains("must-revalidate"))" />
<choose>
<when condition="@(context.Variables.GetValueOrDefault<bool>("mustRevalidate") == true)">
<cache-remove-value key="@(context.Variables.GetValueOrDefault<string>("cacheKey"))" />
</when>
</choose>
<cache-lookup-value key="@(context.Variables.GetValueOrDefault<string>("cacheKey"))" variable-name="cachedResponse" />
<choose>
<when condition="@(context.Variables.GetValueOrDefault<JObject>("cachedResponse") != (JObject)null)">
<return-response>
<set-header name="Content-Type" exists-action="override">
<value>application/json; charset=utf-8</value>
</set-header>
<set-body>@{
return context.Variables.GetValueOrDefault<JObject>("cachedResponse").ToString();
}</set-body>
</return-response>
</when>
</choose>
<base />
</inbound>
<backend>
<base />
</backend>
<outbound>
<cache-store-value key="@(context.Variables.GetValueOrDefault<string>("cacheKey"))" value="@(context.Response.Body.As<JObject>(preserveContent: true))" duration="30" />
<base />
</outbound>
<on-error>
<base />
</on-error>
</policies>
Of course this caches only the response, if you want to cache headers as well you would have to implement mechanism of how to store it, and then when its time to return it inside <return-response>
, how to parse it to headers and body. I hope you can see what is the downside of this solution, you need to implement everything yourself. You no longer can use the built-in functionality of <cache-lookup>
and <cache-store>
. But at the same time you have more freedom and can implement what you want. Anyway, good luck.
PS. <cache-store-value>
policy has caching-type
attribute. You can use it to change cache to external.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With