Skip to content

Commit

Permalink
Cache by header value: a new Header property in (File)CacheOptions co…
Browse files Browse the repository at this point in the history
…nfiguration of a route (#1172)

@EngRajabi, Mohsen Rajabi (7):
      add header to file cache option
      fix private set
      fix
      <none>
      <none>
      fix build fail
      fix: fix review comment. add unit test for change

@raman-m, Raman Maksimchuk (1):
      Update caching.rst

@raman-m (7):
      Fix errors
      Fix errors
      Fix styling warnings
      Refactor tests
      Add Delimiter
      Refactor generator
      Add unit tests
  • Loading branch information
EngRajabi authored Oct 19, 2023
1 parent 3b776a7 commit e92b103
Show file tree
Hide file tree
Showing 12 changed files with 187 additions and 54 deletions.
50 changes: 30 additions & 20 deletions docs/features/caching.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,56 @@ Caching

Ocelot supports some very rudimentary caching at the moment provider by the `CacheManager <https://github.com/MichaCo/CacheManager>`_ project. This is an amazing project that is solving a lot of caching problems. I would recommend using this package to cache with Ocelot.

The following example shows how to add CacheManager to Ocelot so that you can do output caching.
The following example shows how to add **CacheManager** to Ocelot so that you can do output caching.

First of all add the following NuGet package.
First of all add the following `NuGet package <https://www.nuget.org/packages/Ocelot.Cache.CacheManager>`_:

``Install-Package Ocelot.Cache.CacheManager``
.. code-block:: powershell
Install-Package Ocelot.Cache.CacheManager
This will give you access to the Ocelot cache manager extension methods.

The second thing you need to do something like the following to your ConfigureServices..
The second thing you need to do something like the following to your ``ConfigureServices`` method:

.. code-block:: csharp
using Ocelot.Cache.CacheManager;
s.AddOcelot()
.AddCacheManager(x =>
{
x.WithDictionaryHandle();
})
ConfigureServices(services =>
{
services.AddOcelot()
.AddCacheManager(x => x.WithDictionaryHandle());
});
Finally in order to use caching on a route in your Route configuration add this setting.
Finally, in order to use caching on a route in your Route configuration add this setting:

.. code-block:: json
"FileCacheOptions": { "TtlSeconds": 15, "Region": "somename" }
"FileCacheOptions": { "TtlSeconds": 15, "Region": "europe-central", "Header": "Authorization" }
In this example **TtlSeconds** is set to 15 which means the cache will expire after 15 seconds.
The **Region** represents a region of caching.

In this example ttl seconds is set to 15 which means the cache will expire after 15 seconds.
Additionally, if a header name is defined in the **Header** property, that header value is looked up by the key (header name) in the ``HttpRequest`` headers,
and if the header is found, its value will be included in caching key. This causes the cache to become invalid due to the header value changing.

If you look at the example `here <https://github.com/ThreeMammals/Ocelot/blob/main/test/Ocelot.ManualTest/Program.cs>`_ you can see how the cache manager is setup and then passed into the Ocelot AddCacheManager configuration method. You can use any settings supported by the CacheManager package and just pass them in.
If you look at the example `here <https://github.com/ThreeMammals/Ocelot/blob/main/test/Ocelot.ManualTest/Program.cs>`_ you can see how the cache manager is setup and then passed into the Ocelot ``AddCacheManager`` configuration method.
You can use any settings supported by the **CacheManager** package and just pass them in.

Anyway Ocelot currently supports caching on the URL of the downstream service and setting a TTL in seconds to expire the cache. You can also clear the cache for a region by calling Ocelot's administration API.
Anyway, Ocelot currently supports caching on the URL of the downstream service and setting a TTL in seconds to expire the cache. You can also clear the cache for a region by calling Ocelot's administration API.

Your own caching
^^^^^^^^^^^^^^^^
Your Own Caching
----------------

If you want to add your own caching method implement the following interfaces and register them in DI e.g. ``services.AddSingleton<IOcelotCache<CachedResponse>, MyCache>()``
If you want to add your own caching method, implement the following interfaces and register them in DI e.g.

``IOcelotCache<CachedResponse>`` this is for output caching.
.. code-block:: csharp
``IOcelotCache<FileConfiguration>`` this is for caching the file configuration if you are calling something remote to get your config such as Consul.
services.AddSingleton<IOcelotCache<CachedResponse>, MyCache>();
Please dig into the Ocelot source code to find more. I would really appreciate it if anyone wants to implement Redis, memcache etc..
* ``IOcelotCache<CachedResponse>`` this is for output caching.
* ``IOcelotCache<FileConfiguration>`` this is for caching the file configuration if you are calling something remote to get your config such as Consul.

Please dig into the Ocelot source code to find more.
We would really appreciate it if anyone wants to implement `Redis <https://redis.io/>`_, `Memcached <http://www.memcached.org/>`_ etc.
39 changes: 31 additions & 8 deletions src/Ocelot/Cache/CacheKeyGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,43 @@
using Ocelot.Request.Middleware;
using Ocelot.Configuration;
using Ocelot.Request.Middleware;

namespace Ocelot.Cache
{
public class CacheKeyGenerator : ICacheKeyGenerator
{
public string GenerateRequestCacheKey(DownstreamRequest downstreamRequest)
private const char Delimiter = '-';

public async ValueTask<string> GenerateRequestCacheKey(DownstreamRequest downstreamRequest, DownstreamRoute downstreamRoute)
{
var downStreamUrlKeyBuilder = new StringBuilder($"{downstreamRequest.Method}-{downstreamRequest.OriginalString}");
if (downstreamRequest.Content != null)
var builder = new StringBuilder()
.Append(downstreamRequest.Method)
.Append(Delimiter)
.Append(downstreamRequest.OriginalString);

var cacheOptionsHeader = downstreamRoute?.CacheOptions?.Header;
if (!string.IsNullOrEmpty(cacheOptionsHeader))
{
var header = downstreamRequest.Headers
.FirstOrDefault(r => r.Key.Equals(cacheOptionsHeader, StringComparison.OrdinalIgnoreCase))
.Value?.FirstOrDefault();

if (!string.IsNullOrEmpty(header))
{
builder.Append(Delimiter)
.Append(header);
}
}

if (!downstreamRequest.HasContent)
{
var requestContentString = Task.Run(async () => await downstreamRequest.Content.ReadAsStringAsync()).Result;
downStreamUrlKeyBuilder.Append(requestContentString);
return MD5Helper.GenerateMd5(builder.ToString());
}

var hashedContent = MD5Helper.GenerateMd5(downStreamUrlKeyBuilder.ToString());
return hashedContent;
var requestContentString = await downstreamRequest.ReadContentAsync();
builder.Append(Delimiter)
.Append(requestContentString);

return MD5Helper.GenerateMd5(builder.ToString());
}
}
}
5 changes: 3 additions & 2 deletions src/Ocelot/Cache/ICacheKeyGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
using Ocelot.Request.Middleware;
using Ocelot.Configuration;
using Ocelot.Request.Middleware;

namespace Ocelot.Cache
{
public interface ICacheKeyGenerator
{
string GenerateRequestCacheKey(DownstreamRequest downstreamRequest);
ValueTask<string> GenerateRequestCacheKey(DownstreamRequest downstreamRequest, DownstreamRoute downstreamRoute);
}
}
2 changes: 1 addition & 1 deletion src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public async Task Invoke(HttpContext httpContext)

var downstreamRequest = httpContext.Items.DownstreamRequest();
var downstreamUrlKey = $"{downstreamRequest.Method}-{downstreamRequest.OriginalString}";
var downStreamRequestCacheKey = _cacheGenerator.GenerateRequestCacheKey(downstreamRequest);
var downStreamRequestCacheKey = await _cacheGenerator.GenerateRequestCacheKey(downstreamRequest, downstreamRoute);

Logger.LogDebug($"Started checking cache for the '{downstreamUrlKey}' key.");

Expand Down
9 changes: 6 additions & 3 deletions src/Ocelot/Configuration/CacheOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@
{
public class CacheOptions
{
public CacheOptions(int ttlSeconds, string region)
public CacheOptions(int ttlSeconds, string region, string header)
{
TtlSeconds = ttlSeconds;
Region = region;
Region = region;
Header = header;
}

public int TtlSeconds { get; }

public string Region { get; }
public string Region { get; }

public string Header { get; }
}
}
2 changes: 1 addition & 1 deletion src/Ocelot/Configuration/Creator/RoutesCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf
.WithClaimsToDownstreamPath(claimsToDownstreamPath)
.WithRequestIdKey(requestIdKey)
.WithIsCached(fileRouteOptions.IsCached)
.WithCacheOptions(new CacheOptions(fileRoute.FileCacheOptions.TtlSeconds, region))
.WithCacheOptions(new CacheOptions(fileRoute.FileCacheOptions.TtlSeconds, region, fileRoute.FileCacheOptions.Header))
.WithDownstreamScheme(fileRoute.DownstreamScheme)
.WithLoadBalancerOptions(lbOptions)
.WithDownstreamAddresses(downstreamAddresses)
Expand Down
1 change: 1 addition & 0 deletions src/Ocelot/Configuration/File/FileCacheOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ public class FileCacheOptions
{
public int TtlSeconds { get; set; }
public string Region { get; set; }
public string Header { get; set; }
}
}
15 changes: 10 additions & 5 deletions src/Ocelot/Request/Middleware/DownstreamRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ public class DownstreamRequest
{
private readonly HttpRequestMessage _request;

public DownstreamRequest() { }

public DownstreamRequest(HttpRequestMessage request)
{
_request = request;
Expand All @@ -17,14 +19,13 @@ public DownstreamRequest(HttpRequestMessage request)
Headers = _request.Headers;
AbsolutePath = _request.RequestUri.AbsolutePath;
Query = _request.RequestUri.Query;
Content = _request.Content;
}

public HttpRequestHeaders Headers { get; }
public virtual HttpHeaders Headers { get; }

public string Method { get; }
public virtual string Method { get; }

public string OriginalString { get; }
public virtual string OriginalString { get; }

public string Scheme { get; set; }

Expand All @@ -36,7 +37,11 @@ public DownstreamRequest(HttpRequestMessage request)

public string Query { get; set; }

public HttpContent Content { get; set; }
public virtual bool HasContent { get => _request?.Content != null; }

public virtual Task<string> ReadContentAsync() => HasContent
? _request.Content.ReadAsStringAsync()
: Task.FromResult(string.Empty);

public HttpRequestMessage ToHttpRequestMessage()
{
Expand Down
4 changes: 2 additions & 2 deletions src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,14 @@ private void SetOcelotRequestId(HttpContext httpContext)
}
}

