diff --git a/src/Ocelot.Cache.CacheManager/OcelotBuilderExtensions.cs b/src/Ocelot.Cache.CacheManager/OcelotBuilderExtensions.cs index d96ce919..e9841890 100644 --- a/src/Ocelot.Cache.CacheManager/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Cache.CacheManager/OcelotBuilderExtensions.cs @@ -33,6 +33,10 @@ builder.Services.RemoveAll(typeof(IOcelotCache)); builder.Services.AddSingleton>(fileConfigCacheManagerOutputCache); builder.Services.AddSingleton>(fileConfigCacheManager); + + builder.Services.RemoveAll(typeof(ICacheKeyGenerator)); + builder.Services.AddSingleton(); + return builder; } } diff --git a/src/Ocelot/Cache/CacheKeyGenerator.cs b/src/Ocelot/Cache/CacheKeyGenerator.cs new file mode 100644 index 00000000..c33c6104 --- /dev/null +++ b/src/Ocelot/Cache/CacheKeyGenerator.cs @@ -0,0 +1,19 @@ +using System.Text; +using System.Threading.Tasks; +using Ocelot.Middleware; + +namespace Ocelot.Cache { + public class CacheKeyGenerator : ICacheKeyGenerator { + public string GenerateRequestCacheKey(DownstreamContext context) { + string hashedContent = null; + StringBuilder downStreamUrlKeyBuilder = new StringBuilder($"{context.DownstreamRequest.Method}-{context.DownstreamRequest.OriginalString}"); + if (context.DownstreamRequest.Content != null) { + string requestContentString = Task.Run(async () => await context.DownstreamRequest.Content.ReadAsStringAsync()).Result; + downStreamUrlKeyBuilder.Append(requestContentString); + } + + hashedContent = MD5Helper.GenerateMd5(downStreamUrlKeyBuilder.ToString()); + return hashedContent; + } + } +} diff --git a/src/Ocelot/Cache/ICacheKeyGenerator.cs b/src/Ocelot/Cache/ICacheKeyGenerator.cs new file mode 100644 index 00000000..5a65eb8a --- /dev/null +++ b/src/Ocelot/Cache/ICacheKeyGenerator.cs @@ -0,0 +1,7 @@ +using Ocelot.Middleware; + +namespace Ocelot.Cache { + public interface ICacheKeyGenerator { + string GenerateRequestCacheKey(DownstreamContext context); + } +} diff --git a/src/Ocelot/Cache/MD5Helper.cs b/src/Ocelot/Cache/MD5Helper.cs new file mode 100644 index 00000000..d98bb8c5 --- /dev/null +++ b/src/Ocelot/Cache/MD5Helper.cs @@ -0,0 +1,22 @@ +using System.Security.Cryptography; +using System.Text; + +namespace Ocelot.Cache { + public static class MD5Helper { + public static string GenerateMd5(byte[] contentBytes) { + MD5 md5 = MD5.Create(); + byte[] hash = md5.ComputeHash(contentBytes); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < hash.Length; i++) { + sb.Append(hash[i].ToString("X2")); + } + + return sb.ToString(); + } + + public static string GenerateMd5(string contentString) { + byte[] contentBytes = Encoding.Unicode.GetBytes(contentString); + return GenerateMd5(contentBytes); + } + } +} diff --git a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs index 5b96e79c..925e90c6 100644 --- a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs +++ b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs @@ -7,19 +7,23 @@ using Ocelot.Logging; using Ocelot.Middleware; using System.IO; + using System.Text; public class OutputCacheMiddleware : OcelotMiddleware { private readonly OcelotRequestDelegate _next; private readonly IOcelotCache _outputCache; + private readonly ICacheKeyGenerator _cacheGeneratot; public OutputCacheMiddleware(OcelotRequestDelegate next, IOcelotLoggerFactory loggerFactory, - IOcelotCache outputCache) + IOcelotCache outputCache, + ICacheKeyGenerator cacheGeneratot) :base(loggerFactory.CreateLogger()) { _next = next; _outputCache = outputCache; + _cacheGeneratot = cacheGeneratot; } public async Task Invoke(DownstreamContext context) @@ -31,10 +35,11 @@ } var downstreamUrlKey = $"{context.DownstreamRequest.Method}-{context.DownstreamRequest.OriginalString}"; + string downStreamRequestCacheKey = _cacheGeneratot.GenerateRequestCacheKey(context); Logger.LogDebug($"Started checking cache for {downstreamUrlKey}"); - var cached = _outputCache.Get(downstreamUrlKey, context.DownstreamReRoute.CacheOptions.Region); + var cached = _outputCache.Get(downStreamRequestCacheKey, context.DownstreamReRoute.CacheOptions.Region); if (cached != null) { @@ -61,12 +66,13 @@ cached = await CreateCachedResponse(context.DownstreamResponse); - _outputCache.Add(downstreamUrlKey, cached, TimeSpan.FromSeconds(context.DownstreamReRoute.CacheOptions.TtlSeconds), context.DownstreamReRoute.CacheOptions.Region); + _outputCache.Add(downStreamRequestCacheKey, cached, TimeSpan.FromSeconds(context.DownstreamReRoute.CacheOptions.TtlSeconds), context.DownstreamReRoute.CacheOptions.Region); Logger.LogDebug($"finished response added to cache for {downstreamUrlKey}"); } - private void SetHttpResponseMessageThisRequest(DownstreamContext context, DownstreamResponse response) + private void SetHttpResponseMessageThisRequest(DownstreamContext context, + DownstreamResponse response) { context.DownstreamResponse = response; } diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 9f81e11d..3fee15e7 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -107,6 +107,7 @@ namespace Ocelot.DependencyInjection Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); + Services.TryAddSingleton(); // see this for why we register this as singleton http://stackoverflow.com/questions/37371264/invalidoperationexception-unable-to-resolve-service-for-type-microsoft-aspnetc // could maybe use a scoped data repository diff --git a/src/Ocelot/Request/Middleware/DownstreamRequest.cs b/src/Ocelot/Request/Middleware/DownstreamRequest.cs index 665039d0..49d27efe 100644 --- a/src/Ocelot/Request/Middleware/DownstreamRequest.cs +++ b/src/Ocelot/Request/Middleware/DownstreamRequest.cs @@ -19,6 +19,7 @@ namespace Ocelot.Request.Middleware Headers = _request.Headers; AbsolutePath = _request.RequestUri.AbsolutePath; Query = _request.RequestUri.Query; + Content = _request.Content; } public HttpRequestHeaders Headers { get; } @@ -37,6 +38,8 @@ namespace Ocelot.Request.Middleware public string Query { get; set; } + public HttpContent Content { get; set; } + public HttpRequestMessage ToHttpRequestMessage() { var uriBuilder = new UriBuilder diff --git a/test/Ocelot.UnitTests/Cache/CacheKeyGeneratorTests.cs b/test/Ocelot.UnitTests/Cache/CacheKeyGeneratorTests.cs new file mode 100644 index 00000000..362fcc9a --- /dev/null +++ b/test/Ocelot.UnitTests/Cache/CacheKeyGeneratorTests.cs @@ -0,0 +1,34 @@ +using System.Net.Http; +using Microsoft.AspNetCore.Http; +using Ocelot.Cache; +using Ocelot.Middleware; +using Shouldly; +using TestStack.BDDfy; +using Xunit; + +namespace Ocelot.UnitTests.Cache { + public class CacheKeyGeneratorTests { + private readonly ICacheKeyGenerator _cacheKeyGenerator; + private readonly DownstreamContext _downstreamContext; + + public CacheKeyGeneratorTests() { + _cacheKeyGenerator = new CacheKeyGenerator(); + _cacheKeyGenerator = new CacheKeyGenerator(); + _downstreamContext = new DownstreamContext(new DefaultHttpContext()) { + DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123")) + }; + } + + [Fact] + public void should_generate_cache_key_from_context() { + this.Given(x => x.GivenCacheKeyFromContext(_downstreamContext)) + .BDDfy(); + } + + private void GivenCacheKeyFromContext(DownstreamContext context) { + string generatedCacheKey = _cacheKeyGenerator.GenerateRequestCacheKey(context); + string cachekey = MD5Helper.GenerateMd5("GET-https://some.url/blah?abcd=123"); + generatedCacheKey.ShouldBe(cachekey); + } + } +} diff --git a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs index 6f32739b..62d972f9 100644 --- a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs @@ -27,14 +27,17 @@ private OutputCacheMiddleware _middleware; private readonly DownstreamContext _downstreamContext; private readonly OcelotRequestDelegate _next; + private readonly ICacheKeyGenerator _cacheKeyGenerator; private CachedResponse _response; + public OutputCacheMiddlewareTests() { _cache = new Mock>(); _downstreamContext = new DownstreamContext(new DefaultHttpContext()); _loggerFactory = new Mock(); _logger = new Mock(); + _cacheKeyGenerator = new CacheKeyGenerator(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); _next = context => Task.CompletedTask; _downstreamContext.DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123")); @@ -89,7 +92,7 @@ private void WhenICallTheMiddleware() { - _middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cache.Object); + _middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cache.Object, _cacheKeyGenerator); _middleware.Invoke(_downstreamContext).GetAwaiter().GetResult(); } diff --git a/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs b/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs index 424c8efd..853354ef 100644 --- a/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs +++ b/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs @@ -23,6 +23,7 @@ public class OutputCacheMiddlewareRealCacheTests { private readonly IOcelotCache _cacheManager; + private readonly ICacheKeyGenerator _cacheKeyGenerator; private readonly OutputCacheMiddleware _middleware; private readonly DownstreamContext _downstreamContext; private OcelotRequestDelegate _next; @@ -32,17 +33,18 @@ public OutputCacheMiddlewareRealCacheTests() { _loggerFactory = new Mock(); - _logger = new Mock(); + _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); var cacheManagerOutputCache = CacheFactory.Build("OcelotOutputCache", x => { x.WithDictionaryHandle(); }); _cacheManager = new OcelotCacheManagerCache(cacheManagerOutputCache); + _cacheKeyGenerator = new CacheKeyGenerator(); _downstreamContext = new DownstreamContext(new DefaultHttpContext()); _downstreamContext.DownstreamRequest = new Ocelot.Request.Middleware.DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123")); _next = context => Task.CompletedTask; - _middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cacheManager); + _middleware = new OutputCacheMiddleware(_next, _loggerFactory.Object, _cacheManager, _cacheKeyGenerator); } [Fact] @@ -69,7 +71,8 @@ private void ThenTheContentTypeHeaderIsCached() { - var result = _cacheManager.Get("GET-https://some.url/blah?abcd=123", "kanken"); + string cacheKey = MD5Helper.GenerateMd5("GET-https://some.url/blah?abcd=123"); + var result = _cacheManager.Get(cacheKey, "kanken"); var header = result.ContentHeaders["Content-Type"]; header.First().ShouldBe("application/json"); }