Added some basic cache stuff

This commit is contained in:
TomPallister 2016-11-04 15:05:59 +00:00
parent 5afcebe7cb
commit 47afc850ff
31 changed files with 800 additions and 148 deletions

View File

@ -81,12 +81,21 @@ This is pretty much all you need to get going.......more to come!
Ocelot uses the standard logging interfaces ILoggerFactory / ILogger<T> as such you can use any logging provider you like such as default, nlog, serilog or whatever you want.
## Caching
Ocelot supports some very rudimentary caching at the moment provider by the [CacheManager](http://cachemanager.net/) project. This is an amazing project
that is solving a lot of caching problems. I would reccomend using this package to cache with Ocelot. If you look at the example [here](https://github.com/TomPallister/Ocelot/blob/develop/test/Ocelot.ManualTest/Startup.cs)
you can see how the cache manager is setup and then passed into the Ocelot AddOcelotOutputCaching 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. More to come!
## Not supported
Ocelot does not support...
- Chunked Encoding - Ocelot will always get the body size and return Content-Length header. Sorry
* Chunked Encoding - Ocelot will always get the body size and return Content-Length header. Sorry
if this doesn't work for your use case!
- Fowarding a host header - The host header that you send to Ocelot will not be forwarded to
* Fowarding a host header - The host header that you send to Ocelot will not be forwarded to
the downstream service. Obviously this would break everything :(

View File

@ -60,5 +60,11 @@
# This tells Ocelot to look for a header and use its value as a request/correlation id.
# If it is set here then the id will be forwarded to the downstream service. If it
# does not then it will not be forwarded
"RequestIdKey": "OcRequestId"
"RequestIdKey": "OcRequestId",
# If this is set the response from the downstream service will be cached using the key that called it.
# This gives the user a chance to influence the key by adding some random query string paramter for
# a user id or something that would get ignored by the downstream service. This is a hack and I
# intend to provide a mechanism the user can specify for the ttl caching. Also want to expand
# the caching a lot.
"FileCacheOptions": { "TtlSeconds": 15 }
}

View File

@ -0,0 +1,10 @@
using System;
namespace Ocelot.Cache
{
public interface IOcelotCache<T>
{
void Add(string key, T value, TimeSpan ttl);
T Get(string key);
}
}

View File

@ -0,0 +1,69 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Ocelot.Infrastructure.RequestData;
using Ocelot.Middleware;
namespace Ocelot.Cache.Middleware
{
public class OutputCacheMiddleware : OcelotMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger _logger;
private readonly IOcelotCache<HttpResponseMessage> _outputCache;
public OutputCacheMiddleware(RequestDelegate next,
ILoggerFactory loggerFactory,
IRequestScopedDataRepository scopedDataRepository,
IOcelotCache<HttpResponseMessage> outputCache)
:base(scopedDataRepository)
{
_next = next;
_outputCache = outputCache;
_logger = loggerFactory.CreateLogger<OutputCacheMiddleware>();
}
public async Task Invoke(HttpContext context)
{
var downstreamUrlKey = DownstreamUrl;
if (!DownstreamRoute.ReRoute.IsCached)
{
await _next.Invoke(context);
return;
}
_logger.LogDebug("OutputCacheMiddleware.Invoke stared checking cache for downstreamUrlKey", downstreamUrlKey);
var cached = _outputCache.Get(downstreamUrlKey);
if (cached != null)
{
SetHttpResponseMessageThisRequest(cached);
_logger.LogDebug("OutputCacheMiddleware.Invoke finished returned cached response for downstreamUrlKey", downstreamUrlKey);
return;
}
_logger.LogDebug("OutputCacheMiddleware.Invoke no resonse cached for downstreamUrlKey", downstreamUrlKey);
await _next.Invoke(context);
if (PipelineError())
{
_logger.LogDebug("OutputCacheMiddleware.Invoke there was a pipeline error for downstreamUrlKey", downstreamUrlKey);
return;
}
var response = HttpResponseMessage;
_outputCache.Add(downstreamUrlKey, response, TimeSpan.FromSeconds(DownstreamRoute.ReRoute.FileCacheOptions.TtlSeconds));
_logger.LogDebug("OutputCacheMiddleware.Invoke finished response added to cache for downstreamUrlKey", downstreamUrlKey);
}
}
}

View File

@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Builder;
namespace Ocelot.Cache.Middleware
{
public static class OutputCacheMiddlewareExtensions
{
public static IApplicationBuilder UseOutputCacheMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<OutputCacheMiddleware>();
}
}
}

View File

@ -0,0 +1,25 @@
using System;
using CacheManager.Core;
namespace Ocelot.Cache
{
public class OcelotCacheManagerCache<T> : IOcelotCache<T>
{
private readonly ICacheManager<T> _cacheManager;
public OcelotCacheManagerCache(ICacheManager<T> cacheManager)
{
_cacheManager = cacheManager;
}
public void Add(string key, T value, TimeSpan ttl)
{
_cacheManager.Add(new CacheItem<T>(key, value, ExpirationMode.Absolute, ttl));
}
public T Get(string key)
{
return _cacheManager.Get<T>(key);
}
}
}

