The Ocelot ETag Caching library adds support for ETag caching to the Ocelot API Gateway.
ETag caching is a mechanism that allows a client to cache data and the next time the same data is requested,
the client can verify that the data is still up to date. If the data is still current, the server will return a status
of 304 Not Modified
and the client will use the cached data. If the data is not up-to-date,
the server returns the data and the client caches it.
The idea is that the server adds two important headers to the response:
ETag
- data identifier (randomly generated value)cache-control
- identifier that the data can be cached. Contains only the valueprivate
(⚠️ beware it must not bepublic
, because then the data can remain cached anywhere.) It also does not containmax-age
because in this case the client would not verify the data on the server (for a given amount of time. Occasionally this may be OK).
If the client (browser) finds these two headers in the response, it adds the If-None-Match
header with the ETag
value in the next request to the server with the same path and the server knows if the data is still up to date
based on this value. If they are up to date it returns a status of 304 Not Modified
and the client uses the data from the cache.
👌 The server does not send the data. This saves resources and bandwidth
On the client, this works automatically because this behavior is defined in the HTTP specification (no need to do anything).
sequenceDiagram
participant Browser
participant Server
Browser->>Server: Request
Server-->>Browser: Response with ETag and Cache-Control headers
note right of Server: Caches ETag with request info
Browser->>Server: Request with ETag header
alt ETag unchanged
Server-->>Browser: 304 Not Modified
else ETag changed
Server-->>Browser: Response with new ETag and data
note right of Server: Generates new ETag
end
The implementation is based on Ocelot middleware. All caching will be done in this middleware and nothing will be needed on the service side. The data itself is not cached, but only its identifier (ETag), based on which the data is verified to be up-to-date.
We use IOutputCacheStore
to store ETags and invalidate them.
{
"Routes": [
{
"Key": "getAllProducts",
"DownstreamPathTemplate": "/api/producsts/",
"UpstreamPathTemplate": "/products/",
"CachePolicy": "getAllProducts",
...
},
{
"Key": "getProduct",
"DownstreamPathTemplate": "/api/producsts/{id}",
"UpstreamPathTemplate": "/products/{id}",
"CachePolicy": "getProduct",
...
},
{
"Key": "deleteProduct",
"DownstreamPathTemplate": "/api/producsts/{id}",
"UpstreamPathTemplate": "/products/{id}",
"InvalidateCachePolicy": "invalidateProductCachePolicy"
...
}
]
}
builder.Services.AddOcelotETagCaching((c) =>
{
// 👇 Add ETag caching policies
// 👇 Simple policy with Expire and tag templates
c.AddPolicy("getAllProducts", p =>
{
p.Expire(TimeSpan.FromMinutes(5));
p.TagTemplates("products:{tenantId}", "all", "tenantAll:{tenantId}");
});
// 👇 Policy with custom cache key, etag generator and custom cache control
c.AddPolicy("getProduct", p =>
{
p.Expire(TimeSpan.FromMinutes(5));
p.TagTemplates("product:{tenantId}:{id}", "tenant:{tenantId}:all", "all");
p.CacheKey(context => context.Request.Headers.GetValues("id").FirstOrDefault()); // 👈 Custom cache key
p.ETag(context => new($"\"{Guid.NewGuid()}\"")); // 👈 Custom etag
p.CacheControl(new() { Public = false }); // 👈 Custom cache control
p.StatusCode(222); // 👈 Custom status code
p.AddPolicy<MyCustompolicy>(); // 👈 Custom policy
});
}
);
...
app.UseOcelot(c =>
{
// 👇 Add etag caching middleware
c.AddETagCaching();
}).Wait();
app.Run();
Tag templates are used to create tag for cache entry. It is used to invalidate cache entries. Tag is created by replacing placeholders with values from request route parameters.
For example, for route /api/{tenantId}/products/{id}
and tag template product:{tenantId}:{id}
the tag will be product:1:2
.
You can invalidate cache entries by tags defined in tag templates.
{
"Key": "deleteProduct",
"UpstreamHttpMethod": [ "Delete" ],
"DownstreamPathTemplate": "/api/producsts/{id}",
"UpstreamPathTemplate": "/products/{id}",
"InvalidateCachePolicy": "invalidateProductCachePolicy", // 👈 Invalidate cache policy
}
builder.Services.AddOcelotETagCaching(conf =>
{
//...
// define cache policy
//...
// 👇 Add invalidate cache policy
conf.AddInvalidatePolicy("invalidateProductCachePolicy", builder =>
{
builder.TagTemplates("product:{tenantId}", "product:{tenantId}:{id}");
});
});
public class ProductsService {
private readonly IOutputCacheStore _outputCacheStore;
public ProductsService(IOutputCacheStore outputCacheStore)
{
_outputCacheStore = outputCacheStore;
}
public async Task DeleteProduct(int tenantId, int id)
{
// 👇 Invalidate cache by tags
await _outputCacheStore.InvalidateAsync($"product:{tenantId}", $"product:{tenantId}:{id}");
// ...
}
}
By default is used InMemoryCacheStore
but you can use Redis
as well.
builder.Services.AddStackExchangeRedisOutputCache(options =>
{
options.Configuration =
builder.Configuration.GetConnectionString("MyRedisConStr");
options.InstanceName = "SampleInstance";
});