private static bool ShouldAddRequestId(RequestId requestId, HttpRequestHeaders headers)
private static bool ShouldAddRequestId(RequestId requestId, HttpHeaders headers)
{
return !string.IsNullOrEmpty(requestId?.RequestIdKey)
&& !string.IsNullOrEmpty(requestId.RequestIdValue)
&& !RequestIdInHeaders(requestId, headers);
}

private static bool RequestIdInHeaders(RequestId requestId, HttpRequestHeaders headers)
private static bool RequestIdInHeaders(RequestId requestId, HttpHeaders headers)
{
return headers.TryGetValues(requestId.RequestIdKey, out var value);
}
Expand Down
108 changes: 99 additions & 9 deletions test/Ocelot.UnitTests/Cache/CacheKeyGeneratorTests.cs
Original file line number Diff line number Diff line change
@@ -1,32 +1,122 @@
using Ocelot.Cache;
using Ocelot.Configuration;
using Ocelot.Configuration.Builder;
using Ocelot.Request.Middleware;
using System.Net.Http.Headers;

namespace Ocelot.UnitTests.Cache
{
public class CacheKeyGeneratorTests
{
private readonly ICacheKeyGenerator _cacheKeyGenerator;
private readonly DownstreamRequest _downstreamRequest;
private readonly Mock<DownstreamRequest> _downstreamRequest;

private const string verb = "GET";
private const string url = "https://some.url/blah?abcd=123";
private const string header = nameof(CacheKeyGeneratorTests);
private const string headerName = "auth";

public CacheKeyGeneratorTests()
{
_cacheKeyGenerator = new CacheKeyGenerator();
_cacheKeyGenerator = new CacheKeyGenerator();
_downstreamRequest = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123"));

_downstreamRequest = new Mock<DownstreamRequest>();
_downstreamRequest.SetupGet(x => x.Method).Returns(verb);
_downstreamRequest.SetupGet(x => x.OriginalString).Returns(url);

var headers = new HttpHeadersStub
{
{ headerName, header },
};
_downstreamRequest.SetupGet(x => x.Headers).Returns(headers);
}

[Fact]
public void should_generate_cache_key_with_request_content()
{
const string content = nameof(should_generate_cache_key_with_request_content);

_downstreamRequest.SetupGet(x => x.HasContent).Returns(true);
_downstreamRequest.Setup(x => x.ReadContentAsync()).ReturnsAsync(content);

var cachekey = MD5Helper.GenerateMd5($"{verb}-{url}-{content}");

this.Given(x => x.GivenDownstreamRoute(null))
.When(x => x.WhenGenerateRequestCacheKey())
.Then(x => x.ThenGeneratedCacheKeyIs(cachekey))
.BDDfy();
}

[Fact]
public void should_generate_cache_key_without_request_content()
{
_downstreamRequest.SetupGet(x => x.HasContent).Returns(false);

CacheOptions options = null;
var cachekey = MD5Helper.GenerateMd5($"{verb}-{url}");

this.Given(x => x.GivenDownstreamRoute(options))
.When(x => x.WhenGenerateRequestCacheKey())
.Then(x => x.ThenGeneratedCacheKeyIs(cachekey))
.BDDfy();
}

[Fact]
public void should_generate_cache_key_with_cache_options_header()
{
_downstreamRequest.SetupGet(x => x.HasContent).Returns(false);

CacheOptions options = new CacheOptions(100, "region", headerName);
var cachekey = MD5Helper.GenerateMd5($"{verb}-{url}-{header}");

this.Given(x => x.GivenDownstreamRoute(options))
.When(x => x.WhenGenerateRequestCacheKey())
.Then(x => x.ThenGeneratedCacheKeyIs(cachekey))
.BDDfy();
}

[Fact]
public void should_generate_cache_key_from_context()
public void should_generate_cache_key_happy_path()
{
this.Given(x => x.GivenCacheKeyFromContext(_downstreamRequest))
const string content = nameof(should_generate_cache_key_happy_path);

_downstreamRequest.SetupGet(x => x.HasContent).Returns(true);
_downstreamRequest.Setup(x => x.ReadContentAsync()).ReturnsAsync(content);

CacheOptions options = new CacheOptions(100, "region", headerName);
var cachekey = MD5Helper.GenerateMd5($"{verb}-{url}-{header}-{content}");

this.Given(x => x.GivenDownstreamRoute(options))
.When(x => x.WhenGenerateRequestCacheKey())
.Then(x => x.ThenGeneratedCacheKeyIs(cachekey))
.BDDfy();
}

private void GivenCacheKeyFromContext(DownstreamRequest downstreamRequest)
private DownstreamRoute _downstreamRoute;

private void GivenDownstreamRoute(CacheOptions options)
{
_downstreamRoute = new DownstreamRouteBuilder()
.WithKey("key1")
.WithCacheOptions(options)
.Build();
}

private string _generatedCacheKey;

private async Task WhenGenerateRequestCacheKey()
{
_generatedCacheKey = await _cacheKeyGenerator.GenerateRequestCacheKey(_downstreamRequest.Object, _downstreamRoute);
}

private void ThenGeneratedCacheKeyIs(string expected)
{
var generatedCacheKey = _cacheKeyGenerator.GenerateRequestCacheKey(downstreamRequest);
var cachekey = MD5Helper.GenerateMd5("GET-https://some.url/blah?abcd=123");
generatedCacheKey.ShouldBe(cachekey);
_generatedCacheKey.ShouldBe(expected);
}
}

internal class HttpHeadersStub : HttpHeaders
{
public HttpHeadersStub() : base() { }
}
}
2 changes: 1 addition & 1 deletion test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ private void GivenTheDownstreamRouteIs()
var route = new RouteBuilder()
.WithDownstreamRoute(new DownstreamRouteBuilder()
.WithIsCached(true)
.WithCacheOptions(new CacheOptions(100, "kanken"))
.WithCacheOptions(new CacheOptions(100, "kanken", null))
.WithUpstreamHttpMethod(new List<string> { "Get" })
.Build())
.WithUpstreamHttpMethod(new List<string> { "Get" })
Expand Down
Loading

0 comments on commit e92b103

Please sign in to comment.