View File

@ -21,6 +21,8 @@ namespace Ocelot.Configuration.Builder
private bool _isAuthorised;
private List<ClaimToThing> _claimToQueries;
private string _requestIdHeaderKey;
private bool _isCached;
private CacheOptions _fileCacheOptions;
public ReRouteBuilder()
{
@ -127,9 +129,24 @@ namespace Ocelot.Configuration.Builder
return this;
}
public ReRouteBuilder WithIsCached(bool input)
{
_isCached = input;
return this;
}
public ReRouteBuilder WithCacheOptions(CacheOptions input)
{
_fileCacheOptions = input;
return this;
}
public ReRoute Build()
{
return new ReRoute(_downstreamTemplate, _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern, _isAuthenticated, new AuthenticationOptions(_authenticationProvider, _authenticationProviderUrl, _scopeName, _requireHttps, _additionalScopes, _scopeSecret), _configHeaderExtractorProperties, _claimToClaims, _routeClaimRequirement, _isAuthorised, _claimToQueries, _requestIdHeaderKey);
return new ReRoute(_downstreamTemplate, _upstreamTemplate, _upstreamHttpMethod, _upstreamTemplatePattern,
_isAuthenticated, new AuthenticationOptions(_authenticationProvider, _authenticationProviderUrl, _scopeName,
_requireHttps, _additionalScopes, _scopeSecret), _configHeaderExtractorProperties, _claimToClaims, _routeClaimRequirement,
_isAuthorised, _claimToQueries, _requestIdHeaderKey, _isCached, _fileCacheOptions);
}
}
}

View File

@ -0,0 +1,12 @@
namespace Ocelot.Configuration
{
public class CacheOptions
{
public CacheOptions(int ttlSeconds)
{
TtlSeconds = ttlSeconds;
}
public int TtlSeconds { get; private set; }
}
}

View File

@ -100,6 +100,8 @@ namespace Ocelot.Configuration.Creator
var isAuthorised = reRoute.RouteClaimsRequirement?.Count > 0;
var isCached = reRoute.FileCacheOptions.TtlSeconds > 0;
if (isAuthenticated)
{
var authOptionsForRoute = new AuthenticationOptions(reRoute.AuthenticationOptions.Provider,
@ -115,7 +117,7 @@ namespace Ocelot.Configuration.Creator
reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated,
authOptionsForRoute, claimsToHeaders, claimsToClaims,
reRoute.RouteClaimsRequirement, isAuthorised, claimsToQueries,
reRoute.RequestIdKey
reRoute.RequestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds)
);
}
@ -123,7 +125,7 @@ namespace Ocelot.Configuration.Creator
reRoute.UpstreamHttpMethod, upstreamTemplate, isAuthenticated,
null, new List<ClaimToThing>(), new List<ClaimToThing>(),
reRoute.RouteClaimsRequirement, isAuthorised, new List<ClaimToThing>(),
reRoute.RequestIdKey);
reRoute.RequestIdKey, isCached, new CacheOptions(reRoute.FileCacheOptions.TtlSeconds));
}
private List<ClaimToThing> GetAddThingsToRequest(Dictionary<string,string> thingBeingAdded)

View File

@ -0,0 +1,7 @@
namespace Ocelot.Configuration.File
{
public class FileCacheOptions
{
public int TtlSeconds { get; set; }
}
}

View File

@ -11,6 +11,7 @@ namespace Ocelot.Configuration.File
RouteClaimsRequirement = new Dictionary<string, string>();
AddQueriesToRequest = new Dictionary<string, string>();
AuthenticationOptions = new FileAuthenticationOptions();
FileCacheOptions = new FileCacheOptions();
}
public string DownstreamTemplate { get; set; }
@ -22,5 +23,6 @@ namespace Ocelot.Configuration.File
public Dictionary<string, string> RouteClaimsRequirement { get; set; }
public Dictionary<string, string> AddQueriesToRequest { get; set; }
public string RequestIdKey { get; set; }
public FileCacheOptions FileCacheOptions { get; set; }
}
}

View File

