Kros.Ocelot.ETagCaching 1.2.0

dotnet add package Kros.Ocelot.ETagCaching --version 1.2.0                
NuGet\Install-Package Kros.Ocelot.ETagCaching -Version 1.2.0                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Kros.Ocelot.ETagCaching" Version="1.2.0" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Kros.Ocelot.ETagCaching --version 1.2.0                
#r "nuget: Kros.Ocelot.ETagCaching, 1.2.0"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Kros.Ocelot.ETagCaching as a Cake Addin
#addin nuget:?package=Kros.Ocelot.ETagCaching&version=1.2.0

// Install Kros.Ocelot.ETagCaching as a Cake Tool
#tool nuget:?package=Kros.Ocelot.ETagCaching&version=1.2.0                

Ocelot ETag Caching

Basic

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 value private (⚠️ beware it must not be public, because then the data can remain cached anywhere.) It also does not contain max-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).

Implementation

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.

Get started

Ocelot configuration

{
    "Routes": [
        {
            "Key": "getAllProducts",
            "DownstreamPathTemplate": "/api/producsts/",
            "UpstreamPathTemplate": "/products/",
            "CachePolicy": "getAllProduct",
            ...
        },
        {
            "Key": "getProduct",
            "DownstreamPathTemplate": "/api/producsts/{id}",
            "UpstreamPathTemplate": "/products/{id}",
            "CachePolicy": "getProduct",
            ...
        },
        {
            "Key": "deleteProduct",
            "DownstreamPathTemplate": "/api/producsts/{id}",
            "UpstreamPathTemplate": "/products/{id}",
            "InvalidateCachePolicy": "invalidateProductCachePolicy"
            ...
        }
    ]
}

Program.cs configuration

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

Tag tamplates are use 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.

Cache invalidation

You can invalidate cache entries by tags defined in tag templates.

Automatic by endpoints

{
    "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}");
    });
});

Manual

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}");
        // ...
    }
}

Redis

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";
});
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.2.0 1,344 6/11/2024
1.1.0 294 5/14/2024
1.0.0 120 4/18/2024
0.0.1 121 4/5/2024