@ -4,7 +4,10 @@ namespace Ocelot.Configuration
{
public class ReRoute
{
public ReRoute(string downstreamTemplate, string upstreamTemplate, string upstreamHttpMethod, string upstreamTemplatePattern, bool isAuthenticated, AuthenticationOptions authenticationOptions, List<ClaimToThing> configurationHeaderExtractorProperties, List<ClaimToThing> claimsToClaims, Dictionary<string, string> routeClaimsRequirement, bool isAuthorised, List<ClaimToThing> claimsToQueries, string requestIdKey)
public ReRoute(string downstreamTemplate, string upstreamTemplate, string upstreamHttpMethod, string upstreamTemplatePattern,
bool isAuthenticated, AuthenticationOptions authenticationOptions, List<ClaimToThing> configurationHeaderExtractorProperties,
List<ClaimToThing> claimsToClaims, Dictionary<string, string> routeClaimsRequirement, bool isAuthorised, List<ClaimToThing> claimsToQueries,
string requestIdKey, bool isCached, CacheOptions fileCacheOptions)
{
DownstreamTemplate = downstreamTemplate;
UpstreamTemplate = upstreamTemplate;
@ -15,6 +18,8 @@ namespace Ocelot.Configuration
RouteClaimsRequirement = routeClaimsRequirement;
IsAuthorised = isAuthorised;
RequestIdKey = requestIdKey;
IsCached = isCached;
FileCacheOptions = fileCacheOptions;
ClaimsToQueries = claimsToQueries
?? new List<ClaimToThing>();
ClaimsToClaims = claimsToClaims
@ -35,5 +40,7 @@ namespace Ocelot.Configuration
public List<ClaimToThing> ClaimsToClaims { get; private set; }
public Dictionary<string, string> RouteClaimsRequirement { get; private set; }
public string RequestIdKey { get; private set; }
public bool IsCached { get; private set; }
public CacheOptions FileCacheOptions { get; private set; }
}
}

View File

@ -1,9 +1,14 @@
using Microsoft.AspNetCore.Http;
using System;
using System.Net.Http;
using CacheManager.Core;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Ocelot.Authentication.Handler.Creator;
using Ocelot.Authentication.Handler.Factory;
using Ocelot.Authorisation;
using Ocelot.Cache;
using Ocelot.Claims;
using Ocelot.Configuration.Creator;
using Ocelot.Configuration.File;
@ -26,11 +31,21 @@ namespace Ocelot.DependencyInjection
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddOcelotOutputCaching(this IServiceCollection services, Action<ConfigurationBuilderCachePart> settings)
{
var cacheManagerOutputCache = CacheFactory.Build<HttpResponseMessage>("OcelotOutputCache", settings);
var ocelotCacheManager = new OcelotCacheManagerCache<HttpResponseMessage>(cacheManagerOutputCache);
services.AddSingleton<ICacheManager<HttpResponseMessage>>(cacheManagerOutputCache);
services.AddSingleton<IOcelotCache<HttpResponseMessage>>(ocelotCacheManager);
return services;
}
public static IServiceCollection AddOcelotFileConfiguration(this IServiceCollection services, IConfigurationRoot configurationRoot)
{
services.Configure<FileConfiguration>(configurationRoot);
// ocelot services.
services.AddSingleton<IOcelotConfigurationCreator, FileOcelotConfigurationCreator>();
services.AddSingleton<IOcelotConfigurationRepository, InMemoryOcelotConfigurationRepository>();
services.AddSingleton<IConfigurationValidator, FileConfigurationValidator>();
@ -40,11 +55,9 @@ namespace Ocelot.DependencyInjection
public static IServiceCollection AddOcelot(this IServiceCollection services)
{
// framework services dependency for the identity server middleware
services.AddMvcCore().AddJsonFormatters();
services.AddLogging();
// ocelot services.
services.AddSingleton<IRemoveOutputHeaders, RemoveOutputHeaders>();
services.AddSingleton<IOcelotConfigurationProvider, OcelotConfigurationProvider>();
services.AddSingleton<IClaimToThingConfigurationParser, ClaimToThingConfigurationParser>();

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Net.Http;
using Ocelot.DownstreamRouteFinder;
using Ocelot.Errors;
using Ocelot.Infrastructure.RequestData;
@ -59,6 +60,15 @@ namespace Ocelot.Middleware
}
}
public HttpResponseMessage HttpResponseMessage
{
get
{
var request = _requestScopedDataRepository.Get<HttpResponseMessage>("HttpResponseMessage");
return request.Data;
}
}
public void SetDownstreamRouteForThisRequest(DownstreamRoute downstreamRoute)
{
_requestScopedDataRepository.Add("DownstreamRoute", downstreamRoute);
@ -73,5 +83,11 @@ namespace Ocelot.Middleware
{
_requestScopedDataRepository.Add("Request", request);
}
public void SetHttpResponseMessageThisRequest(HttpResponseMessage responseMessage)
{
_requestScopedDataRepository.Add("HttpResponseMessage", responseMessage);
}
}
}

View File

@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Builder;
using Ocelot.Authentication.Middleware;
using Ocelot.Cache.Middleware;
using Ocelot.Claims.Middleware;
using Ocelot.DownstreamRouteFinder.Middleware;
using Ocelot.DownstreamUrlCreator.Middleware;
@ -99,6 +100,10 @@ namespace Ocelot.Middleware
// This takes the downstream route we retrieved earlier and replaces any placeholders with the variables that should be used
builder.UseDownstreamUrlCreatorMiddleware();
// Not sure if this is the best place for this but we use the downstream url
// as the basis for our cache key.
builder.UseOutputCacheMiddleware();
// Everything should now be ready to build or HttpRequest
builder.UseHttpRequestBuilderMiddleware();

View File

@ -1,10 +1,7 @@
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Ocelot.Infrastructure.RequestData;
using Ocelot.Middleware;
using Ocelot.Responder;
namespace Ocelot.Requester.Middleware
{
@ -12,17 +9,14 @@ namespace Ocelot.Requester.Middleware
{
private readonly RequestDelegate _next;
private readonly IHttpRequester _requester;
private readonly IHttpResponder _responder;
public HttpRequesterMiddleware(RequestDelegate next,
IHttpRequester requester,
IRequestScopedDataRepository requestScopedDataRepository,
IHttpResponder responder)
IRequestScopedDataRepository requestScopedDataRepository)
:base(requestScopedDataRepository)
{
_next = next;
_requester = requester;
_responder = responder;
}
public async Task Invoke(HttpContext context)
@ -35,12 +29,7 @@ namespace Ocelot.Requester.Middleware
return;
}
var setResponse = await _responder.SetResponseOnHttpContext(context, response.Data);
if (setResponse.IsError)
{
SetPipelineError(response.Errors);
}
SetHttpResponseMessageThisRequest(response.Data);
}
}
}

View File

@ -38,7 +38,7 @@ namespace Ocelot.Responder
AddHeaderIfDoesntExist(context, httpResponseHeader);
}
var content = await response.Content.ReadAsStreamAsync();
var content = await response.Content.ReadAsByteArrayAsync();
AddHeaderIfDoesntExist(context, new KeyValuePair<string, IEnumerable<string>>("Content-Length", new []{ content.Length.ToString() }) );
@ -52,12 +52,10 @@ namespace Ocelot.Responder
}, context);
using (var reader = new StreamReader(content))
using (Stream stream = new MemoryStream(content))
{
var responseContent = reader.ReadToEnd();
await context.Response.WriteAsync(responseContent);
await stream.CopyToAsync(context.Response.Body);
}
return new OkResponse();
}

View File

@ -1,6 +1,7 @@
using System.Net.Http;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Ocelot.Errors;
using Ocelot.Infrastructure.RequestData;
using Ocelot.Middleware;
@ -31,6 +32,21 @@ namespace Ocelot.Responder.Middleware
{
var errors = GetPipelineErrors();
await SetErrorResponse(context, errors);
}
else
{
var setResponse = await _responder.SetResponseOnHttpContext(context, HttpResponseMessage);
if (setResponse.IsError)
{
await SetErrorResponse(context, setResponse.Errors);
}
}
}
private async Task SetErrorResponse(HttpContext context, List<Error> errors)
{
var statusCode = _codeMapper.Map(errors);
if (!statusCode.IsError)
@ -43,5 +59,4 @@ namespace Ocelot.Responder.Middleware
}
}
}
}
}

View File

@ -27,7 +27,10 @@
"Microsoft.NETCore.App": {
"version": "1.0.1",
"type": "platform"
}
},
"CacheManager.Core": "0.9.1",
"CacheManager.Microsoft.Extensions.Configuration": "0.9.1",
"CacheManager.Microsoft.Extensions.Logging": "0.9.1"
},
"frameworks": {

View File

@ -0,0 +1,130 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Ocelot.Configuration.File;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.AcceptanceTests
{
public class CachingTests : IDisposable
{
private IWebHost _builder;
private readonly Steps _steps;
public CachingTests()
{
_steps = new Steps();
}
[Fact]
public void should_return_cached_response()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamTemplate = "http://localhost:51879/",
UpstreamTemplate = "/",
UpstreamHttpMethod = "Get",
FileCacheOptions = new FileCacheOptions
{
TtlSeconds = 100
}
}
}
};
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", 200, "Hello from Laura"))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
.Given(x => x.GivenTheServiceNowReturns("http://localhost:51879", 200, "Hello from Tom"))
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
.BDDfy();
}
[Fact]
public void should_not_return_cached_response_as_ttl_expires()
{
var configuration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamTemplate = "http://localhost:51879/",
UpstreamTemplate = "/",
UpstreamHttpMethod = "Get",
FileCacheOptions = new FileCacheOptions
{
TtlSeconds = 1
}
}
}
};
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:51879", 200, "Hello from Laura"))
.And(x => _steps.GivenThereIsAConfiguration(configuration))
.And(x => _steps.GivenOcelotIsRunning())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura"))
.Given(x => x.GivenTheServiceNowReturns("http://localhost:51879", 200, "Hello from Tom"))
.And(x => x.GivenTheCacheExpires())
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom"))
.BDDfy();
}
private void GivenTheCacheExpires()
{
Thread.Sleep(1000);
}
private void GivenTheServiceNowReturns(string url, int statusCode, string responseBody)
{
_builder.Dispose();
GivenThereIsAServiceRunningOn(url, statusCode, responseBody);
}
private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody)
{
_builder = new WebHostBuilder()
.UseUrls(url)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseUrls(url)
.Configure(app =>
{
app.Run(async context =>
{
context.Response.StatusCode = statusCode;
await context.Response.WriteAsync(responseBody);
});
})
.Build();
_builder.Start();
}
public void Dispose()
{
_builder?.Dispose();
_steps.Dispose();
}
}
}

View File

@ -2,15 +2,12 @@
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Ocelot.Configuration.File;
using Ocelot.Infrastructure.RequestData;
using Ocelot.Middleware;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
@ -21,21 +18,160 @@ namespace Ocelot.AcceptanceTests
private readonly string _configurationPath;
private IWebHost _builder;
private readonly Steps _steps;
private int _counter;
public CustomMiddlewareTests()
{
_counter = 0;
_steps = new Steps();;
_configurationPath = "configuration.json";
}
[Fact]
public void response_should_come_from_pre_authorisation_middleware()
public void should_call_pre_query_string_builder_middleware()
{
var configuration = new OcelotMiddlewareConfiguration
{
AuthorisationMiddleware = async (ctx, next) =>
{
_counter++;
await next.Invoke();
}
};
var fileConfiguration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamTemplate = "http://localhost:41879/",
UpstreamTemplate = "/",
UpstreamHttpMethod = "Get",
}
}
};
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:41879", 200))
.And(x => _steps.GivenThereIsAConfiguration(fileConfiguration, _configurationPath))
.And(x => _steps.GivenOcelotIsRunning(configuration))
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => x.ThenTheCounterIs(1))
.BDDfy();
}
[Fact]
public void should_call_authorisation_middleware()
{
var configuration = new OcelotMiddlewareConfiguration
{
AuthorisationMiddleware = async (ctx, next) =>
{
_counter++;
await next.Invoke();
}
};
var fileConfiguration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamTemplate = "http://localhost:41879/",
UpstreamTemplate = "/",
UpstreamHttpMethod = "Get",
}
}
};
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:41879", 200))
.And(x => _steps.GivenThereIsAConfiguration(fileConfiguration, _configurationPath))
.And(x => _steps.GivenOcelotIsRunning(configuration))
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => x.ThenTheCounterIs(1))
.BDDfy();
}
[Fact]
public void should_call_authentication_middleware()
{
var configuration = new OcelotMiddlewareConfiguration
{
AuthenticationMiddleware = async (ctx, next) =>
{
_counter++;
await next.Invoke();
}
};
var fileConfiguration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamTemplate = "http://localhost:41879/",
UpstreamTemplate = "/",
UpstreamHttpMethod = "Get",
}
}
};
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:41879", 200))
.And(x => _steps.GivenThereIsAConfiguration(fileConfiguration, _configurationPath))
.And(x => _steps.GivenOcelotIsRunning(configuration))
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => x.ThenTheCounterIs(1))
.BDDfy();
}
[Fact]
public void should_call_pre_error_middleware()
{
var configuration = new OcelotMiddlewareConfiguration
{
PreErrorResponderMiddleware = async (ctx, next) =>
{
_counter++;
await next.Invoke();
}
};
var fileConfiguration = new FileConfiguration
{
ReRoutes = new List<FileReRoute>
{
new FileReRoute
{
DownstreamTemplate = "http://localhost:41879/",
UpstreamTemplate = "/",
UpstreamHttpMethod = "Get",
}
}
};
this.Given(x => x.GivenThereIsAServiceRunningOn("http://localhost:41879", 200))
.And(x => _steps.GivenThereIsAConfiguration(fileConfiguration, _configurationPath))
.And(x => _steps.GivenOcelotIsRunning(configuration))
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => x.ThenTheCounterIs(1))
.BDDfy();
}
[Fact]
public void should_call_pre_authorisation_middleware()
{
var configuration = new OcelotMiddlewareConfiguration
{
PreAuthorisationMiddleware = async (ctx, next) =>
{
await ctx.Response.WriteAsync("PreHttpResponderMiddleware");
_counter++;
await next.Invoke();
}
};
@ -57,18 +193,19 @@ namespace Ocelot.AcceptanceTests
.And(x => _steps.GivenOcelotIsRunning(configuration))
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("PreHttpResponderMiddleware"))
.And(x => x.ThenTheCounterIs(1))
.BDDfy();
}
[Fact]
public void response_should_come_from_pre_http_authentication_middleware()
public void should_call_pre_http_authentication_middleware()
{
var configuration = new OcelotMiddlewareConfiguration
{
PreAuthenticationMiddleware = async (ctx, next) =>
{
await ctx.Response.WriteAsync("PreHttpRequesterMiddleware");
_counter++;
await next.Invoke();
}
};
@ -90,10 +227,15 @@ namespace Ocelot.AcceptanceTests
.And(x => _steps.GivenOcelotIsRunning(configuration))
.When(x => _steps.WhenIGetUrlOnTheApiGateway("/"))
.Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK))
.And(x => _steps.ThenTheResponseBodyShouldBe("PreHttpRequesterMiddleware"))
.And(x => x.ThenTheCounterIs(1))
.BDDfy();
}
private void ThenTheCounterIs(int expected)
{
_counter.ShouldBe(expected);
}
private void GivenThereIsAServiceRunningOn(string url, int statusCode)
{
_builder = new WebHostBuilder()

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using CacheManager.Core;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
@ -15,6 +16,7 @@ using Ocelot.DependencyInjection;
using Ocelot.ManualTest;
using Ocelot.Middleware;
using Shouldly;
using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder;
namespace Ocelot.AcceptanceTests
{
@ -82,6 +84,16 @@ namespace Ocelot.AcceptanceTests
.UseConfiguration(configuration)
.ConfigureServices(s =>
{
Action<ConfigurationBuilderCachePart> settings = (x) =>
{
x.WithMicrosoftLogging(log =>
{
log.AddConsole(LogLevel.Debug);
})
.WithDictionaryHandle();
};
s.AddOcelotOutputCaching(settings);
s.AddOcelotFileConfiguration(configuration);
s.AddOcelot();
})

View File

@ -1 +1 @@
{"ReRoutes":[{"DownstreamTemplate":"http://localhost:41879/","UpstreamTemplate":"/","UpstreamHttpMethod":"Get","AuthenticationOptions":{"Provider":null,"ProviderRootUrl":null,"ScopeName":null,"RequireHttps":false,"AdditionalScopes":[],"ScopeSecret":null},"AddHeadersToRequest":{},"AddClaimsToRequest":{},"RouteClaimsRequirement":{},"AddQueriesToRequest":{},"RequestIdKey":null}]}
{"ReRoutes":[{"DownstreamTemplate":"http://localhost:41879/","UpstreamTemplate":"/","UpstreamHttpMethod":"Get","AuthenticationOptions":{"Provider":null,"ProviderRootUrl":null,"ScopeName":null,"RequireHttps":false,"AdditionalScopes":[],"ScopeSecret":null},"AddHeadersToRequest":{},"AddClaimsToRequest":{},"RouteClaimsRequirement":{},"AddQueriesToRequest":{},"RequestIdKey":null,"FileCacheOptions":{"TtlSeconds":0}}]}

View File

@ -1,10 +1,13 @@
using Microsoft.AspNetCore.Builder;
using System;
using CacheManager.Core;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder;
namespace Ocelot.ManualTest
{
@ -27,6 +30,16 @@ namespace Ocelot.ManualTest
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
Action<ConfigurationBuilderCachePart> settings = (x) =>
{
x.WithMicrosoftLogging(log =>
{
log.AddConsole(LogLevel.Debug);
})
.WithDictionaryHandle();
};
services.AddOcelotOutputCaching(settings);
services.AddOcelotFileConfiguration(Configuration);
services.AddOcelot();
}

View File

@ -40,7 +40,8 @@
{
"DownstreamTemplate": "http://jsonplaceholder.typicode.com/posts",
"UpstreamTemplate": "/posts",
"UpstreamHttpMethod": "Get"
"UpstreamHttpMethod": "Get",
"FileCacheOptions": { "TtlSeconds": 15 }
},
{
"DownstreamTemplate": "http://jsonplaceholder.typicode.com/posts/{postId}",

View File

@ -0,0 +1,77 @@
using System;
using CacheManager.Core;
using Moq;
using Ocelot.Cache;
using Shouldly;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests.Cache
{
public class CacheManagerCacheTests
{
private OcelotCacheManagerCache<string> _ocelotOcelotCacheManager;
private Mock<ICacheManager<string>> _mockCacheManager;
private string _key;
private string _value;
private string _resultGet;
private TimeSpan _ttlSeconds;
public CacheManagerCacheTests()
{
_mockCacheManager = new Mock<ICacheManager<string>>();
_ocelotOcelotCacheManager = new OcelotCacheManagerCache<string>(_mockCacheManager.Object);
}
[Fact]
public void should_get_from_cache()
{
this.Given(x => x.GivenTheFollowingIsCached("someKey", "someValue"))
.When(x => x.WhenIGetFromTheCache())
.Then(x => x.ThenTheResultIs("someValue"))
.BDDfy();
}
[Fact]
public void should_add_to_cache()
{
this.When(x => x.WhenIAddToTheCache("someKey", "someValue", TimeSpan.FromSeconds(1)))
.Then(x => x.ThenTheCacheIsCalledCorrectly())
.BDDfy();
}
private void WhenIAddToTheCache(string key, string value, TimeSpan ttlSeconds)
{
_key = key;
_value = value;
_ttlSeconds = ttlSeconds;
_ocelotOcelotCacheManager.Add(_key, _value, _ttlSeconds);
}
private void ThenTheCacheIsCalledCorrectly()
{
_mockCacheManager
.Verify(x => x.Add(It.IsAny<CacheItem<string>>()), Times.Once);
}
private void ThenTheResultIs(string expected)
{
_resultGet.ShouldBe(expected);
}
private void WhenIGetFromTheCache()
{
_resultGet = _ocelotOcelotCacheManager.Get(_key);
}
private void GivenTheFollowingIsCached(string key, string value)
{
_key = key;
_value = value;
_mockCacheManager
.Setup(x => x.Get<string>(It.IsAny<string>()))
.Returns(value);
}
}
}

View File

@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using CacheManager.Core;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Ocelot.Cache;
using Ocelot.Cache.Middleware;
using Ocelot.Configuration;
using Ocelot.Configuration.Builder;
using Ocelot.DownstreamRouteFinder;
using Ocelot.DownstreamRouteFinder.UrlMatcher;
using Ocelot.Infrastructure.RequestData;
using Ocelot.Responses;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests.Cache
{
public class OutputCacheMiddlewareTests
{
private readonly Mock<IOcelotCache<HttpResponseMessage>> _cacheManager;
private readonly Mock<IRequestScopedDataRepository> _scopedRepo;
private readonly string _url;
private readonly TestServer _server;
private readonly HttpClient _client;
private HttpResponseMessage _result;
private HttpResponseMessage _response;
public OutputCacheMiddlewareTests()
{
_cacheManager = new Mock<IOcelotCache<HttpResponseMessage>>();
_scopedRepo = new Mock<IRequestScopedDataRepository>();
_url = "http://localhost:51879";
var builder = new WebHostBuilder()
.ConfigureServices(x =>
{
x.AddLogging();
x.AddSingleton(_cacheManager.Object);
x.AddSingleton(_scopedRepo.Object);
})
.UseUrls(_url)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseUrls(_url)
.Configure(app =>
{
app.UseOutputCacheMiddleware();
});
_server = new TestServer(builder);
_client = _server.CreateClient();
}
[Fact]
public void should_returned_cached_item_when_it_is_in_cache()
{
this.Given(x => x.GivenThereIsACachedResponse(new HttpResponseMessage()))
.And(x => x.GivenTheDownstreamRouteIs())
.And(x => x.GivenThereIsADownstreamUrl())
.When(x => x.WhenICallTheMiddleware())
.Then(x => x.ThenTheCacheGetIsCalledCorrectly())
.BDDfy();
}
[Fact]
public void should_continue_with_pipeline_and_cache_response()
{
this.Given(x => x.GivenResponseIsNotCached())
.And(x => x.GivenTheDownstreamRouteIs())
.And(x => x.GivenThereAreNoErrors())
.And(x => x.GivenThereIsADownstreamUrl())
.When(x => x.WhenICallTheMiddleware())
.Then(x => x.ThenTheCacheAddIsCalledCorrectly())
.BDDfy();
}
private void GivenTheDownstreamRouteIs()
{
var reRoute = new ReRouteBuilder().WithIsCached(true).WithCacheOptions(new CacheOptions(100)).Build();
var downstreamRoute = new DownstreamRoute(new List<UrlPathPlaceholderNameAndValue>(), reRoute);
_scopedRepo
.Setup(x => x.Get<DownstreamRoute>(It.IsAny<string>()))
.Returns(new OkResponse<DownstreamRoute>(downstreamRoute));
}
private void GivenThereAreNoErrors()
{
_scopedRepo
.Setup(x => x.Get<bool>("OcelotMiddlewareError"))
.Returns(new OkResponse<bool>(false));
}
private void GivenThereIsADownstreamUrl()
{
_scopedRepo
.Setup(x => x.Get<string>("DownstreamUrl"))
.Returns(new OkResponse<string>("anything"));
}
private void ThenTheCacheGetIsCalledCorrectly()
{
_cacheManager
.Verify(x => x.Get(It.IsAny<string>()), Times.Once);
}
private void ThenTheCacheAddIsCalledCorrectly()
{
_cacheManager
.Verify(x => x.Add(It.IsAny<string>(), It.IsAny<HttpResponseMessage>(), It.IsAny<TimeSpan>()), Times.Once);
}
private void GivenResponseIsNotCached()
{
_scopedRepo
.Setup(x => x.Get<HttpResponseMessage>("HttpResponseMessage"))
.Returns(new OkResponse<HttpResponseMessage>(new HttpResponseMessage()));
}
private void GivenThereIsACachedResponse(HttpResponseMessage response)
{
_response = response;
_cacheManager
.Setup(x => x.Get(It.IsAny<string>()))
.Returns(_response);
}
private void WhenICallTheMiddleware()
{
_result = _client.GetAsync(_url).Result;
}
}
}

View File

@ -1,83 +0,0 @@
/*
using System;
using System.IO;
using System.Net.Http;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Moq;
using Ocelot.Middleware;
using Ocelot.RequestId.Provider;
using TestStack.BDDfy;
using Xunit;
namespace Ocelot.UnitTests.Errors
{
public class GobalErrorHandlerTests
{
private readonly Mock<ILoggerFactory> _loggerFactory;
private readonly Mock<ILogger<ExceptionHandlerMiddleware>> _logger;
private readonly Mock<IRequestIdProvider> _requestIdProvider;
private readonly string _url;
private readonly TestServer _server;
private readonly HttpClient _client;
private HttpResponseMessage _result;
public GobalErrorHandlerTests()
{
_url = "http://localhost:51879";
_logger = new Mock<ILogger<ExceptionHandlerMiddleware>>();
_loggerFactory = new Mock<ILoggerFactory>();
_requestIdProvider = new Mock<IRequestIdProvider>();
var builder = new WebHostBuilder()
.ConfigureServices(x =>
{
x.AddSingleton(_requestIdProvider.Object);
x.AddSingleton(_loggerFactory.Object);
})
.UseUrls(_url)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseIISIntegration()
.UseUrls(_url)
.Configure(app =>
{
app.UseExceptionHandlerMiddleware();
app.Run(x =>
{
throw new Exception("BLAM");
});
});
_loggerFactory
.Setup(x => x.CreateLogger<ExceptionHandlerMiddleware>())
.Returns(_logger.Object);
_server = new TestServer(builder);
_client = _server.CreateClient();
}
[Fact]
public void should_catch_exception_and_log()
{
this.When(x => x.WhenICallTheMiddleware())
.And(x => x.TheLoggerIsCalledCorrectly())
.BDDfy();
}
private void TheLoggerIsCalledCorrectly()
{
_logger
.Verify(x => x.LogError(It.IsAny<EventId>(), It.IsAny<string>(), It.IsAny<Exception>()), Times.Once);
}
private void WhenICallTheMiddleware()
{
_result = _client.GetAsync(_url).Result;
}
}
}
*/

View File

@ -4,7 +4,6 @@ namespace Ocelot.UnitTests.Infrastructure
{
using System.Collections.Generic;
using System.Security.Claims;
using Errors;
using Ocelot.Infrastructure.Claims.Parser;
using Responses;
using Shouldly;

View File

@ -27,19 +27,16 @@ namespace Ocelot.UnitTests.Requester
private HttpResponseMessage _result;
private OkResponse<HttpResponseMessage> _response;
private OkResponse<Ocelot.Request.Request> _request;
private readonly Mock<IHttpResponder> _responder;
public HttpRequesterMiddlewareTests()
{
_url = "http://localhost:51879";
_requester = new Mock<IHttpRequester>();
_scopedRepository = new Mock<IRequestScopedDataRepository>();
_responder = new Mock<IHttpResponder>();
var builder = new WebHostBuilder()
.ConfigureServices(x =>
{
x.AddSingleton(_responder.Object);
x.AddSingleton(_requester.Object);
x.AddSingleton(_scopedRepository.Object);
})
@ -62,9 +59,9 @@ namespace Ocelot.UnitTests.Requester
{
this.Given(x => x.GivenTheRequestIs(new Ocelot.Request.Request(new HttpRequestMessage(),new CookieContainer())))
.And(x => x.GivenTheRequesterReturns(new HttpResponseMessage()))
.And(x => x.GivenTheResponderReturns())
.And(x => x.GivenTheScopedRepoReturns())
.When(x => x.WhenICallTheMiddleware())
.Then(x => x.ThenTheResponderIsCalledCorrectly())
.Then(x => x.ThenTheScopedRepoIsCalledCorrectly())
.BDDfy();
}
@ -76,17 +73,17 @@ namespace Ocelot.UnitTests.Requester
.ReturnsAsync(_response);
}
private void GivenTheResponderReturns()
private void GivenTheScopedRepoReturns()
{
_responder
.Setup(x => x.SetResponseOnHttpContext(It.IsAny<HttpContext>(), _response.Data))
.ReturnsAsync(new OkResponse());
_scopedRepository
.Setup(x => x.Add(It.IsAny<string>(), _response.Data))
.Returns(new OkResponse());
}
private void ThenTheResponderIsCalledCorrectly()
private void ThenTheScopedRepoIsCalledCorrectly()
{
_responder
.Verify(x => x.SetResponseOnHttpContext(It.IsAny<HttpContext>(), _response.Data), Times.Once());
_scopedRepository
.Verify(x => x.Add("HttpResponseMessage", _response.Data), Times.Once());
}
private void WhenICallTheMiddleware()

View File

@ -59,11 +59,19 @@ namespace Ocelot.UnitTests.Responder
{
this.Given(x => x.GivenTheHttpResponseMessageIs(new HttpResponseMessage()))
.And(x => x.GivenThereAreNoPipelineErrors())
.And(x => x.GivenTheResponderReturns())
.When(x => x.WhenICallTheMiddleware())
.Then(x => x.ThenThereAreNoErrors())
.BDDfy();
}
private void GivenTheResponderReturns()
{
_responder
.Setup(x => x.SetResponseOnHttpContext(It.IsAny<HttpContext>(), It.IsAny<HttpResponseMessage>()))
.ReturnsAsync(new OkResponse());
}
private void GivenThereAreNoPipelineErrors()
{
_scopedRepository