From 3b776a753cfecd17b70c91c8f3ea7e46ccb7de84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Raynald=20Messi=C3=A9?= Date: Fri, 13 Oct 2023 18:40:23 +0200 Subject: [PATCH 01/12] #1712 Bump to Polly 8.0 (#1714) * #1712 Migrate to Polly 8.0 * code review post merge * post PR * #1712 Migrate to Polly 8.0 * code review post merge * Update src/Ocelot.Provider.Polly/PollyQoSProvider.cs Co-authored-by: Raman Maksimchuk * namespaces * Refactor QoS provider * Refactor AddPolly extension * Remove single quote because semicolon ends sentence --------- Co-authored-by: Ray Co-authored-by: Raman Maksimchuk --- src/Ocelot.Provider.Polly/CircuitBreaker.cs | 11 +-- .../Ocelot.Provider.Polly.csproj | 2 +- .../OcelotBuilderExtensions.cs | 16 ++-- .../PollyCircuitBreakingDelegatingHandler.cs | 3 +- src/Ocelot.Provider.Polly/PollyQoSProvider.cs | 73 ++++++++----------- src/Ocelot.Provider.Polly/Usings.cs | 2 - test/Ocelot.UnitTests/Ocelot.UnitTests.csproj | 2 +- .../Polly/PollyQoSProviderTests.cs | 10 ++- 8 files changed, 51 insertions(+), 68 deletions(-) diff --git a/src/Ocelot.Provider.Polly/CircuitBreaker.cs b/src/Ocelot.Provider.Polly/CircuitBreaker.cs index ce2a89bf2..c9be6d924 100644 --- a/src/Ocelot.Provider.Polly/CircuitBreaker.cs +++ b/src/Ocelot.Provider.Polly/CircuitBreaker.cs @@ -1,19 +1,12 @@ -using Polly; - namespace Ocelot.Provider.Polly { public class CircuitBreaker { - private readonly List _policies = new(); - public CircuitBreaker(params IAsyncPolicy[] policies) { - foreach (var policy in policies.Where(p => p != null)) - { - _policies.Add(policy); - } + Policies = policies.Where(p => p != null).ToArray(); } - public IAsyncPolicy[] Policies => _policies.ToArray(); + public IAsyncPolicy[] Policies { get; } } } diff --git a/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj b/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj index 4ffcb9eb6..78150d630 100644 --- a/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj +++ b/src/Ocelot.Provider.Polly/Ocelot.Provider.Polly.csproj @@ -34,7 +34,7 @@ all - + diff --git a/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs index 0d167f571..229b2171e 100644 --- a/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs @@ -20,15 +20,13 @@ public static IOcelotBuilder AddPolly(this IOcelotBuilder builder) {typeof(BrokenCircuitException), e => new RequestTimedOutError(e)}, }; - builder.Services.AddSingleton(errorMapping); - - static DelegatingHandler QosDelegatingHandlerDelegate(DownstreamRoute route, IOcelotLoggerFactory logger) - { - return new PollyCircuitBreakingDelegatingHandler(new PollyQoSProvider(route, logger), logger); - } - - builder.Services.AddSingleton((QosDelegatingHandlerDelegate)QosDelegatingHandlerDelegate); + builder.Services + .AddSingleton(errorMapping) + .AddSingleton(GetDelegatingHandler); return builder; - } + } + + private static DelegatingHandler GetDelegatingHandler(DownstreamRoute route, IOcelotLoggerFactory logger) + => new PollyCircuitBreakingDelegatingHandler(new PollyQoSProvider(route, logger), logger); } } diff --git a/src/Ocelot.Provider.Polly/PollyCircuitBreakingDelegatingHandler.cs b/src/Ocelot.Provider.Polly/PollyCircuitBreakingDelegatingHandler.cs index 9153b70ce..94d66962f 100644 --- a/src/Ocelot.Provider.Polly/PollyCircuitBreakingDelegatingHandler.cs +++ b/src/Ocelot.Provider.Polly/PollyCircuitBreakingDelegatingHandler.cs @@ -1,6 +1,5 @@ using Ocelot.Logging; using Ocelot.Provider.Polly.Interfaces; -using Polly; using Polly.CircuitBreaker; namespace Ocelot.Provider.Polly @@ -28,7 +27,7 @@ protected override async Task SendAsync(HttpRequestMessage return await base.SendAsync(request, cancellationToken); } - IAsyncPolicy policy = policies.Length > 1 + var policy = policies.Length > 1 ? Policy.WrapAsync(policies) : policies[0]; diff --git a/src/Ocelot.Provider.Polly/PollyQoSProvider.cs b/src/Ocelot.Provider.Polly/PollyQoSProvider.cs index 420939de4..58a918162 100644 --- a/src/Ocelot.Provider.Polly/PollyQoSProvider.cs +++ b/src/Ocelot.Provider.Polly/PollyQoSProvider.cs @@ -1,65 +1,54 @@ -using Ocelot.Configuration; -using Ocelot.Logging; -using Ocelot.Provider.Polly.Interfaces; -using Polly; -using Polly.CircuitBreaker; -using Polly.Timeout; +using Ocelot.Configuration; +using Ocelot.Logging; +using Ocelot.Provider.Polly.Interfaces; +using Polly.CircuitBreaker; +using Polly.Timeout; namespace Ocelot.Provider.Polly { public class PollyQoSProvider : IPollyQoSProvider { - private readonly AsyncCircuitBreakerPolicy _circuitBreakerPolicy; - private readonly AsyncTimeoutPolicy _timeoutPolicy; - private readonly IOcelotLogger _logger; - - public PollyQoSProvider(AsyncCircuitBreakerPolicy circuitBreakerPolicy, AsyncTimeoutPolicy timeoutPolicy, IOcelotLogger logger) - { - _circuitBreakerPolicy = circuitBreakerPolicy; - _timeoutPolicy = timeoutPolicy; - _logger = logger; - } - public PollyQoSProvider(DownstreamRoute route, IOcelotLoggerFactory loggerFactory) { - _logger = loggerFactory.CreateLogger(); - - _ = Enum.TryParse(route.QosOptions.TimeoutStrategy, out TimeoutStrategy strategy); - - _timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromMilliseconds(route.QosOptions.TimeoutValue), strategy); - + AsyncCircuitBreakerPolicy circuitBreakerPolicy = null; if (route.QosOptions.ExceptionsAllowedBeforeBreaking > 0) - { - _circuitBreakerPolicy = Policy + { + var info = $"Route: {GetRouteName(route)}; Breaker logging in {nameof(PollyQoSProvider)}: "; + var logger = loggerFactory.CreateLogger(); + circuitBreakerPolicy = Policy .Handle() .Or() .Or() .CircuitBreakerAsync( exceptionsAllowedBeforeBreaking: route.QosOptions.ExceptionsAllowedBeforeBreaking, durationOfBreak: TimeSpan.FromMilliseconds(route.QosOptions.DurationOfBreak), - onBreak: (ex, breakDelay) => - { - _logger.LogError( - ".Breaker logging: Breaking the circuit for " + breakDelay.TotalMilliseconds + "ms!", ex); - }, + onBreak: (ex, breakDelay) => + logger.LogError(info + $"Breaking the circuit for {breakDelay.TotalMilliseconds} ms!", ex), onReset: () => - { - _logger.LogDebug(".Breaker logging: Call ok! Closed the circuit again."); - }, + logger.LogDebug(info + "Call OK! Closed the circuit again."), onHalfOpen: () => - { - _logger.LogDebug(".Breaker logging: Half-open; next call is a trial."); - } + logger.LogDebug(info + "Half-open; Next call is a trial.") ); } - else - { - _circuitBreakerPolicy = null; - } - CircuitBreaker = new CircuitBreaker(_circuitBreakerPolicy, _timeoutPolicy); + _ = Enum.TryParse(route.QosOptions.TimeoutStrategy, out TimeoutStrategy strategy); + var timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromMilliseconds(route.QosOptions.TimeoutValue), strategy); + CircuitBreaker = new CircuitBreaker(circuitBreakerPolicy, timeoutPolicy); + } + + private const string ObsoleteConstructorMessage = $"Use the constructor {nameof(PollyQoSProvider)}({nameof(DownstreamRoute)} route, {nameof(IOcelotLoggerFactory)} loggerFactory)!"; + + [Obsolete(ObsoleteConstructorMessage)] + public PollyQoSProvider(AsyncCircuitBreakerPolicy circuitBreakerPolicy, AsyncTimeoutPolicy timeoutPolicy, IOcelotLogger logger) + { + throw new NotSupportedException(ObsoleteConstructorMessage); } - public CircuitBreaker CircuitBreaker { get; } + public CircuitBreaker CircuitBreaker { get; } + + private static string GetRouteName(DownstreamRoute route) + => string.IsNullOrWhiteSpace(route.ServiceName) + ? route.UpstreamPathTemplate?.Template ?? route.DownstreamPathTemplate?.Value ?? string.Empty + : route.ServiceName; } } diff --git a/src/Ocelot.Provider.Polly/Usings.cs b/src/Ocelot.Provider.Polly/Usings.cs index 0cc4c6d4f..fb5c12dc6 100644 --- a/src/Ocelot.Provider.Polly/Usings.cs +++ b/src/Ocelot.Provider.Polly/Usings.cs @@ -1,12 +1,10 @@ // Default Microsoft.NET.Sdk namespaces global using System; global using System.Collections.Generic; -global using System.IO; global using System.Linq; global using System.Net.Http; global using System.Threading; global using System.Threading.Tasks; // Project extra global namespaces -global using Ocelot; global using Polly; diff --git a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj index 0c9fe46f7..8f0638fc5 100644 --- a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj +++ b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj @@ -88,7 +88,7 @@ - + diff --git a/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs b/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs index e04d56411..537712e01 100644 --- a/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs +++ b/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs @@ -1,7 +1,9 @@ using Ocelot.Configuration.Builder; using Ocelot.Logging; using Ocelot.Provider.Polly; - +using Polly.CircuitBreaker; +using Polly.Timeout; + namespace Ocelot.UnitTests.Polly { public class PollyQoSProviderTests @@ -18,7 +20,11 @@ public void Should_build() .Build(); var factory = new Mock(); var pollyQoSProvider = new PollyQoSProvider(route, factory.Object); - pollyQoSProvider.CircuitBreaker.ShouldNotBeNull(); + var policies = pollyQoSProvider.CircuitBreaker.ShouldNotBeNull() + .Policies.ShouldNotBeNull(); + policies.Length.ShouldBeGreaterThan(0); + policies.ShouldContain(p => p is AsyncCircuitBreakerPolicy); + policies.ShouldContain(p => p is AsyncTimeoutPolicy); } } } From e92b103644a573a73e3f720b134b9baac86cf145 Mon Sep 17 00:00:00 2001 From: Mohsen Rajabi Date: Thu, 19 Oct 2023 14:10:13 +0330 Subject: [PATCH 02/12] Cache by header value: a new Header property in (File)CacheOptions configuration of a route (#1172) @EngRajabi, Mohsen Rajabi (7): add header to file cache option fix private set fix 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 --- docs/features/caching.rst | 50 ++++---- src/Ocelot/Cache/CacheKeyGenerator.cs | 39 +++++-- src/Ocelot/Cache/ICacheKeyGenerator.cs | 5 +- .../Cache/Middleware/OutputCacheMiddleware.cs | 2 +- src/Ocelot/Configuration/CacheOptions.cs | 9 +- .../Configuration/Creator/RoutesCreator.cs | 2 +- .../Configuration/File/FileCacheOptions.cs | 1 + .../Request/Middleware/DownstreamRequest.cs | 15 ++- .../Middleware/RequestIdMiddleware.cs | 4 +- .../Cache/CacheKeyGeneratorTests.cs | 108 ++++++++++++++++-- .../Cache/OutputCacheMiddlewareTests.cs | 2 +- .../OutputCacheMiddlewareRealCacheTests.cs | 4 +- 12 files changed, 187 insertions(+), 54 deletions(-) diff --git a/docs/features/caching.rst b/docs/features/caching.rst index 0a1cac37b..51bc7b8aa 100644 --- a/docs/features/caching.rst +++ b/docs/features/caching.rst @@ -3,46 +3,56 @@ Caching Ocelot supports some very rudimentary caching at the moment provider by the `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 `_: - ``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 `_ 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 `_ 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, MyCache>()`` +If you want to add your own caching method, implement the following interfaces and register them in DI e.g. -``IOcelotCache`` this is for output caching. +.. code-block:: csharp -``IOcelotCache`` this is for caching the file configuration if you are calling something remote to get your config such as Consul. + services.AddSingleton, 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`` this is for output caching. +* ``IOcelotCache`` 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 `_, `Memcached `_ etc. diff --git a/src/Ocelot/Cache/CacheKeyGenerator.cs b/src/Ocelot/Cache/CacheKeyGenerator.cs index e6ae88213..0b25212aa 100644 --- a/src/Ocelot/Cache/CacheKeyGenerator.cs +++ b/src/Ocelot/Cache/CacheKeyGenerator.cs @@ -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 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()); } } } diff --git a/src/Ocelot/Cache/ICacheKeyGenerator.cs b/src/Ocelot/Cache/ICacheKeyGenerator.cs index 32a1f989e..d2ccb0ef5 100644 --- a/src/Ocelot/Cache/ICacheKeyGenerator.cs +++ b/src/Ocelot/Cache/ICacheKeyGenerator.cs @@ -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 GenerateRequestCacheKey(DownstreamRequest downstreamRequest, DownstreamRoute downstreamRoute); } } diff --git a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs index 0117c4536..134708881 100644 --- a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs +++ b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs @@ -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."); diff --git a/src/Ocelot/Configuration/CacheOptions.cs b/src/Ocelot/Configuration/CacheOptions.cs index d509b38e9..5b8d2987b 100644 --- a/src/Ocelot/Configuration/CacheOptions.cs +++ b/src/Ocelot/Configuration/CacheOptions.cs @@ -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; } } } diff --git a/src/Ocelot/Configuration/Creator/RoutesCreator.cs b/src/Ocelot/Configuration/Creator/RoutesCreator.cs index 100c65116..8c1f1de63 100644 --- a/src/Ocelot/Configuration/Creator/RoutesCreator.cs +++ b/src/Ocelot/Configuration/Creator/RoutesCreator.cs @@ -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) diff --git a/src/Ocelot/Configuration/File/FileCacheOptions.cs b/src/Ocelot/Configuration/File/FileCacheOptions.cs index b5168b3c0..e1438bbab 100644 --- a/src/Ocelot/Configuration/File/FileCacheOptions.cs +++ b/src/Ocelot/Configuration/File/FileCacheOptions.cs @@ -4,5 +4,6 @@ public class FileCacheOptions { public int TtlSeconds { get; set; } public string Region { get; set; } + public string Header { get; set; } } } diff --git a/src/Ocelot/Request/Middleware/DownstreamRequest.cs b/src/Ocelot/Request/Middleware/DownstreamRequest.cs index 18b8641e1..03e4134fc 100644 --- a/src/Ocelot/Request/Middleware/DownstreamRequest.cs +++ b/src/Ocelot/Request/Middleware/DownstreamRequest.cs @@ -6,6 +6,8 @@ public class DownstreamRequest { private readonly HttpRequestMessage _request; + public DownstreamRequest() { } + public DownstreamRequest(HttpRequestMessage request) { _request = request; @@ -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; } @@ -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 ReadContentAsync() => HasContent + ? _request.Content.ReadAsStringAsync() + : Task.FromResult(string.Empty); public HttpRequestMessage ToHttpRequestMessage() { diff --git a/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs b/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs index f2fc77ca1..795fd89f9 100644 --- a/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs +++ b/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs @@ -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); } diff --git a/test/Ocelot.UnitTests/Cache/CacheKeyGeneratorTests.cs b/test/Ocelot.UnitTests/Cache/CacheKeyGeneratorTests.cs index f71e684af..c0572cfb2 100644 --- a/test/Ocelot.UnitTests/Cache/CacheKeyGeneratorTests.cs +++ b/test/Ocelot.UnitTests/Cache/CacheKeyGeneratorTests.cs @@ -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; + + 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.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() { } + } } diff --git a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs index b494cbe7f..6ad948232 100644 --- a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs @@ -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 { "Get" }) .Build()) .WithUpstreamHttpMethod(new List { "Get" }) diff --git a/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs b/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs index bb02d98b0..9a588002e 100644 --- a/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs +++ b/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs @@ -25,7 +25,7 @@ public OutputCacheMiddlewareRealCacheTests() { _httpContext = new DefaultHttpContext(); _loggerFactory = new Mock(); - _logger = new Mock(); + _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); var cacheManagerOutputCache = CacheFactory.Build("OcelotOutputCache", x => { @@ -77,7 +77,7 @@ private void GivenTheDownstreamRouteIs() { var route = new DownstreamRouteBuilder() .WithIsCached(true) - .WithCacheOptions(new CacheOptions(100, "kanken")) + .WithCacheOptions(new CacheOptions(100, "kanken", null)) .WithUpstreamHttpMethod(new List { "Get" }) .Build(); From ab3d8e63f55d592e25ce763879ef4681b768b11b Mon Sep 17 00:00:00 2001 From: jlukawska <56401969+jlukawska@users.noreply.github.com> Date: Fri, 27 Oct 2023 21:20:54 +0200 Subject: [PATCH 03/12] Find available port in integration tests (#1173) * use random ports in integration tests * Remove and Sort Usings * Code modern look * code review * code review: fix messages * code review: fix some messages * code review: Use simple `using` statement * Add Ocelot.Testing project --------- Co-authored-by: raman-m --- Ocelot.sln | 7 +++ test/Ocelot.AcceptanceTests/AggregateTests.cs | 28 +++++----- .../AuthenticationTests.cs | 12 ++--- .../AuthorizationTests.cs | 12 ++--- .../ButterflyTracingTests.cs | 10 ++-- test/Ocelot.AcceptanceTests/CachingTests.cs | 8 +-- .../CancelRequestTests.cs | 2 +- .../CaseSensitiveRoutingTests.cs | 12 ++--- .../ClaimsToDownstreamPathTests.cs | 4 +- .../ClaimsToHeadersForwardingTests.cs | 4 +- .../ClaimsToQueryStringForwardingTests.cs | 6 +-- .../ClientRateLimitTests.cs | 6 +-- .../ConfigurationInConsulTests.cs | 4 +- .../ConsulConfigurationInConsulTests.cs | 16 +++--- .../ConsulWebSocketTests.cs | 6 +-- test/Ocelot.AcceptanceTests/ContentTests.cs | 6 +-- .../CustomMiddlewareTests.cs | 16 +++--- .../EurekaServiceDiscoveryTests.cs | 2 +- test/Ocelot.AcceptanceTests/GzipTests.cs | 2 +- test/Ocelot.AcceptanceTests/HeaderTests.cs | 18 +++---- .../HttpClientCachingTests.cs | 4 +- .../HttpDelegatingHandlersTests.cs | 8 +-- test/Ocelot.AcceptanceTests/HttpTests.cs | 10 ++-- .../LoadBalancerTests.cs | 12 ++--- test/Ocelot.AcceptanceTests/MethodTests.cs | 6 +-- .../Ocelot.AcceptanceTests.csproj | 1 + .../OpenTracingTests.cs | 10 ++-- test/Ocelot.AcceptanceTests/PollyQoSTests.cs | 10 ++-- .../RandomPortFinder.cs | 40 -------------- .../ReasonPhraseTests.cs | 2 +- test/Ocelot.AcceptanceTests/RequestIdTests.cs | 8 +-- .../ResponseCodeTests.cs | 2 +- .../ReturnsErrorTests.cs | 4 +- test/Ocelot.AcceptanceTests/RoutingTests.cs | 52 +++++++++---------- .../RoutingWithQueryStringTests.cs | 14 ++--- .../ServiceDiscoveryTests.cs | 34 ++++++------ .../ServiceFabricTests.cs | 8 +-- test/Ocelot.AcceptanceTests/SslTests.cs | 4 +- test/Ocelot.AcceptanceTests/StartupTests.cs | 2 +- .../StickySessionsTests.cs | 12 ++--- .../TwoDownstreamServicesTests.cs | 6 +-- .../UpstreamHostTests.cs | 10 ++-- test/Ocelot.AcceptanceTests/Usings.cs | 1 + test/Ocelot.AcceptanceTests/WebSocketTests.cs | 6 +-- .../AdministrationTests.cs | 38 +++++++------- test/Ocelot.IntegrationTests/HeaderTests.cs | 23 ++++---- .../Ocelot.IntegrationTests.csproj | 1 + .../ThreadSafeHeadersTests.cs | 18 ++++--- test/Ocelot.IntegrationTests/Usings.cs | 1 + test/Ocelot.Testing/Ocelot.Testing.csproj | 9 ++++ test/Ocelot.Testing/PortFinder.cs | 49 +++++++++++++++++ 51 files changed, 310 insertions(+), 276 deletions(-) delete mode 100644 test/Ocelot.AcceptanceTests/RandomPortFinder.cs create mode 100644 test/Ocelot.Testing/Ocelot.Testing.csproj create mode 100644 test/Ocelot.Testing/PortFinder.cs diff --git a/Ocelot.sln b/Ocelot.sln index afcec039d..8e6de833c 100644 --- a/Ocelot.sln +++ b/Ocelot.sln @@ -92,6 +92,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDisco EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ocelot.Samples.ServiceDiscovery.DownstreamService", "samples\OcelotServiceDiscovery\DownstreamService\Ocelot.Samples.ServiceDiscovery.DownstreamService.csproj", "{E2AC741A-4120-4D59-B5E4-16382ED45E8D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Testing", "test\Ocelot.Testing\Ocelot.Testing.csproj", "{AE6BCCBD-0687-4C58-B30F-4ABBC6422087}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -202,6 +204,10 @@ Global {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Debug|Any CPU.Build.0 = Debug|Any CPU {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Release|Any CPU.ActiveCfg = Release|Any CPU {E2AC741A-4120-4D59-B5E4-16382ED45E8D}.Release|Any CPU.Build.0 = Release|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -241,6 +247,7 @@ Global {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7} = {8FA0CBA0-0338-48EB-B37F-83CA5022237C} {D37209EA-C13E-42AE-B851-A8604F1FCD0E} = {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7} {E2AC741A-4120-4D59-B5E4-16382ED45E8D} = {25C30AAA-12DD-4BA5-A53F-9271E54EBAB7} + {AE6BCCBD-0687-4C58-B30F-4ABBC6422087} = {5B401523-36DA-4491-B73A-7590A26E420B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {21476EFF-778A-4F97-8A56-D1AF1CEC0C48} diff --git a/test/Ocelot.AcceptanceTests/AggregateTests.cs b/test/Ocelot.AcceptanceTests/AggregateTests.cs index 5810bf457..758b4295d 100644 --- a/test/Ocelot.AcceptanceTests/AggregateTests.cs +++ b/test/Ocelot.AcceptanceTests/AggregateTests.cs @@ -21,7 +21,7 @@ public AggregateTests() [Fact] public void should_fix_issue_597() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -132,9 +132,9 @@ public void should_fix_issue_597() [Fact] public void should_return_response_200_with_advanced_aggregate_configs() { - var port1 = RandomPortFinder.GetRandomPort(); - var port2 = RandomPortFinder.GetRandomPort(); - var port3 = RandomPortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var port3 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -229,8 +229,8 @@ public void should_return_response_200_with_advanced_aggregate_configs() [Fact] public void should_return_response_200_with_simple_url_user_defined_aggregate() { - var port1 = RandomPortFinder.GetRandomPort(); - var port2 = RandomPortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -300,8 +300,8 @@ public void should_return_response_200_with_simple_url_user_defined_aggregate() [Fact] public void should_return_response_200_with_simple_url() { - var port1 = RandomPortFinder.GetRandomPort(); - var port2 = RandomPortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -370,8 +370,8 @@ public void should_return_response_200_with_simple_url() [Fact] public void should_return_response_200_with_simple_url_one_service_404() { - var port1 = RandomPortFinder.GetRandomPort(); - var port2 = RandomPortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -440,8 +440,8 @@ public void should_return_response_200_with_simple_url_one_service_404() [Fact] public void should_return_response_200_with_simple_url_both_service_404() { - var port1 = RandomPortFinder.GetRandomPort(); - var port2 = RandomPortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -510,8 +510,8 @@ public void should_return_response_200_with_simple_url_both_service_404() [Fact] public void should_be_thread_safe() { - var port1 = RandomPortFinder.GetRandomPort(); - var port2 = RandomPortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List diff --git a/test/Ocelot.AcceptanceTests/AuthenticationTests.cs b/test/Ocelot.AcceptanceTests/AuthenticationTests.cs index 8316d926c..29656437c 100644 --- a/test/Ocelot.AcceptanceTests/AuthenticationTests.cs +++ b/test/Ocelot.AcceptanceTests/AuthenticationTests.cs @@ -26,7 +26,7 @@ public AuthenticationTests() { _serviceHandler = new ServiceHandler(); _steps = new Steps(); - var identityServerPort = RandomPortFinder.GetRandomPort(); + var identityServerPort = PortFinder.GetRandomPort(); _identityServerRootUrl = $"http://localhost:{identityServerPort}"; _options = o => { @@ -41,7 +41,7 @@ public AuthenticationTests() [Fact] public void should_return_401_using_identity_server_access_token() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -82,7 +82,7 @@ public void should_return_401_using_identity_server_access_token() [Fact] public void should_return_response_200_using_identity_server() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -125,7 +125,7 @@ public void should_return_response_200_using_identity_server() [Fact] public void should_return_response_401_using_identity_server_with_token_requested_for_other_api() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -167,7 +167,7 @@ public void should_return_response_401_using_identity_server_with_token_requeste [Fact] public void should_return_201_using_identity_server_access_token() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -210,7 +210,7 @@ public void should_return_201_using_identity_server_access_token() [Fact] public void should_return_201_using_identity_server_reference_token() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/AuthorizationTests.cs b/test/Ocelot.AcceptanceTests/AuthorizationTests.cs index f44add4ae..c15ebade7 100644 --- a/test/Ocelot.AcceptanceTests/AuthorizationTests.cs +++ b/test/Ocelot.AcceptanceTests/AuthorizationTests.cs @@ -22,7 +22,7 @@ public AuthorizationTests() { _serviceHandler = new ServiceHandler(); _steps = new Steps(); - var identityServerPort = RandomPortFinder.GetRandomPort(); + var identityServerPort = PortFinder.GetRandomPort(); _identityServerRootUrl = $"http://localhost:{identityServerPort}"; _options = o => { @@ -37,7 +37,7 @@ public AuthorizationTests() [Fact] public void should_return_response_200_authorizing_route() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -97,7 +97,7 @@ public void should_return_response_200_authorizing_route() [Fact] public void should_return_response_403_authorizing_route() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -155,7 +155,7 @@ public void should_return_response_403_authorizing_route() [Fact] public void should_return_response_200_using_identity_server_with_allowed_scope() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -198,7 +198,7 @@ public void should_return_response_200_using_identity_server_with_allowed_scope( [Fact] public void should_return_response_403_using_identity_server_with_scope_not_allowed() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -241,7 +241,7 @@ public void should_return_response_403_using_identity_server_with_scope_not_allo [Fact] public void should_fix_issue_240() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs b/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs index c3a4f19e9..a90560f02 100644 --- a/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs +++ b/test/Ocelot.AcceptanceTests/ButterflyTracingTests.cs @@ -27,8 +27,8 @@ public ButterflyTracingTests(ITestOutputHelper output) [Fact] public void should_forward_tracing_information_from_ocelot_and_downstream_services() { - var port1 = RandomPortFinder.GetRandomPort(); - var port2 = RandomPortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -74,7 +74,7 @@ public void should_forward_tracing_information_from_ocelot_and_downstream_servic }, }; - var butterflyPort = RandomPortFinder.GetRandomPort(); + var butterflyPort = PortFinder.GetRandomPort(); var butterflyUrl = $"http://localhost:{butterflyPort}"; this.Given(x => GivenFakeButterfly(butterflyUrl)) @@ -100,7 +100,7 @@ public void should_forward_tracing_information_from_ocelot_and_downstream_servic [Fact] public void should_return_tracing_header() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -132,7 +132,7 @@ public void should_return_tracing_header() }, }; - var butterflyPort = RandomPortFinder.GetRandomPort(); + var butterflyPort = PortFinder.GetRandomPort(); var butterflyUrl = $"http://localhost:{butterflyPort}"; this.Given(x => GivenFakeButterfly(butterflyUrl)) diff --git a/test/Ocelot.AcceptanceTests/CachingTests.cs b/test/Ocelot.AcceptanceTests/CachingTests.cs index 95f6f5166..60f8f957e 100644 --- a/test/Ocelot.AcceptanceTests/CachingTests.cs +++ b/test/Ocelot.AcceptanceTests/CachingTests.cs @@ -17,7 +17,7 @@ public CachingTests() [Fact] public void should_return_cached_response() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -62,7 +62,7 @@ public void should_return_cached_response() [Fact] public void should_return_cached_response_with_expires_header() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -108,7 +108,7 @@ public void should_return_cached_response_with_expires_header() [Fact] public void should_return_cached_response_when_using_jsonserialized_cache() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -152,7 +152,7 @@ public void should_return_cached_response_when_using_jsonserialized_cache() [Fact] public void should_not_return_cached_response_as_ttl_expires() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/CancelRequestTests.cs b/test/Ocelot.AcceptanceTests/CancelRequestTests.cs index 983d98fc0..984160591 100644 --- a/test/Ocelot.AcceptanceTests/CancelRequestTests.cs +++ b/test/Ocelot.AcceptanceTests/CancelRequestTests.cs @@ -33,7 +33,7 @@ public CancelRequestTests() [Fact] public void Should_abort_service_work_when_cancelling_the_request() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs b/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs index 64759aed2..202fe2d27 100644 --- a/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/CaseSensitiveRoutingTests.cs @@ -17,7 +17,7 @@ public CaseSensitiveRoutingTests() [Fact] public void should_return_response_200_when_global_ignore_case_sensitivity_set() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -52,7 +52,7 @@ public void should_return_response_200_when_global_ignore_case_sensitivity_set() [Fact] public void should_return_response_200_when_route_ignore_case_sensitivity_set() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -88,7 +88,7 @@ public void should_return_response_200_when_route_ignore_case_sensitivity_set() [Fact] public void should_return_response_404_when_route_respect_case_sensitivity_set() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -124,7 +124,7 @@ public void should_return_response_404_when_route_respect_case_sensitivity_set() [Fact] public void should_return_response_200_when_route_respect_case_sensitivity_set() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -160,7 +160,7 @@ public void should_return_response_200_when_route_respect_case_sensitivity_set() [Fact] public void should_return_response_404_when_global_respect_case_sensitivity_set() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -196,7 +196,7 @@ public void should_return_response_404_when_global_respect_case_sensitivity_set( [Fact] public void should_return_response_200_when_global_respect_case_sensitivity_set() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs index 445239c4b..3fed134a4 100644 --- a/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs +++ b/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs @@ -20,7 +20,7 @@ public class ClaimsToDownstreamPathTests : IDisposable public ClaimsToDownstreamPathTests() { - var identityServerPort = RandomPortFinder.GetRandomPort(); + var identityServerPort = PortFinder.GetRandomPort(); _identityServerRootUrl = $"http://localhost:{identityServerPort}"; _steps = new Steps(); _options = o => @@ -43,7 +43,7 @@ public void should_return_200_and_change_downstream_path() SubjectId = "registered|1231231", }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs index b8f2ece35..00aa955c8 100644 --- a/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs +++ b/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs @@ -24,7 +24,7 @@ public ClaimsToHeadersForwardingTests() { _serviceHandler = new ServiceHandler(); _steps = new Steps(); - var identityServerPort = RandomPortFinder.GetRandomPort(); + var identityServerPort = PortFinder.GetRandomPort(); _identityServerRootUrl = $"http://localhost:{identityServerPort}"; _options = o => { @@ -51,7 +51,7 @@ public void should_return_response_200_and_foward_claim_as_header() }, }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs index 969faebb0..e8325d612 100644 --- a/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs +++ b/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs @@ -22,7 +22,7 @@ public class ClaimsToQueryStringForwardingTests : IDisposable public ClaimsToQueryStringForwardingTests() { _steps = new Steps(); - var identityServerPort = RandomPortFinder.GetRandomPort(); + var identityServerPort = PortFinder.GetRandomPort(); _identityServerRootUrl = $"http://localhost:{identityServerPort}"; _options = o => { @@ -49,7 +49,7 @@ public void should_return_response_200_and_foward_claim_as_query_string() }, }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -115,7 +115,7 @@ public void should_return_response_200_and_foward_claim_as_query_string_and_pres }, }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs index 3b04ca849..dad9af3dc 100644 --- a/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs +++ b/test/Ocelot.AcceptanceTests/ClientRateLimitTests.cs @@ -18,7 +18,7 @@ public ClientRateLimitTests() [Fact] public void should_call_withratelimiting() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -78,7 +78,7 @@ public void should_call_withratelimiting() [Fact] public void should_wait_for_period_timespan_to_elapse_before_making_next_request() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -145,7 +145,7 @@ public void should_wait_for_period_timespan_to_elapse_before_making_next_request [Fact] public void should_call_middleware_withWhitelistClient() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs b/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs index ef35a8efc..c889f353e 100644 --- a/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs +++ b/test/Ocelot.AcceptanceTests/ConfigurationInConsulTests.cs @@ -27,8 +27,8 @@ public ConfigurationInConsulTests() [Fact] public void should_return_response_200_with_simple_url_when_using_jsonserialized_cache() { - var consulPort = RandomPortFinder.GetRandomPort(); - var servicePort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs b/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs index f138675b9..9c4d9bf70 100644 --- a/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs +++ b/test/Ocelot.AcceptanceTests/ConsulConfigurationInConsulTests.cs @@ -26,8 +26,8 @@ public ConsulConfigurationInConsulTests() [Fact] public void should_return_response_200_with_simple_url() { - var consulPort = RandomPortFinder.GetRandomPort(); - var servicePort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -75,8 +75,8 @@ public void should_return_response_200_with_simple_url() [Fact] public void should_load_configuration_out_of_consul() { - var consulPort = RandomPortFinder.GetRandomPort(); - var servicePort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -138,8 +138,8 @@ public void should_load_configuration_out_of_consul() [Fact] public void should_load_configuration_out_of_consul_if_it_is_changed() { - var consulPort = RandomPortFinder.GetRandomPort(); - var servicePort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -234,9 +234,9 @@ public void should_load_configuration_out_of_consul_if_it_is_changed() [Fact] public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes_and_rate_limit() { - var consulPort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); const string serviceName = "web"; - var downstreamServicePort = RandomPortFinder.GetRandomPort(); + var downstreamServicePort = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; var serviceEntryOne = new ServiceEntry diff --git a/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs b/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs index d03d7e6b9..13579f47e 100644 --- a/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs +++ b/test/Ocelot.AcceptanceTests/ConsulWebSocketTests.cs @@ -28,14 +28,14 @@ public ConsulWebSocketTests() [Fact] public void ShouldProxyWebsocketInputToDownstreamServiceAndUseServiceDiscoveryAndLoadBalancer() { - var downstreamPort = RandomPortFinder.GetRandomPort(); + var downstreamPort = PortFinder.GetRandomPort(); var downstreamHost = "localhost"; - var secondDownstreamPort = RandomPortFinder.GetRandomPort(); + var secondDownstreamPort = PortFinder.GetRandomPort(); var secondDownstreamHost = "localhost"; var serviceName = "websockets"; - var consulPort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; var serviceEntryOne = new ServiceEntry { diff --git a/test/Ocelot.AcceptanceTests/ContentTests.cs b/test/Ocelot.AcceptanceTests/ContentTests.cs index a976ddb70..ba95dfd22 100644 --- a/test/Ocelot.AcceptanceTests/ContentTests.cs +++ b/test/Ocelot.AcceptanceTests/ContentTests.cs @@ -20,7 +20,7 @@ public ContentTests() [Fact] public void should_not_add_content_type_or_content_length_headers() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -58,7 +58,7 @@ public void should_not_add_content_type_or_content_length_headers() [Fact] public void should_add_content_type_and_content_length_headers() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -99,7 +99,7 @@ public void should_add_content_type_and_content_length_headers() [Fact] public void should_add_default_content_type_header() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs b/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs index 5f9bc2926..33246f147 100644 --- a/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs +++ b/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs @@ -30,7 +30,7 @@ public void should_call_pre_query_string_builder_middleware() }, }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var fileConfiguration = new FileConfiguration { @@ -75,7 +75,7 @@ public void should_call_authorization_middleware() }, }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var fileConfiguration = new FileConfiguration { @@ -120,7 +120,7 @@ public void should_call_authentication_middleware() }, }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var fileConfiguration = new FileConfiguration { @@ -165,7 +165,7 @@ public void should_call_pre_error_middleware() }, }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var fileConfiguration = new FileConfiguration { @@ -210,7 +210,7 @@ public void should_call_pre_authorization_middleware() }, }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var fileConfiguration = new FileConfiguration { @@ -255,7 +255,7 @@ public void should_call_pre_http_authentication_middleware() }, }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var fileConfiguration = new FileConfiguration { @@ -301,7 +301,7 @@ public void should_not_throw_when_pipeline_terminates_early() }), }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var fileConfiguration = new FileConfiguration { @@ -350,7 +350,7 @@ public void should_fix_issue_237() return Task.CompletedTask; }; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var fileConfiguration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs index 8def02990..67be0cfb7 100644 --- a/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs @@ -26,7 +26,7 @@ public void should_use_eureka_service_discovery_and_make_request(bool dotnetRunn Environment.SetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER", dotnetRunningInContainer.ToString()); var eurekaPort = 8761; var serviceName = "product"; - var downstreamServicePort = RandomPortFinder.GetRandomPort(); + var downstreamServicePort = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; var fakeEurekaServiceDiscoveryUrl = $"http://localhost:{eurekaPort}"; diff --git a/test/Ocelot.AcceptanceTests/GzipTests.cs b/test/Ocelot.AcceptanceTests/GzipTests.cs index ca10deedc..753ce70a9 100644 --- a/test/Ocelot.AcceptanceTests/GzipTests.cs +++ b/test/Ocelot.AcceptanceTests/GzipTests.cs @@ -18,7 +18,7 @@ public GzipTests() [Fact] public void should_return_response_200_with_simple_url() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/HeaderTests.cs b/test/Ocelot.AcceptanceTests/HeaderTests.cs index 5a28e1753..a58fc9aa6 100644 --- a/test/Ocelot.AcceptanceTests/HeaderTests.cs +++ b/test/Ocelot.AcceptanceTests/HeaderTests.cs @@ -18,7 +18,7 @@ public HeaderTests() [Fact] public void should_transform_upstream_header() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -59,7 +59,7 @@ public void should_transform_upstream_header() [Fact] public void should_transform_downstream_header() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -99,7 +99,7 @@ public void should_transform_downstream_header() [Fact] public void should_fix_issue_190() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -143,7 +143,7 @@ public void should_fix_issue_190() [Fact] public void should_fix_issue_205() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -187,7 +187,7 @@ public void should_fix_issue_205() [Fact] public void should_fix_issue_417() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -235,7 +235,7 @@ public void should_fix_issue_417() [Fact] public void request_should_reuse_cookies_with_cookie_container() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -278,7 +278,7 @@ public void request_should_reuse_cookies_with_cookie_container() [Fact] public void request_should_have_own_cookies_no_cookie_container() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -321,7 +321,7 @@ public void request_should_have_own_cookies_no_cookie_container() [Fact] public void issue_474_should_not_put_spaces_in_header() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -358,7 +358,7 @@ public void issue_474_should_not_put_spaces_in_header() [Fact] public void issue_474_should_put_spaces_in_header() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/HttpClientCachingTests.cs b/test/Ocelot.AcceptanceTests/HttpClientCachingTests.cs index 39a42ad71..eca147ac8 100644 --- a/test/Ocelot.AcceptanceTests/HttpClientCachingTests.cs +++ b/test/Ocelot.AcceptanceTests/HttpClientCachingTests.cs @@ -20,7 +20,7 @@ public HttpClientCachingTests() [Fact] public void should_cache_one_http_client_same_re_route() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -62,7 +62,7 @@ public void should_cache_one_http_client_same_re_route() [Fact] public void should_cache_two_http_client_different_re_route() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs b/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs index 51ef4051c..ac475a75a 100644 --- a/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs +++ b/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs @@ -18,7 +18,7 @@ public HttpDelegatingHandlersTests() [Fact] public void should_call_re_route_ordered_specific_handlers() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -60,7 +60,7 @@ public void should_call_re_route_ordered_specific_handlers() [Fact] public void should_call_global_di_handlers() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -97,7 +97,7 @@ public void should_call_global_di_handlers() [Fact] public void should_call_global_di_handlers_multiple_times() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -150,7 +150,7 @@ public void should_call_global_di_handlers_multiple_times() [Fact] public void should_call_global_di_handlers_with_dependency() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/HttpTests.cs b/test/Ocelot.AcceptanceTests/HttpTests.cs index 9a80a4519..532bd6276 100644 --- a/test/Ocelot.AcceptanceTests/HttpTests.cs +++ b/test/Ocelot.AcceptanceTests/HttpTests.cs @@ -18,7 +18,7 @@ public HttpTests() [Fact] public void should_return_response_200_when_using_http_one() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -55,7 +55,7 @@ public void should_return_response_200_when_using_http_one() [Fact] public void should_return_response_200_when_using_http_one_point_one() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -92,7 +92,7 @@ public void should_return_response_200_when_using_http_one_point_one() [Fact] public void should_return_response_200_when_using_http_two_point_zero() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -134,7 +134,7 @@ public void should_return_response_200_when_using_http_two_point_zero() [Fact] public void should_return_response_502_when_using_http_one_to_talk_to_server_running_http_two() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -176,7 +176,7 @@ public void should_return_response_502_when_using_http_one_to_talk_to_server_run [Fact] public void should_return_response_200_when_using_http_two_to_talk_to_server_running_http_one_point_one() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs b/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs index dfc4c3486..f882868d6 100644 --- a/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs +++ b/test/Ocelot.AcceptanceTests/LoadBalancerTests.cs @@ -25,8 +25,8 @@ public LoadBalancerTests() [Fact] public void should_load_balance_request_with_least_connection() { - var portOne = RandomPortFinder.GetRandomPort(); - var portTwo = RandomPortFinder.GetRandomPort(); + var portOne = PortFinder.GetRandomPort(); + var portTwo = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{portOne}"; var downstreamServiceTwoUrl = $"http://localhost:{portTwo}"; @@ -73,8 +73,8 @@ public void should_load_balance_request_with_least_connection() [Fact] public void should_load_balance_request_with_round_robin() { - var downstreamPortOne = RandomPortFinder.GetRandomPort(); - var downstreamPortTwo = RandomPortFinder.GetRandomPort(); + var downstreamPortOne = PortFinder.GetRandomPort(); + var downstreamPortTwo = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; @@ -120,8 +120,8 @@ public void should_load_balance_request_with_round_robin() [Fact] public void should_load_balance_request_with_custom_load_balancer() { - var downstreamPortOne = RandomPortFinder.GetRandomPort(); - var downstreamPortTwo = RandomPortFinder.GetRandomPort(); + var downstreamPortOne = PortFinder.GetRandomPort(); + var downstreamPortTwo = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; diff --git a/test/Ocelot.AcceptanceTests/MethodTests.cs b/test/Ocelot.AcceptanceTests/MethodTests.cs index 6ea2977e4..c71fdd473 100644 --- a/test/Ocelot.AcceptanceTests/MethodTests.cs +++ b/test/Ocelot.AcceptanceTests/MethodTests.cs @@ -17,7 +17,7 @@ public MethodTests() [Fact] public void should_return_response_200_when_get_converted_to_post() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -53,7 +53,7 @@ public void should_return_response_200_when_get_converted_to_post() [Fact] public void should_return_response_200_when_get_converted_to_post_with_content() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -93,7 +93,7 @@ public void should_return_response_200_when_get_converted_to_post_with_content() [Fact] public void should_return_response_200_when_get_converted_to_get_with_content() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj index c54810e36..b998eba40 100644 --- a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +++ b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj @@ -39,6 +39,7 @@ + diff --git a/test/Ocelot.AcceptanceTests/OpenTracingTests.cs b/test/Ocelot.AcceptanceTests/OpenTracingTests.cs index 74468ae21..1f467afba 100644 --- a/test/Ocelot.AcceptanceTests/OpenTracingTests.cs +++ b/test/Ocelot.AcceptanceTests/OpenTracingTests.cs @@ -30,8 +30,8 @@ public OpenTracingTests(ITestOutputHelper output) [Fact] public void should_forward_tracing_information_from_ocelot_and_downstream_services() { - var port1 = RandomPortFinder.GetRandomPort(); - var port2 = RandomPortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -77,7 +77,7 @@ public void should_forward_tracing_information_from_ocelot_and_downstream_servic }, }; - var tracingPort = RandomPortFinder.GetRandomPort(); + var tracingPort = PortFinder.GetRandomPort(); var tracingUrl = $"http://localhost:{tracingPort}"; var fakeTracer = new FakeTracer(); @@ -100,7 +100,7 @@ public void should_forward_tracing_information_from_ocelot_and_downstream_servic [Fact] public void should_return_tracing_header() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -132,7 +132,7 @@ public void should_return_tracing_header() }, }; - var butterflyPort = RandomPortFinder.GetRandomPort(); + var butterflyPort = PortFinder.GetRandomPort(); var butterflyUrl = $"http://localhost:{butterflyPort}"; diff --git a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs index c182721ad..a8893a86e 100644 --- a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs +++ b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs @@ -18,7 +18,7 @@ public PollyQoSTests() [Fact] public void Should_not_timeout() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -59,7 +59,7 @@ public void Should_not_timeout() [Fact] public void Should_timeout() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -99,7 +99,7 @@ public void Should_timeout() [Fact] public void Should_open_circuit_breaker_then_close() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -151,8 +151,8 @@ public void Should_open_circuit_breaker_then_close() [Fact] public void Open_circuit_should_not_effect_different_route() { - var port1 = RandomPortFinder.GetRandomPort(); - var port2 = RandomPortFinder.GetRandomPort(); + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/RandomPortFinder.cs b/test/Ocelot.AcceptanceTests/RandomPortFinder.cs deleted file mode 100644 index 24fdce68c..000000000 --- a/test/Ocelot.AcceptanceTests/RandomPortFinder.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Concurrent; -using System.Net.Sockets; - -namespace Ocelot.AcceptanceTests -{ - public static class RandomPortFinder - { - private const int EndPortRange = 45000; - private static int _currentPort = 20000; - private static readonly object LockObj = new(); - private static readonly ConcurrentBag UsedPorts = new(); - - public static int GetRandomPort() - { - lock (LockObj) - { - if (_currentPort > EndPortRange) - { - throw new Exception("Cannot find available port to bind to."); - } - - var port = UsePort(_currentPort); - _currentPort += 1; - return port; - } - } - - private static int UsePort(int randomPort) - { - UsedPorts.Add(randomPort); - - var ipe = new IPEndPoint(IPAddress.Loopback, randomPort); - - using var socket = new Socket(ipe.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - socket.Bind(ipe); - socket.Close(); - return randomPort; - } - } -} diff --git a/test/Ocelot.AcceptanceTests/ReasonPhraseTests.cs b/test/Ocelot.AcceptanceTests/ReasonPhraseTests.cs index 20f537777..7bbae6c3e 100644 --- a/test/Ocelot.AcceptanceTests/ReasonPhraseTests.cs +++ b/test/Ocelot.AcceptanceTests/ReasonPhraseTests.cs @@ -18,7 +18,7 @@ public ReasonPhraseTests() [Fact] public void should_return_reason_phrase() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/RequestIdTests.cs b/test/Ocelot.AcceptanceTests/RequestIdTests.cs index 2e7987846..c339fa2ff 100644 --- a/test/Ocelot.AcceptanceTests/RequestIdTests.cs +++ b/test/Ocelot.AcceptanceTests/RequestIdTests.cs @@ -16,7 +16,7 @@ public RequestIdTests() [Fact] public void should_use_default_request_id_and_forward() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -52,7 +52,7 @@ public void should_use_default_request_id_and_forward() [Fact] public void should_use_request_id_and_forward() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -89,7 +89,7 @@ public void should_use_request_id_and_forward() [Fact] public void should_use_global_request_id_and_forward() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -130,7 +130,7 @@ public void should_use_global_request_id_and_forward() [Fact] public void should_use_global_request_id_create_and_forward() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs b/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs index 58964fe0f..c72c1fb62 100644 --- a/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs +++ b/test/Ocelot.AcceptanceTests/ResponseCodeTests.cs @@ -16,7 +16,7 @@ public ResponseCodeTests() [Fact] public void ShouldReturnResponse304WhenServiceReturns304() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs b/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs index 926a0e36f..c60ae243d 100644 --- a/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs +++ b/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs @@ -48,7 +48,7 @@ public void should_return_bad_gateway_error_if_downstream_service_doesnt_respond [Fact] public void should_return_internal_server_error_if_downstream_service_returns_internal_server_error() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -83,7 +83,7 @@ public void should_return_internal_server_error_if_downstream_service_returns_in [Fact] public void should_log_warning_if_downstream_service_returns_internal_server_error() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/RoutingTests.cs b/test/Ocelot.AcceptanceTests/RoutingTests.cs index 7b808ad0a..243d973d8 100644 --- a/test/Ocelot.AcceptanceTests/RoutingTests.cs +++ b/test/Ocelot.AcceptanceTests/RoutingTests.cs @@ -18,7 +18,7 @@ public RoutingTests() [Fact] public void should_not_match_forward_slash_in_pattern_before_next_forward_slash() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -63,7 +63,7 @@ public void should_return_response_404_when_no_configuration_at_all() [Fact] public void should_return_response_200_with_forward_slash_and_placeholder_only() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -99,7 +99,7 @@ public void should_return_response_200_with_forward_slash_and_placeholder_only() [Fact] public void should_return_response_200_favouring_forward_slash_with_path_route() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -150,7 +150,7 @@ public void should_return_response_200_favouring_forward_slash_with_path_route() [Fact] public void should_return_response_200_favouring_forward_slash() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -200,7 +200,7 @@ public void should_return_response_200_favouring_forward_slash() [Fact] public void should_return_response_200_favouring_forward_slash_route_because_it_is_first() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -251,7 +251,7 @@ public void should_return_response_200_favouring_forward_slash_route_because_it_ [Fact] public void should_return_response_200_with_nothing_and_placeholder_only() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -287,7 +287,7 @@ public void should_return_response_200_with_nothing_and_placeholder_only() [Fact] public void should_return_response_200_with_simple_url() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -323,7 +323,7 @@ public void should_return_response_200_with_simple_url() [Fact] public void Bug() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -376,7 +376,7 @@ public void Bug() [Fact] public void should_return_response_200_when_path_missing_forward_slash_as_first_char() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -412,7 +412,7 @@ public void should_return_response_200_when_path_missing_forward_slash_as_first_ [Fact] public void should_return_response_200_when_host_has_trailing_slash() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -448,7 +448,7 @@ public void should_return_response_200_when_host_has_trailing_slash() [Fact] public void should_return_ok_when_upstream_url_ends_with_forward_slash_but_template_does_not() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -484,7 +484,7 @@ public void should_return_ok_when_upstream_url_ends_with_forward_slash_but_templ [Fact] public void should_return_not_found_when_upstream_url_ends_with_forward_slash_but_template_does_not() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -519,7 +519,7 @@ public void should_return_not_found_when_upstream_url_ends_with_forward_slash_bu [Fact] public void should_return_not_found() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -554,7 +554,7 @@ public void should_return_not_found() [Fact] public void should_return_response_200_with_complex_url() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -590,7 +590,7 @@ public void should_return_response_200_with_complex_url() [Fact] public void should_return_response_200_with_complex_url_that_starts_with_placeholder() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -626,7 +626,7 @@ public void should_return_response_200_with_complex_url_that_starts_with_placeho [Fact] public void should_not_add_trailing_slash_to_downstream_url() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -661,7 +661,7 @@ public void should_not_add_trailing_slash_to_downstream_url() [Fact] public void should_return_response_201_with_simple_url() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -697,7 +697,7 @@ public void should_return_response_201_with_simple_url() [Fact] public void should_return_response_201_with_complex_query_string() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -733,7 +733,7 @@ public void should_return_response_201_with_complex_query_string() [Fact] public void should_return_response_200_with_placeholder_for_final_url_path() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -769,7 +769,7 @@ public void should_return_response_200_with_placeholder_for_final_url_path() [Fact] public void should_return_response_201_with_simple_url_and_multiple_upstream_http_method() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -805,7 +805,7 @@ public void should_return_response_201_with_simple_url_and_multiple_upstream_htt [Fact] public void should_return_response_200_with_simple_url_and_any_upstream_http_method() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -841,7 +841,7 @@ public void should_return_response_200_with_simple_url_and_any_upstream_http_met [Fact] public void should_return_404_when_calling_upstream_route_with_no_matching_downstream_re_route_github_issue_134() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -893,7 +893,7 @@ public void should_return_404_when_calling_upstream_route_with_no_matching_downs [Fact] public void should_not_set_trailing_slash_on_url_template() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -930,7 +930,7 @@ public void should_not_set_trailing_slash_on_url_template() [Fact] public void should_use_priority() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -982,7 +982,7 @@ public void should_use_priority() [Fact] public void should_match_multiple_paths_with_catch_all() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -1018,7 +1018,7 @@ public void should_match_multiple_paths_with_catch_all() [Fact] public void should_fix_issue_271() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs b/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs index 9fdff5378..ae7e04679 100644 --- a/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs +++ b/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs @@ -19,7 +19,7 @@ public void should_return_response_200_with_query_string_template() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -57,7 +57,7 @@ public void should_return_response_200_with_odata_query_string() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -95,7 +95,7 @@ public void should_return_response_200_with_query_string_upstream_template() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -133,7 +133,7 @@ public void should_return_response_404_with_query_string_upstream_template_no_qu { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -170,7 +170,7 @@ public void should_return_response_404_with_query_string_upstream_template_diffe { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -207,7 +207,7 @@ public void should_return_response_200_with_query_string_upstream_template_multi { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -248,7 +248,7 @@ public void should_copy_query_string_to_downstream_path_issue_1288() var idValue = "3"; var queryName = idName + "1"; var queryValue = "2" + idValue + "12"; - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs index 1814db1e8..1628b4b03 100644 --- a/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceDiscoveryTests.cs @@ -26,9 +26,9 @@ public ServiceDiscoveryTests() [Fact] public void should_use_consul_service_discovery_and_load_balance_request() { - var consulPort = RandomPortFinder.GetRandomPort(); - var servicePort1 = RandomPortFinder.GetRandomPort(); - var servicePort2 = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); + var servicePort1 = PortFinder.GetRandomPort(); + var servicePort2 = PortFinder.GetRandomPort(); var serviceName = "product"; var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; @@ -96,8 +96,8 @@ public void should_use_consul_service_discovery_and_load_balance_request() [Fact] public void should_handle_request_to_consul_for_downstream_service_and_make_request() { - var consulPort = RandomPortFinder.GetRandomPort(); - var servicePort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); const string serviceName = "web"; var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; @@ -152,9 +152,9 @@ public void should_handle_request_to_consul_for_downstream_service_and_make_requ [Fact] public void should_handle_request_to_consul_for_downstream_service_and_make_request_no_re_routes() { - var consulPort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); const string serviceName = "web"; - var downstreamServicePort = RandomPortFinder.GetRandomPort(); + var downstreamServicePort = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; var serviceEntryOne = new ServiceEntry @@ -203,10 +203,10 @@ public void should_handle_request_to_consul_for_downstream_service_and_make_requ [Fact] public void should_use_consul_service_discovery_and_load_balance_request_no_re_routes() { - var consulPort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); var serviceName = "product"; - var serviceOnePort = RandomPortFinder.GetRandomPort(); - var serviceTwoPort = RandomPortFinder.GetRandomPort(); + var serviceOnePort = PortFinder.GetRandomPort(); + var serviceTwoPort = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{serviceOnePort}"; var downstreamServiceTwoUrl = $"http://localhost:{serviceTwoPort}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; @@ -264,9 +264,9 @@ public void should_use_consul_service_discovery_and_load_balance_request_no_re_r public void should_use_token_to_make_request_to_consul() { var token = "abctoken"; - var consulPort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); var serviceName = "web"; - var servicePort = RandomPortFinder.GetRandomPort(); + var servicePort = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{servicePort}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; var serviceEntryOne = new ServiceEntry @@ -322,10 +322,10 @@ public void should_use_token_to_make_request_to_consul() [Fact] public void should_send_request_to_service_after_it_becomes_available_in_consul() { - var consulPort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); var serviceName = "product"; - var servicePort1 = RandomPortFinder.GetRandomPort(); - var servicePort2 = RandomPortFinder.GetRandomPort(); + var servicePort1 = PortFinder.GetRandomPort(); + var servicePort2 = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; @@ -401,9 +401,9 @@ public void should_send_request_to_service_after_it_becomes_available_in_consul( [Fact] public void should_handle_request_to_poll_consul_for_downstream_service_and_make_request() { - var consulPort = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); const string serviceName = "web"; - var downstreamServicePort = RandomPortFinder.GetRandomPort(); + var downstreamServicePort = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamServicePort}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; var serviceEntryOne = new ServiceEntry diff --git a/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs b/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs index 69361d8d7..87d511a33 100644 --- a/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs +++ b/test/Ocelot.AcceptanceTests/ServiceFabricTests.cs @@ -18,7 +18,7 @@ public ServiceFabricTests() [Fact] public void should_fix_issue_555() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -56,7 +56,7 @@ public void should_fix_issue_555() [Fact] public void should_support_service_fabric_naming_and_dns_service_stateless_and_guest() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -94,7 +94,7 @@ public void should_support_service_fabric_naming_and_dns_service_stateless_and_g [Fact] public void should_support_service_fabric_naming_and_dns_service_statefull_and_actors() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -132,7 +132,7 @@ public void should_support_service_fabric_naming_and_dns_service_statefull_and_a [Fact] public void should_support_placeholder_in_service_fabric_service_name() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/SslTests.cs b/test/Ocelot.AcceptanceTests/SslTests.cs index f37f1b430..649285f12 100644 --- a/test/Ocelot.AcceptanceTests/SslTests.cs +++ b/test/Ocelot.AcceptanceTests/SslTests.cs @@ -18,7 +18,7 @@ public SslTests() [Fact] public void should_dangerous_accept_any_server_certificate_validator() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -55,7 +55,7 @@ public void should_dangerous_accept_any_server_certificate_validator() [Fact] public void should_not_dangerous_accept_any_server_certificate_validator() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/StartupTests.cs b/test/Ocelot.AcceptanceTests/StartupTests.cs index afd10e3cc..04672354e 100644 --- a/test/Ocelot.AcceptanceTests/StartupTests.cs +++ b/test/Ocelot.AcceptanceTests/StartupTests.cs @@ -20,7 +20,7 @@ public StartupTests() [Fact] public void should_not_try_and_write_to_disk_on_startup_when_not_using_admin_api() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/StickySessionsTests.cs b/test/Ocelot.AcceptanceTests/StickySessionsTests.cs index ad10984fe..6592ec83b 100644 --- a/test/Ocelot.AcceptanceTests/StickySessionsTests.cs +++ b/test/Ocelot.AcceptanceTests/StickySessionsTests.cs @@ -20,8 +20,8 @@ public StickySessionsTests() [Fact] public void should_use_same_downstream_host() { - var downstreamPortOne = RandomPortFinder.GetRandomPort(); - var downstreamPortTwo = RandomPortFinder.GetRandomPort(); + var downstreamPortOne = PortFinder.GetRandomPort(); + var downstreamPortTwo = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; @@ -71,8 +71,8 @@ public void should_use_same_downstream_host() [Fact] public void should_use_different_downstream_host_for_different_re_route() { - var downstreamPortOne = RandomPortFinder.GetRandomPort(); - var downstreamPortTwo = RandomPortFinder.GetRandomPort(); + var downstreamPortOne = PortFinder.GetRandomPort(); + var downstreamPortTwo = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; @@ -149,8 +149,8 @@ public void should_use_different_downstream_host_for_different_re_route() [Fact] public void should_use_same_downstream_host_for_different_re_route() { - var downstreamPortOne = RandomPortFinder.GetRandomPort(); - var downstreamPortTwo = RandomPortFinder.GetRandomPort(); + var downstreamPortOne = PortFinder.GetRandomPort(); + var downstreamPortTwo = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{downstreamPortOne}"; var downstreamServiceTwoUrl = $"http://localhost:{downstreamPortTwo}"; diff --git a/test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs b/test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs index 30b3d7235..001e33b71 100644 --- a/test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs +++ b/test/Ocelot.AcceptanceTests/TwoDownstreamServicesTests.cs @@ -23,9 +23,9 @@ public TwoDownstreamServicesTests() [Fact] public void should_fix_issue_194() { - var consulPort = RandomPortFinder.GetRandomPort(); - var servicePort1 = RandomPortFinder.GetRandomPort(); - var servicePort2 = RandomPortFinder.GetRandomPort(); + var consulPort = PortFinder.GetRandomPort(); + var servicePort1 = PortFinder.GetRandomPort(); + var servicePort2 = PortFinder.GetRandomPort(); var downstreamServiceOneUrl = $"http://localhost:{servicePort1}"; var downstreamServiceTwoUrl = $"http://localhost:{servicePort2}"; var fakeConsulServiceDiscoveryUrl = $"http://localhost:{consulPort}"; diff --git a/test/Ocelot.AcceptanceTests/UpstreamHostTests.cs b/test/Ocelot.AcceptanceTests/UpstreamHostTests.cs index 2d649e1ce..054ff5c77 100644 --- a/test/Ocelot.AcceptanceTests/UpstreamHostTests.cs +++ b/test/Ocelot.AcceptanceTests/UpstreamHostTests.cs @@ -18,7 +18,7 @@ public UpstreamHostTests() [Fact] public void should_return_response_200_with_simple_url_and_hosts_match() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -55,7 +55,7 @@ public void should_return_response_200_with_simple_url_and_hosts_match() [Fact] public void should_return_response_200_with_simple_url_and_hosts_match_multiple_re_routes() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -108,7 +108,7 @@ public void should_return_response_200_with_simple_url_and_hosts_match_multiple_ [Fact] public void should_return_response_200_with_simple_url_and_hosts_match_multiple_re_routes_reversed() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -161,7 +161,7 @@ public void should_return_response_200_with_simple_url_and_hosts_match_multiple_ [Fact] public void should_return_response_200_with_simple_url_and_hosts_match_multiple_re_routes_reversed_with_no_host_first() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { @@ -213,7 +213,7 @@ public void should_return_response_200_with_simple_url_and_hosts_match_multiple_ [Fact] public void should_return_response_404_with_simple_url_and_hosts_dont_match() { - var port = RandomPortFinder.GetRandomPort(); + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { diff --git a/test/Ocelot.AcceptanceTests/Usings.cs b/test/Ocelot.AcceptanceTests/Usings.cs index 6eb9a3d7b..9648f6280 100644 --- a/test/Ocelot.AcceptanceTests/Usings.cs +++ b/test/Ocelot.AcceptanceTests/Usings.cs @@ -10,6 +10,7 @@ // Project extra global namespaces global using Moq; global using Ocelot; +global using Ocelot.Testing; global using Shouldly; global using System.Net; global using TestStack.BDDfy; diff --git a/test/Ocelot.AcceptanceTests/WebSocketTests.cs b/test/Ocelot.AcceptanceTests/WebSocketTests.cs index 0ede6baea..ad9e7ec68 100644 --- a/test/Ocelot.AcceptanceTests/WebSocketTests.cs +++ b/test/Ocelot.AcceptanceTests/WebSocketTests.cs @@ -23,7 +23,7 @@ public WebSocketTests() [Fact] public void ShouldProxyWebsocketInputToDownstreamService() { - var downstreamPort = RandomPortFinder.GetRandomPort(); + var downstreamPort = PortFinder.GetRandomPort(); var downstreamHost = "localhost"; var config = new FileConfiguration @@ -58,9 +58,9 @@ public void ShouldProxyWebsocketInputToDownstreamService() [Fact] public void ShouldProxyWebsocketInputToDownstreamServiceAndUseLoadBalancer() { - var downstreamPort = RandomPortFinder.GetRandomPort(); + var downstreamPort = PortFinder.GetRandomPort(); var downstreamHost = "localhost"; - var secondDownstreamPort = RandomPortFinder.GetRandomPort(); + var secondDownstreamPort = PortFinder.GetRandomPort(); var secondDownstreamHost = "localhost"; var config = new FileConfiguration diff --git a/test/Ocelot.IntegrationTests/AdministrationTests.cs b/test/Ocelot.IntegrationTests/AdministrationTests.cs index 374634598..34298a03a 100644 --- a/test/Ocelot.IntegrationTests/AdministrationTests.cs +++ b/test/Ocelot.IntegrationTests/AdministrationTests.cs @@ -75,7 +75,8 @@ public void Should_return_response_200_with_call_re_routes_controller() public void Should_return_response_200_with_call_re_routes_controller_using_base_url_added_in_file_config() { _httpClient = new HttpClient(); - _ocelotBaseUrl = "http://localhost:5011"; + var port = PortFinder.GetRandomPort(); + _ocelotBaseUrl = $"http://localhost:{port}"; _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); var configuration = new FileConfiguration @@ -122,12 +123,13 @@ public void Should_return_OK_status_and_multiline_indented_json_response_with_js public void Should_be_able_to_use_token_from_ocelot_a_on_ocelot_b() { var configuration = new FileConfiguration(); + var port = PortFinder.GetRandomPort(); this.Given(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenIdentityServerSigningEnvironmentalVariablesAreSet()) .And(x => GivenOcelotIsRunning()) .And(x => GivenIHaveAnOcelotToken("/administration")) - .And(x => GivenAnotherOcelotIsRunning("http://localhost:5017")) + .And(x => GivenAnotherOcelotIsRunning($"http://localhost:{port}")) .When(x => WhenIGetUrlOnTheSecondOcelot("/administration/configuration")) .Then(x => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); @@ -356,8 +358,8 @@ private static void ThenTheConfigurationIsSavedCorrectly(FileConfiguration expec [Fact] public void Should_get_file_configuration_edit_and_post_updated_version_redirecting_route() { - var fooPort = 47689; - var barPort = 27654; + var fooPort = PortFinder.GetRandomPort(); + var barPort = PortFinder.GetRandomPort(); var initialConfiguration = new FileConfiguration { @@ -490,7 +492,8 @@ public void Should_return_response_200_with_call_re_routes_controller_when_using { var configuration = new FileConfiguration(); - var identityServerRootUrl = "http://localhost:5123"; + var port = PortFinder.GetRandomPort(); + var identityServerRootUrl = $"http://localhost:{port}"; Action options = o => { @@ -525,13 +528,11 @@ private void GivenIHaveAToken(string url) }; var content = new FormUrlEncodedContent(formData); - using (var httpClient = new HttpClient()) - { - var response = httpClient.PostAsync($"{url}/connect/token", content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - } + using var httpClient = new HttpClient(); + var response = httpClient.PostAsync($"{url}/connect/token", content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); } private void GivenThereIsAnIdentityServerOn(string url, string apiName) @@ -593,11 +594,9 @@ private void GivenThereIsAnIdentityServerOn(string url, string apiName) _identityServerBuilder.Start(); - using (var httpClient = new HttpClient()) - { - var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").Result; - response.EnsureSuccessStatusCode(); - } + using var httpClient = new HttpClient(); + var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").Result; + response.EnsureSuccessStatusCode(); } private void GivenAnotherOcelotIsRunning(string baseUrl) @@ -851,7 +850,7 @@ private static void GivenThereIsAConfiguration(FileConfiguration fileConfigurati File.WriteAllText(configurationPath, jsonConfiguration); - var text = File.ReadAllText(configurationPath); + _ = File.ReadAllText(configurationPath); configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; @@ -862,7 +861,7 @@ private static void GivenThereIsAConfiguration(FileConfiguration fileConfigurati File.WriteAllText(configurationPath, jsonConfiguration); - text = File.ReadAllText(configurationPath); + _ = File.ReadAllText(configurationPath); } private void WhenIGetUrlOnTheApiGateway(string url) @@ -901,6 +900,7 @@ public void Dispose() _builder?.Dispose(); _httpClient?.Dispose(); _identityServerBuilder?.Dispose(); + GC.SuppressFinalize(this); } private void GivenThereIsAFooServiceRunningOn(string baseUrl) diff --git a/test/Ocelot.IntegrationTests/HeaderTests.cs b/test/Ocelot.IntegrationTests/HeaderTests.cs index 928b6180a..9072f3189 100644 --- a/test/Ocelot.IntegrationTests/HeaderTests.cs +++ b/test/Ocelot.IntegrationTests/HeaderTests.cs @@ -24,13 +24,15 @@ public class HeaderTests : IDisposable public HeaderTests() { _httpClient = new HttpClient(); - _ocelotBaseUrl = "http://localhost:5010"; + var port = PortFinder.GetRandomPort(); + _ocelotBaseUrl = $"http://localhost:{port}"; _httpClient.BaseAddress = new Uri(_ocelotBaseUrl); } [Fact] - public void should_pass_remote_ip_address_if_as_x_forwarded_for_header() + public void Should_pass_remote_ip_address_if_as_x_forwarded_for_header() { + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -44,7 +46,7 @@ public void should_pass_remote_ip_address_if_as_x_forwarded_for_header() new() { Host = "localhost", - Port = 6773, + Port = port, }, }, UpstreamPathTemplate = "/", @@ -61,7 +63,7 @@ public void should_pass_remote_ip_address_if_as_x_forwarded_for_header() }, }; - this.Given(x => GivenThereIsAServiceRunningOn("http://localhost:6773", 200, "X-Forwarded-For")) + this.Given(x => GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "X-Forwarded-For")) .And(x => GivenThereIsAConfiguration(configuration)) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGateway("/")) @@ -136,8 +138,8 @@ private static void GivenThereIsAConfiguration(FileConfiguration fileConfigurati } File.WriteAllText(configurationPath, jsonConfiguration); - - var text = File.ReadAllText(configurationPath); + + _ = File.ReadAllText(configurationPath); configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; @@ -146,9 +148,9 @@ private static void GivenThereIsAConfiguration(FileConfiguration fileConfigurati File.Delete(configurationPath); } - File.WriteAllText(configurationPath, jsonConfiguration); - - text = File.ReadAllText(configurationPath); + File.WriteAllText(configurationPath, jsonConfiguration); + + _ = File.ReadAllText(configurationPath); } private async Task WhenIGetUrlOnTheApiGateway(string url) @@ -178,7 +180,8 @@ public void Dispose() { _builder?.Dispose(); _httpClient?.Dispose(); - _downstreamBuilder?.Dispose(); + _downstreamBuilder?.Dispose(); + GC.SuppressFinalize(this); } } } diff --git a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj index c78949e19..024f6a30e 100644 --- a/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj +++ b/test/Ocelot.IntegrationTests/Ocelot.IntegrationTests.csproj @@ -33,6 +33,7 @@ + diff --git a/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs b/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs index 1e6a6f127..48eac3686 100644 --- a/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs +++ b/test/Ocelot.IntegrationTests/ThreadSafeHeadersTests.cs @@ -30,8 +30,9 @@ public ThreadSafeHeadersTests() } [Fact] - public void should_return_same_response_for_each_different_header_under_load_to_downsteam_service() - { + public void Should_return_same_response_for_each_different_header_under_load_to_downsteam_service() + { + var port = PortFinder.GetRandomPort(); var configuration = new FileConfiguration { Routes = new List @@ -45,7 +46,7 @@ public void should_return_same_response_for_each_different_header_under_load_to_ new() { Host = "localhost", - Port = 51611, + Port = port, }, }, UpstreamPathTemplate = "/", @@ -55,7 +56,7 @@ public void should_return_same_response_for_each_different_header_under_load_to_ }; this.Given(x => GivenThereIsAConfiguration(configuration)) - .And(x => GivenThereIsAServiceRunningOn("http://localhost:51611")) + .And(x => GivenThereIsAServiceRunningOn($"http://localhost:{port}")) .And(x => GivenOcelotIsRunning()) .When(x => WhenIGetUrlOnTheApiGatewayMultipleTimesWithDifferentHeaderValues("/", 300)) .Then(x => ThenTheSameHeaderValuesAreReturnedByTheDownstreamService()) @@ -126,8 +127,8 @@ private static void GivenThereIsAConfiguration(FileConfiguration fileConfigurati } File.WriteAllText(configurationPath, jsonConfiguration); - - var text = File.ReadAllText(configurationPath); + + _ = File.ReadAllText(configurationPath); configurationPath = $"{AppContext.BaseDirectory}/ocelot.json"; @@ -138,7 +139,7 @@ private static void GivenThereIsAConfiguration(FileConfiguration fileConfigurati File.WriteAllText(configurationPath, jsonConfiguration); - text = File.ReadAllText(configurationPath); + _ = File.ReadAllText(configurationPath); } private void WhenIGetUrlOnTheApiGatewayMultipleTimesWithDifferentHeaderValues(string url, int times) @@ -178,7 +179,8 @@ public void Dispose() { _builder?.Dispose(); _httpClient?.Dispose(); - _downstreamBuilder?.Dispose(); + _downstreamBuilder?.Dispose(); + GC.SuppressFinalize(this); } private class ThreadSafeHeadersTestResult diff --git a/test/Ocelot.IntegrationTests/Usings.cs b/test/Ocelot.IntegrationTests/Usings.cs index ec84c76eb..504bb7314 100644 --- a/test/Ocelot.IntegrationTests/Usings.cs +++ b/test/Ocelot.IntegrationTests/Usings.cs @@ -9,6 +9,7 @@ // Project extra global namespaces global using Ocelot; +global using Ocelot.Testing; global using Shouldly; global using TestStack.BDDfy; global using Xunit; diff --git a/test/Ocelot.Testing/Ocelot.Testing.csproj b/test/Ocelot.Testing/Ocelot.Testing.csproj new file mode 100644 index 000000000..cfadb03dd --- /dev/null +++ b/test/Ocelot.Testing/Ocelot.Testing.csproj @@ -0,0 +1,9 @@ + + + + net7.0 + enable + enable + + + diff --git a/test/Ocelot.Testing/PortFinder.cs b/test/Ocelot.Testing/PortFinder.cs new file mode 100644 index 000000000..6eb6b64d4 --- /dev/null +++ b/test/Ocelot.Testing/PortFinder.cs @@ -0,0 +1,49 @@ +using System.Collections.Concurrent; +using System.Net; +using System.Net.Sockets; + +namespace Ocelot.Testing; + +public static class PortFinder +{ + private const int EndPortRange = 45000; + private static int CurrentPort = 20000; + private static readonly object LockObj = new(); + private static readonly ConcurrentBag UsedPorts = new(); + + /// + /// Gets a pseudo-random port from the range [, ]. + /// + /// New allocated port for testing scenario. + /// Critical situation where available ports range has been exceeded. + public static int GetRandomPort() + { + lock (LockObj) + { + if (CurrentPort > EndPortRange) + { + throw new ExceedingPortRangeException(); + } + + return UsePort(CurrentPort++); + } + } + + private static int UsePort(int port) + { + UsedPorts.Add(port); + + var ipe = new IPEndPoint(IPAddress.Loopback, port); + + using var socket = new Socket(ipe.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + socket.Bind(ipe); + socket.Close(); + return port; + } +} + +public class ExceedingPortRangeException : Exception +{ + public ExceedingPortRangeException() + : base("Cannot find available port to bind to!") { } +} From ae43f32fe32b322487623e4b1a22299c3d91bb73 Mon Sep 17 00:00:00 2001 From: Stjepan Date: Wed, 1 Nov 2023 12:41:13 +0100 Subject: [PATCH 04/12] #952 #1174 Merge query strings without duplicate values (#1182) * Fix issue #952 and #1174 * Fix compiling errors * Fix warnings * Fix errors * Remove and Sort Usings * CA1845 Use span-based 'string.Concat' and 'AsSpan' instead of 'Substring'. Use 'AsSpan' with 'string.Concat' * IDE1006 Naming rule violation: These words must begin with upper case characters: {should_*}. Fix name violation * Add namespace * Fix build errors * Test class should match the name of tested class * Simplify too long class names, and they should match * Move to the parent folder which was empty * Fix warnings * Process dictionaries using LINQ to Objects approach * Fix code review issues from @RaynaldM * Remove tiny private helper with one reference * Fix warning & messages * Define theory instead of 2 facts * Add unit test for issue #952 * Add additional unit test for #952 to keep param * Add tests for issue #1174 * Remove unnecessary parameter * Copy routing.rst from released version * Refactor the middleware body for query params * Update routing.rst: Describe query string user scenarios --------- Co-authored-by: Stjepan Majdak Co-authored-by: raman-m --- docs/features/routing.rst | 374 +++++++++--------- .../DependencyInjection/OcelotBuilder.cs | 4 +- ...s => DownstreamPathPlaceholderReplacer.cs} | 4 +- .../IDownstreamPathPlaceholderReplacer.cs | 2 +- .../DownstreamUrlCreatorMiddleware.cs | 94 +++-- .../RoutingWithQueryStringTests.cs | 128 ++++-- ...DownstreamPathPlaceholderReplacerTests.cs} | 10 +- .../DownstreamUrlCreatorMiddlewareTests.cs | 147 +++++-- 8 files changed, 479 insertions(+), 284 deletions(-) rename src/Ocelot/DownstreamUrlCreator/{UrlTemplateReplacer/DownstreamTemplatePathPlaceholderReplacer.cs => DownstreamPathPlaceholderReplacer.cs} (82%) rename src/Ocelot/DownstreamUrlCreator/{UrlTemplateReplacer => }/IDownstreamPathPlaceholderReplacer.cs (83%) rename test/Ocelot.UnitTests/DownstreamUrlCreator/{UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs => DownstreamPathPlaceholderReplacerTests.cs} (96%) diff --git a/docs/features/routing.rst b/docs/features/routing.rst index f1bd79375..e2ca941bf 100644 --- a/docs/features/routing.rst +++ b/docs/features/routing.rst @@ -9,239 +9,270 @@ In order to get anything working in Ocelot you need to set up a Route in the con .. code-block:: json - { - "Routes": [ - ] - } + { + "Routes": [] + } -To configure a Route you need to add one to the Routes json array. +To configure a Route you need to add one to the Routes JSON array. .. code-block:: json - { - "DownstreamPathTemplate": "/api/posts/{postId}", - "DownstreamScheme": "https", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 80, - } - ], - "UpstreamPathTemplate": "/posts/{postId}", - "UpstreamHttpMethod": [ "Put", "Delete" ] - } + { + "UpstreamHttpMethod": [ "Put", "Delete" ], + "UpstreamPathTemplate": "/posts/{postId}", + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { "Host": "localhost", "Port": 80 } + ] + } -The DownstreamPathTemplate, DownstreamScheme and DownstreamHostAndPorts define the URL that a request will be forwarded to. +The **DownstreamPathTemplate**, **DownstreamScheme** and **DownstreamHostAndPorts** define the URL that a request will be forwarded to. -DownstreamHostAndPorts is a collection that defines the host and port of any downstream services that you wish to forward requests to. Usually this will just contain a single entry but sometimes you might want to load balance requests to your downstream services and Ocelot allows you add more than one entry and then select a load balancer. +The **DownstreamHostAndPorts** property is a collection that defines the host and port of any downstream services that you wish to forward requests to. +Usually this will just contain a single entry, but sometimes you might want to load balance requests to your downstream services and Ocelot allows you add more than one entry and then select a load balancer. -The UpstreamPathTemplate is the URL that Ocelot will use to identify which DownstreamPathTemplate to use for a given request. The UpstreamHttpMethod is used so Ocelot can distinguish between requests with different HTTP verbs to the same URL. You can set a specific list of HTTP Methods or set an empty list to allow any of them. +The **UpstreamPathTemplate** property is the URL that Ocelot will use to identify which **DownstreamPathTemplate** to use for a given request. +The **UpstreamHttpMethod** is used so Ocelot can distinguish between requests with different HTTP verbs to the same URL. +You can set a specific list of HTTP methods or set an empty list to allow any of them. -In Ocelot you can add placeholders for variables to your Templates in the form of {something}. The placeholder variable needs to be present in both the DownstreamPathTemplate and UpstreamPathTemplate properties. When it is Ocelot will attempt to substitute the value in the UpstreamPathTemplate placeholder into the DownstreamPathTemplate for each request Ocelot processes. +Placeholders +------------ -You can also do a catch all type of Route e.g. +In Ocelot you can add placeholders for variables to your Templates in the form of ``{something}``. +The placeholder variable needs to be present in both the **DownstreamPathTemplate** and **UpstreamPathTemplate** properties. +When it is Ocelot will attempt to substitute the value in the **UpstreamPathTemplate** placeholder into the **DownstreamPathTemplate** for each request Ocelot processes. -.. code-block:: json +You can also do a `Catch All <#catch-all>`_ type of Route e.g. - { - "DownstreamPathTemplate": "/api/{everything}", - "DownstreamScheme": "https", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 80, - } - ], - "UpstreamPathTemplate": "/{everything}", - "UpstreamHttpMethod": [ "Get", "Post" ] - } +.. code-block:: json -This will forward any path + query string combinations to the downstream service after the path /api. + { + "UpstreamHttpMethod": [ "Get", "Post" ], + "UpstreamPathTemplate": "/{everything}", + "DownstreamPathTemplate": "/api/{everything}", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { "Host": "localhost", "Port": 80 } + ] + } +This will forward any path + query string combinations to the downstream service after the path ``/api``. -The default ReRouting configuration is case insensitive! +**Note**, the default Routing configuration is case insensitive! -In order to change this you can specify on a per Route basis the following setting. +In order to change this you can specify on a per Route basis the following setting: .. code-block:: json - "RouteIsCaseSensitive": true + "RouteIsCaseSensitive": true -This means that when Ocelot tries to match the incoming upstream url with an upstream template the -evaluation will be case sensitive. +This means that when Ocelot tries to match the incoming upstream URL with an upstream template the evaluation will be case sensitive. Catch All -^^^^^^^^^ +--------- -Ocelot's routing also supports a catch all style routing where the user can specify that they want to match all traffic. +Ocelot's routing also supports a *Catch All* style routing where the user can specify that they want to match all traffic. -If you set up your config like below, all requests will be proxied straight through. The placeholder {url} name is not significant, any name will work. +If you set up your config like below, all requests will be proxied straight through. +The placeholder ``{url}`` name is not significant, any name will work. .. code-block:: json - { - "DownstreamPathTemplate": "/{url}", - "DownstreamScheme": "https", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 80, - } - ], - "UpstreamPathTemplate": "/{url}", - "UpstreamHttpMethod": [ "Get" ] - } + { + "UpstreamHttpMethod": [ "Get" ], + "UpstreamPathTemplate": "/{url}", + "DownstreamPathTemplate": "/{url}", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { "Host": "localhost", "Port": 80 } + ] + } -The catch all has a lower priority than any other Route. If you also have the Route below in your config then Ocelot would match it before the catch all. +The *Catch All* has a lower priority than any other Route. +If you also have the Route below in your config then Ocelot would match it before the *Catch All*. .. code-block:: json - { - "DownstreamPathTemplate": "/", - "DownstreamScheme": "https", - "DownstreamHostAndPorts": [ - { - "Host": "10.0.10.1", - "Port": 80, - } - ], - "UpstreamPathTemplate": "/", - "UpstreamHttpMethod": [ "Get" ] - } + { + "UpstreamHttpMethod": [ "Get" ], + "UpstreamPathTemplate": "/", + "DownstreamPathTemplate": "/", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { "Host": "10.0.10.1", "Port": 80 } + ] + } Upstream Host -^^^^^^^^^^^^^ +------------- -This feature allows you to have Routes based on the upstream host. This works by looking at the host header the client has used and then using this as part of the information we use to identify a Route. +This feature allows you to have Routes based on the *upstream host*. +This works by looking at the ``Host`` header the client has used and then using this as part of the information we use to identify a Route. -In order to use this feature please add the following to your config. +In order to use this feature please add the following to your config: .. code-block:: json - { - "DownstreamPathTemplate": "/", - "DownstreamScheme": "https", - "DownstreamHostAndPorts": [ - { - "Host": "10.0.10.1", - "Port": 80, - } - ], - "UpstreamPathTemplate": "/", - "UpstreamHttpMethod": [ "Get" ], - "UpstreamHost": "somedomain.com" - } + { + "UpstreamHost": "somedomain.com" + } -The Route above will only be matched when the host header value is somedomain.com. +The Route above will only be matched when the ``Host`` header value is ``somedomain.com``. -If you do not set UpstreamHost on a Route then any host header will match it. This means that if you have two Routes that are the same, apart from the UpstreamHost, where one is null and the other set Ocelot will favour the one that has been set. +If you do not set **UpstreamHost** on a Route then any ``Host`` header will match it. +This means that if you have two Routes that are the same, apart from the **UpstreamHost**, where one is null and the other set Ocelot will favour the one that has been set. -This feature was requested as part of `Issue 216 `_ . +This feature was requested as part of `issue 216 `_. Priority -^^^^^^^^ +-------- -You can define the order you want your Routes to match the Upstream HttpRequest by including a "Priority" property in ocelot.json -See `Issue 270 `_ for reference +You can define the order you want your Routes to match the Upstream ``HttpRequest`` by including a **Priority** property in **ocelot.json**. +See `issue 270 `_ for reference. .. code-block:: json - { - "Priority": 0 - } + { + "Priority": 0 + } -0 is the lowest priority, Ocelot will always use 0 for /{catchAll} Routes and this is still hardcoded. After that you are free to set any priority you wish. +``0`` is the lowest priority, Ocelot will always use ``0`` for ``/{catchAll}`` Routes and this is still hardcoded. +After that you are free to set any priority you wish. e.g. you could have .. code-block:: json - { - "UpstreamPathTemplate": "/goods/{catchAll}" - "Priority": 0 - } + { + "UpstreamPathTemplate": "/goods/{catchAll}", + "Priority": 0 + } -and +and .. code-block:: json - { - "UpstreamPathTemplate": "/goods/delete" - "Priority": 1 - } + { + "UpstreamPathTemplate": "/goods/delete", + "Priority": 1 + } -In the example above if you make a request into Ocelot on /goods/delete Ocelot will match /goods/delete Route. Previously it would have matched /goods/{catchAll} (because this is the first Route in the list!). +In the example above if you make a request into Ocelot on ``/goods/delete``, Ocelot will match ``/goods/delete`` Route. +Previously it would have matched ``/goods/{catchAll}``, because this is the first Route in the list! Dynamic Routing -^^^^^^^^^^^^^^^ +--------------- This feature was requested in `issue 340 `_. -The idea is to enable dynamic routing when using a service discovery provider so you don't have to provide the Route config. See the docs :ref:`service-discovery` if -this sounds interesting to you. +The idea is to enable dynamic routing when using a service discovery provider so you don't have to provide the Route config. +See the docs :doc:`../features/servicediscovery` if this sounds interesting to you. + +Query String Placeholders +------------------------- -Query Strings -^^^^^^^^^^^^^ +In addition to URL path `placeholders <#placeholders>`_ Ocelot is able to forward query string parameters with their processing in the form of ``{something}``. +Also, the query parameter placeholder needs to be present in both the **DownstreamPathTemplate** and **UpstreamPathTemplate** properties. +Placeholder replacement works bi-directionally between path and query strings, with some `restrictions <#restrictions-on-use>`_ on usage. -Ocelot allows you to specify a query string as part of the DownstreamPathTemplate like the example below. +Path to Query String direction +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Ocelot allows you to specify a query string as part of the **DownstreamPathTemplate** like the example below: .. code-block:: json - { - "Routes": [ - { - "DownstreamPathTemplate": "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - "UpstreamPathTemplate": "/api/units/{subscriptionId}/{unitId}/updates", - "UpstreamHttpMethod": [ - "Get" - ], - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 50110 - } - ] - } - ], - "GlobalConfiguration": { - } - } + { + "UpstreamPathTemplate": "/api/units/{subscription}/{unit}/updates", + "DownstreamPathTemplate": "/api/subscriptions/{subscription}/updates?unitId={unit}", + } + +In this example Ocelot will use the value from the ``{unit}`` placeholder in the upstream path template and add it to the downstream request as a query string parameter called ``unitId``! Make sure you name the placeholder differently due to `restrictions <#restrictions-on-use>`_ on usage. + -In this example Ocelot will use the value from the {unitId} in the upstream path template and add it to the downstream request as a query string parameter called unitId! +Query String to Path direction +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Ocelot will also allow you to put query string parameters in the UpstreamPathTemplate so you can match certain queries to certain services. +Ocelot will also allow you to put query string parameters in the **UpstreamPathTemplate** so you can match certain queries to certain services: .. code-block:: json + { + "UpstreamPathTemplate": "/api/subscriptions/{subscriptionId}/updates?unitId={uid}", + "DownstreamPathTemplate": "/api/units/{subscriptionId}/{uid}/updates", + } + +In this example Ocelot will only match requests that have a matching URL path and the query string starts with ``unitId=something``. +You can have other queries after this but you must start with the matching parameter. +Also Ocelot will swap the ``{uid}`` parameter from the query string and use it in the downstream request path. +Note, the best practice is giving different placeholder name than the name of query parameter due to `restrictions <#restrictions-on-use>`_ on usage. + +Catch All Query String +^^^^^^^^^^^^^^^^^^^^^^ + +Ocelot's routing also supports a *Catch All* style routing to forward all query string parameters. +The placeholder ``{everything}`` name does not matter, any name will work. + +.. code-block:: json + + { + "UpstreamPathTemplate": "/contracts?{everything}", + "DownstreamPathTemplate": "/apipath/contracts?{everything}", + } + +This entire query string routing feature is very useful in cases where the query string should not be transformed but rather routed without any changes, +such as OData filters and etc (see issue `1174 `_). + +Restrictions on use +^^^^^^^^^^^^^^^^^^^ + +The query string parameters are ordered and merged to produce the final downstream URL. +This is necessary because the ``DownstreamUrlCreatorMiddleware`` needs to have some control when replacing placeholders and merging duplicate parameters. +So, even if your parameter is presented as the first parameter in the upstream, then in the final downstream URL the said query parameter will have a different position. +But this doesn't seem to break anything in the downstream API. + +Because of parameters merging, special ASP.NET API `model binding `_ +for arrays is not supported if you use array items representation like ``selectedCourses=1050&selectedCourses=2000``. +This query string will be merged as ``selectedCourses=1050`` in downstream URL. So, array data will be lost! +Make sure upstream clients generate correct query string for array models like ``selectedCourses[0]=1050&selectedCourses[1]=2000``. +To understand array model bidings, see `Bind arrays and string values from headers and query strings `_ docs. + +**Warning!** Query string placeholders have naming restrictions due to ``DownstreamUrlCreatorMiddleware`` implementations. +On the other hand, it gives you the flexibility to control whether the parameter is present in the final downstream URL. +Here are two user scenarios. + +* User wants to save the parameter after replacing the placeholder (see issue `473 `_). + To do this you need to use the following template definition: + + .. code-block:: json + { - "Routes": [ - { - "DownstreamPathTemplate": "/api/units/{subscriptionId}/{unitId}/updates", - "UpstreamPathTemplate": "/api/subscriptions/{subscriptionId}/updates?unitId={unitId}", - "UpstreamHttpMethod": [ - "Get" - ], - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 50110 - } - ] - } - ], - "GlobalConfiguration": { - } + "UpstreamPathTemplate": "/path/{serverId}/{action}", + "DownstreamPathTemplate": "/path2/{action}?server={serverId}" } -In this example Ocelot will only match requests that have a matching url path and the query string starts with unitId=something. You can have other queries after this -but you must start with the matching parameter. Also Ocelot will swap the {unitId} parameter from the query string and use it in the downstream request path. + So, ``{serverId}`` placeholder and ``server`` parameter **names are different**! + Finally, the ``server`` parameter is kept. + +* User wants to remove old parameter after replacing placeholder (see issue `952 `_). + To do this you need to use the same names: + + .. code-block:: json + + { + "UpstreamPathTemplate": "/users?userId={userId}", + "DownstreamPathTemplate": "/persons?personId={userId}" + } + + So, both ``{userId}`` placeholder and ``userId`` parameter **names are the same**! + Finally, the ``userId`` parameter is removed. Security Options -^^^^^^^^^^^^^^^^ +---------------- -Ocelot allows you to manage multiple patterns for allowed/blocked IPs using the `IPAddressRange `_ package with `MPL-2.0 License `_. +Ocelot allows you to manage multiple patterns for allowed/blocked IPs using the `IPAddressRange `_ package +with `MPL-2.0 License `_. This feature is designed to allow greater IP management in order to include or exclude a wide IP range via CIDR notation or IP range. The current patterns managed are the following: @@ -253,35 +284,18 @@ The current patterns managed are the following: * CIDR: :code:`192.168.1.0/24` * CIDR for IPv6: :code:`fe80::/10` * The allowed/blocked lists are evaluated during configuration loading -* The *ExcludeAllowedFromBlocked* property is intended to provide the ability to specify a wide range of blocked IP addresses and allow a subrange of IP addresses. +* The **ExcludeAllowedFromBlocked** property is intended to provide the ability to specify a wide range of blocked IP addresses and allow a subrange of IP addresses. Default value: :code:`false` * The absence of a property in **SecurityOptions** is allowed, it takes the default value. .. code-block:: json - { - "Routes": [ - { - "DownstreamPathTemplate": "/api/service/{Id}", - "UpstreamPathTemplate": "/api/internal-service/{Id}/full", - "UpstreamHttpMethod": [ - "Get" - ], - "DownstreamScheme": "http", - "DownstreamHostAndPorts": [ - { - "Host": "localhost", - "Port": 50110 - } - ], - "SecurityOptions": { - "IPBlockedList": [ "192.168.0.0/23" ], - "IPAllowedList": ["192.168.0.15", "192.168.1.15"], - "ExcludeAllowedFromBlocked": true - }, - }, - ], - "GlobalConfiguration": { } + { + "SecurityOptions": { + "IPBlockedList": [ "192.168.0.0/23" ], + "IPAllowedList": ["192.168.0.15", "192.168.1.15"], + "ExcludeAllowedFromBlocked": true } + } -This feature was requested in the `issue 1400 `_. +This feature was requested as part of `issue 1400 `_. diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 159d336ca..0464f2b1c 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -16,7 +16,7 @@ using Ocelot.Configuration.Validator; using Ocelot.DownstreamRouteFinder.Finder; using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; +using Ocelot.DownstreamUrlCreator; using Ocelot.Headers; using Ocelot.Infrastructure; using Ocelot.Infrastructure.Claims.Parser; @@ -103,7 +103,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); + Services.TryAddSingleton(); Services.AddSingleton(); Services.AddSingleton(); Services.TryAddSingleton(); diff --git a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamTemplatePathPlaceholderReplacer.cs b/src/Ocelot/DownstreamUrlCreator/DownstreamPathPlaceholderReplacer.cs similarity index 82% rename from src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamTemplatePathPlaceholderReplacer.cs rename to src/Ocelot/DownstreamUrlCreator/DownstreamPathPlaceholderReplacer.cs index cebbfda76..11bbae38b 100644 --- a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/DownstreamTemplatePathPlaceholderReplacer.cs +++ b/src/Ocelot/DownstreamUrlCreator/DownstreamPathPlaceholderReplacer.cs @@ -2,9 +2,9 @@ using Ocelot.Responses; using Ocelot.Values; -namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer +namespace Ocelot.DownstreamUrlCreator { - public class DownstreamTemplatePathPlaceholderReplacer : IDownstreamPathPlaceholderReplacer + public class DownstreamPathPlaceholderReplacer : IDownstreamPathPlaceholderReplacer { public Response Replace(string downstreamPathTemplate, List urlPathPlaceholderNameAndValues) diff --git a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamPathPlaceholderReplacer.cs b/src/Ocelot/DownstreamUrlCreator/IDownstreamPathPlaceholderReplacer.cs similarity index 83% rename from src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamPathPlaceholderReplacer.cs rename to src/Ocelot/DownstreamUrlCreator/IDownstreamPathPlaceholderReplacer.cs index 6cbb83b02..edc657e72 100644 --- a/src/Ocelot/DownstreamUrlCreator/UrlTemplateReplacer/IDownstreamPathPlaceholderReplacer.cs +++ b/src/Ocelot/DownstreamUrlCreator/IDownstreamPathPlaceholderReplacer.cs @@ -2,7 +2,7 @@ using Ocelot.Responses; using Ocelot.Values; -namespace Ocelot.DownstreamUrlCreator.UrlTemplateReplacer +namespace Ocelot.DownstreamUrlCreator { public interface IDownstreamPathPlaceholderReplacer { diff --git a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs index a9513c4af..7492756c5 100644 --- a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs +++ b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs @@ -1,24 +1,29 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; -using Ocelot.Logging; -using Ocelot.Middleware; +using Ocelot.Logging; +using Ocelot.Middleware; using Ocelot.Request.Middleware; -using Ocelot.Responses; +using Ocelot.Responses; using Ocelot.Values; - +using System.Web; + namespace Ocelot.DownstreamUrlCreator.Middleware { public class DownstreamUrlCreatorMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IDownstreamPathPlaceholderReplacer _replacer; - - public DownstreamUrlCreatorMiddleware(RequestDelegate next, + + private const char Ampersand = '&'; + private const char QuestionMark = '?'; + private const char OpeningBrace = '{'; + private const char ClosingBrace = '}'; + + public DownstreamUrlCreatorMiddleware( + RequestDelegate next, IOcelotLoggerFactory loggerFactory, - IDownstreamPathPlaceholderReplacer replacer - ) + IDownstreamPathPlaceholderReplacer replacer) : base(loggerFactory.CreateLogger()) { _next = next; @@ -28,17 +33,13 @@ IDownstreamPathPlaceholderReplacer replacer public async Task Invoke(HttpContext httpContext) { var downstreamRoute = httpContext.Items.DownstreamRoute(); - - var templatePlaceholderNameAndValues = httpContext.Items.TemplatePlaceholderNameAndValues(); - - var response = _replacer - .Replace(downstreamRoute.DownstreamPathTemplate.Value, templatePlaceholderNameAndValues); - + var placeholders = httpContext.Items.TemplatePlaceholderNameAndValues(); + var response = _replacer.Replace(downstreamRoute.DownstreamPathTemplate.Value, placeholders); var downstreamRequest = httpContext.Items.DownstreamRequest(); if (response.IsError) { - Logger.LogDebug("IDownstreamPathPlaceholderReplacer returned an error, setting pipeline error"); + Logger.LogDebug($"{nameof(IDownstreamPathPlaceholderReplacer)} returned an error, setting pipeline error"); httpContext.Items.UpsertErrors(response.Errors); return; @@ -54,7 +55,7 @@ public async Task Invoke(HttpContext httpContext) if (ServiceFabricRequest(internalConfiguration, downstreamRoute)) { - var (path, query) = CreateServiceFabricUri(downstreamRequest, downstreamRoute, templatePlaceholderNameAndValues, response); + var (path, query) = CreateServiceFabricUri(downstreamRequest, downstreamRoute, placeholders, response); //todo check this works again hope there is a test.. downstreamRequest.AbsolutePath = path; @@ -63,23 +64,17 @@ public async Task Invoke(HttpContext httpContext) else { var dsPath = response.Data; - - if (ContainsQueryString(dsPath)) + if (dsPath.Value.Contains(QuestionMark)) { downstreamRequest.AbsolutePath = GetPath(dsPath); - - if (string.IsNullOrEmpty(downstreamRequest.Query)) - { - downstreamRequest.Query = GetQueryString(dsPath); - } - else - { - downstreamRequest.Query += GetQueryString(dsPath).Replace('?', '&'); - } + var newQuery = GetQueryString(dsPath); + downstreamRequest.Query = string.IsNullOrEmpty(downstreamRequest.Query) + ? newQuery + : MergeQueryStringsWithoutDuplicateValues(downstreamRequest.Query, newQuery, placeholders); } else { - RemoveQueryStringParametersThatHaveBeenUsedInTemplate(downstreamRequest, templatePlaceholderNameAndValues); + RemoveQueryStringParametersThatHaveBeenUsedInTemplate(downstreamRequest, placeholders); downstreamRequest.AbsolutePath = dsPath.Value; } @@ -90,11 +85,35 @@ public async Task Invoke(HttpContext httpContext) await _next.Invoke(httpContext); } + private static string MergeQueryStringsWithoutDuplicateValues(string queryString, string newQueryString, List placeholders) + { + newQueryString = newQueryString.Replace(QuestionMark, Ampersand); + var queries = HttpUtility.ParseQueryString(queryString); + var newQueries = HttpUtility.ParseQueryString(newQueryString); + + var parameters = newQueries.AllKeys + .Where(key => !string.IsNullOrEmpty(key)) + .ToDictionary(key => key, key => newQueries[key]); + + _ = queries.AllKeys + .Where(key => !string.IsNullOrEmpty(key) && !parameters.ContainsKey(key)) + .All(key => parameters.TryAdd(key, queries[key])); + + // Remove old replaced query parameters + foreach (var placeholder in placeholders) + { + parameters.Remove(placeholder.Name.Trim(OpeningBrace, ClosingBrace)); + } + + var orderedParams = parameters.OrderBy(x => x.Key).Select(x => $"{x.Key}={x.Value}"); + return QuestionMark + string.Join(Ampersand, orderedParams); + } + private static void RemoveQueryStringParametersThatHaveBeenUsedInTemplate(DownstreamRequest downstreamRequest, List templatePlaceholderNameAndValues) { foreach (var nAndV in templatePlaceholderNameAndValues) { - var name = nAndV.Name.Replace("{", string.Empty).Replace("}", string.Empty); + var name = nAndV.Name.Trim(OpeningBrace, ClosingBrace); var rgx = new Regex($@"\b{name}={nAndV.Value}\b"); @@ -106,29 +125,24 @@ private static void RemoveQueryStringParametersThatHaveBeenUsedInTemplate(Downst if (!string.IsNullOrEmpty(downstreamRequest.Query)) { - downstreamRequest.Query = string.Concat("?", downstreamRequest.Query.AsSpan(1)); + downstreamRequest.Query = QuestionMark + downstreamRequest.Query[1..]; } } - } + } } private static string GetPath(DownstreamPath dsPath) { - int length = dsPath.Value.IndexOf('?', StringComparison.Ordinal); + int length = dsPath.Value.IndexOf(QuestionMark, StringComparison.Ordinal); return dsPath.Value[..length]; } private static string GetQueryString(DownstreamPath dsPath) { - int startIndex = dsPath.Value.IndexOf('?', StringComparison.Ordinal); + int startIndex = dsPath.Value.IndexOf(QuestionMark, StringComparison.Ordinal); return dsPath.Value[startIndex..]; } - private static bool ContainsQueryString(DownstreamPath dsPath) - { - return dsPath.Value.Contains('?'); - } - private (string Path, string Query) CreateServiceFabricUri(DownstreamRequest downstreamRequest, DownstreamRoute downstreamRoute, List templatePlaceholderNameAndValues, Response dsPath) { var query = downstreamRequest.Query; @@ -138,7 +152,7 @@ private static bool ContainsQueryString(DownstreamPath dsPath) } private static bool ServiceFabricRequest(IInternalConfiguration config, DownstreamRoute downstreamRoute) - { + { return config.ServiceProviderConfiguration.Type?.ToLower() == "servicefabric" && downstreamRoute.UseServiceDiscovery; } } diff --git a/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs b/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs index ae7e04679..5c34167ac 100644 --- a/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs +++ b/test/Ocelot.AcceptanceTests/RoutingWithQueryStringTests.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration.File; - + namespace Ocelot.AcceptanceTests { public class RoutingWithQueryStringTests : IDisposable @@ -15,7 +15,7 @@ public RoutingWithQueryStringTests() } [Fact] - public void should_return_response_200_with_query_string_template() + public void Should_return_response_200_with_query_string_template() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); @@ -43,7 +43,7 @@ public void should_return_response_200_with_query_string_template() }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}", 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}", "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/{unitId}/updates")) @@ -51,9 +51,82 @@ public void should_return_response_200_with_query_string_template() .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } + + [Theory(DisplayName = "1182: " + nameof(Should_return_200_with_query_string_template_different_keys))] + [InlineData("")] + [InlineData("&x=xxx")] + public void Should_return_200_with_query_string_template_different_keys(string additionalParams) + { + var subscriptionId = Guid.NewGuid().ToString(); + var unitId = Guid.NewGuid().ToString(); + var port = PortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/subscriptions/{subscriptionId}/updates?unitId={unit}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + UpstreamPathTemplate = "/api/units/{subscriptionId}/updates?unit={unit}", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/subscriptions/{subscriptionId}/updates", $"?unitId={unitId}{additionalParams}", "Hello from Laura")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/units/{subscriptionId}/updates?unit={unitId}{additionalParams}")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Theory(DisplayName = "1174: " + nameof(Should_return_200_and_forward_query_parameters_without_duplicates))] + [InlineData("projectNumber=45&startDate=2019-12-12&endDate=2019-12-12", "endDate=2019-12-12&projectNumber=45&startDate=2019-12-12")] + [InlineData("$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z", "$filter=ProjectNumber%20eq%2045%20and%20DateOfSale%20ge%202020-03-01T00:00:00z%20and%20DateOfSale%20le%202020-03-15T00:00:00z")] + public void Should_return_200_and_forward_query_parameters_without_duplicates(string everythingelse, string expectedOrdered) + { + var port = PortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/api/contracts?{everythingelse}", + DownstreamScheme = "http", + DownstreamHostAndPorts = new List + { + new() { Host = "localhost", Port = port }, + }, + UpstreamPathTemplate = "/contracts?{everythingelse}", + UpstreamHttpMethod = new() { "Get" }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/contracts", $"?{expectedOrdered}", "Hello from @sunilk3")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunning()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/contracts?{everythingelse}")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from @sunilk3")) + .BDDfy(); + } + [Fact] - public void should_return_response_200_with_odata_query_string() + public void Should_return_response_200_with_odata_query_string() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); @@ -81,7 +154,7 @@ public void should_return_response_200_with_odata_query_string() }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/odata/customers", "?$filter=Name%20eq%20'Sam'", 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/odata/customers", "?$filter=Name%20eq%20'Sam'", "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/odata/customers?$filter=Name eq 'Sam' ")) @@ -91,7 +164,7 @@ public void should_return_response_200_with_odata_query_string() } [Fact] - public void should_return_response_200_with_query_string_upstream_template() + public void Should_return_response_200_with_query_string_upstream_template() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); @@ -119,7 +192,7 @@ public void should_return_response_200_with_query_string_upstream_template() }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}")) @@ -129,7 +202,7 @@ public void should_return_response_200_with_query_string_upstream_template() } [Fact] - public void should_return_response_404_with_query_string_upstream_template_no_query_string() + public void Should_return_response_404_with_query_string_upstream_template_no_query_string() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); @@ -157,7 +230,7 @@ public void should_return_response_404_with_query_string_upstream_template_no_qu }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates")) @@ -166,7 +239,7 @@ public void should_return_response_404_with_query_string_upstream_template_no_qu } [Fact] - public void should_return_response_404_with_query_string_upstream_template_different_query_string() + public void Should_return_response_404_with_query_string_upstream_template_different_query_string() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); @@ -194,7 +267,7 @@ public void should_return_response_404_with_query_string_upstream_template_diffe }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", string.Empty, "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?test=1")) @@ -203,7 +276,7 @@ public void should_return_response_404_with_query_string_upstream_template_diffe } [Fact] - public void should_return_response_200_with_query_string_upstream_template_multiple_params() + public void Should_return_response_200_with_query_string_upstream_template_multiple_params() { var subscriptionId = Guid.NewGuid().ToString(); var unitId = Guid.NewGuid().ToString(); @@ -231,18 +304,20 @@ public void should_return_response_200_with_query_string_upstream_template_multi }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", "?productId=1", 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/api/units/{subscriptionId}/{unitId}/updates", "?productId=1", "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/api/subscriptions/{subscriptionId}/updates?unitId={unitId}&productId=1")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); - } - - // to reproduce 1288: query string should contain the placeholder name and value - [Fact] - public void should_copy_query_string_to_downstream_path_issue_1288() + } + + /// + /// To reproduce 1288: query string should contain the placeholder name and value. + /// + [Fact(DisplayName = "1288: " + nameof(Should_copy_query_string_to_downstream_path))] + public void Should_copy_query_string_to_downstream_path() { var idName = "id"; var idValue = "3"; @@ -260,11 +335,7 @@ public void should_copy_query_string_to_downstream_path_issue_1288() DownstreamScheme = "http", DownstreamHostAndPorts = new List { - new FileHostAndPort - { - Host = "localhost", - Port = port, - }, + new() { Host = "localhost", Port = port }, }, UpstreamPathTemplate = $"/safe/{{{idName}}}", UpstreamHttpMethod = new List { "Get" }, @@ -272,7 +343,7 @@ public void should_copy_query_string_to_downstream_path_issue_1288() }, }; - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/cpx/t1/{idValue}", $"?{queryName}={queryValue}", 200, "Hello from Laura")) + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", $"/cpx/t1/{idValue}", $"?{queryName}={queryValue}", "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunning()) .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/safe/{idValue}?{queryName}={queryValue}")) @@ -281,18 +352,18 @@ public void should_copy_query_string_to_downstream_path_issue_1288() .BDDfy(); } - private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, string queryString, int statusCode, string responseBody) + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, string queryString, string responseBody) { _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, basePath, async context => { if ((context.Request.PathBase.Value != basePath) || context.Request.QueryString.Value != queryString) { - context.Response.StatusCode = 500; + context.Response.StatusCode = StatusCodes.Status500InternalServerError; await context.Response.WriteAsync("downstream path didnt match base path"); } else { - context.Response.StatusCode = statusCode; + context.Response.StatusCode = StatusCodes.Status200OK; await context.Response.WriteAsync(responseBody); } }); @@ -301,7 +372,8 @@ private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, stri public void Dispose() { _serviceHandler?.Dispose(); - _steps.Dispose(); + _steps.Dispose(); + GC.SuppressFinalize(this); } } } diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs similarity index 96% rename from test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs rename to test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs index da6b64e6f..b7ff203dd 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/UrlTemplateReplacer/UpstreamUrlPathTemplateVariableReplacerTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamPathPlaceholderReplacerTests.cs @@ -1,21 +1,21 @@ using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; +using Ocelot.DownstreamUrlCreator; using Ocelot.Responses; using Ocelot.Values; -namespace Ocelot.UnitTests.DownstreamUrlCreator.UrlTemplateReplacer +namespace Ocelot.UnitTests.DownstreamUrlCreator { - public class UpstreamUrlPathTemplateVariableReplacerTests + public class DownstreamPathPlaceholderReplacerTests { private DownstreamRouteHolder _downstreamRoute; private Response _result; private readonly IDownstreamPathPlaceholderReplacer _downstreamPathReplacer; - public UpstreamUrlPathTemplateVariableReplacerTests() + public DownstreamPathPlaceholderReplacerTests() { - _downstreamPathReplacer = new DownstreamTemplatePathPlaceholderReplacer(); + _downstreamPathReplacer = new DownstreamPathPlaceholderReplacer(); } [Fact] diff --git a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs index dafb3564b..febe8748c 100644 --- a/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/DownstreamUrlCreator/DownstreamUrlCreatorMiddlewareTests.cs @@ -3,8 +3,8 @@ using Ocelot.Configuration.Builder; using Ocelot.DownstreamRouteFinder; using Ocelot.DownstreamRouteFinder.UrlMatcher; +using Ocelot.DownstreamUrlCreator; using Ocelot.DownstreamUrlCreator.Middleware; -using Ocelot.DownstreamUrlCreator.UrlTemplateReplacer; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; @@ -24,7 +24,7 @@ public class DownstreamUrlCreatorMiddlewareTests private readonly RequestDelegate _next; private readonly HttpRequestMessage _request; private readonly HttpContext _httpContext; - private Mock _repo; + private readonly Mock _repo; public DownstreamUrlCreatorMiddlewareTests() { @@ -39,7 +39,7 @@ public DownstreamUrlCreatorMiddlewareTests() } [Fact] - public void should_replace_scheme_and_path() + public void Should_replace_scheme_and_path() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") @@ -67,7 +67,7 @@ public void should_replace_scheme_and_path() } [Fact] - public void should_replace_query_string() + public void Should_replace_query_string() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") @@ -99,7 +99,7 @@ public void should_replace_query_string() } [Fact] - public void should_replace_query_string_but_leave_non_placeholder_queries() + public void Should_replace_query_string_but_leave_non_placeholder_queries() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") @@ -131,7 +131,7 @@ public void should_replace_query_string_but_leave_non_placeholder_queries() } [Fact] - public void should_replace_query_string_but_leave_non_placeholder_queries_2() + public void Should_replace_query_string_but_leave_non_placeholder_queries_2() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates") @@ -163,7 +163,7 @@ public void should_replace_query_string_but_leave_non_placeholder_queries_2() } [Fact] - public void should_replace_query_string_exact_match() + public void Should_replace_query_string_exact_match() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("/api/units/{subscriptionId}/{unitId}/updates/{unitIdIty}") @@ -196,7 +196,7 @@ public void should_replace_query_string_exact_match() } [Fact] - public void should_not_create_service_fabric_url() + public void Should_not_create_service_fabric_url() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamPathTemplate("any old string") @@ -226,7 +226,7 @@ public void should_not_create_service_fabric_url() } [Fact] - public void should_create_service_fabric_url() + public void Should_create_service_fabric_url() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamScheme("http") @@ -256,7 +256,7 @@ public void should_create_service_fabric_url() } [Fact] - public void should_create_service_fabric_url_with_query_string_for_stateless_service() + public void Should_create_service_fabric_url_with_query_string_for_stateless_service() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamScheme("http") @@ -286,7 +286,7 @@ public void should_create_service_fabric_url_with_query_string_for_stateless_ser } [Fact] - public void should_create_service_fabric_url_with_query_string_for_stateful_service() + public void Should_create_service_fabric_url_with_query_string_for_stateful_service() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamScheme("http") @@ -316,7 +316,7 @@ public void should_create_service_fabric_url_with_query_string_for_stateful_serv } [Fact] - public void should_create_service_fabric_url_with_version_from_upstream_path_template() + public void Should_create_service_fabric_url_with_version_from_upstream_path_template() { var downstreamRoute = new DownstreamRouteHolder( new List(), @@ -343,14 +343,16 @@ public void should_create_service_fabric_url_with_version_from_upstream_path_tem .BDDfy(); } - [Fact] - public void issue_473_should_not_remove_additional_query_string() + [Fact(DisplayName = "473: " + nameof(Should_not_remove_additional_query_parameter_when_placeholder_and_parameter_names_are_different))] + public void Should_not_remove_additional_query_parameter_when_placeholder_and_parameter_names_are_different() { + var methods = new List { "Post", "Get" }; var downstreamRoute = new DownstreamRouteBuilder() - .WithDownstreamPathTemplate("/Authorized/{action}?server={server}") - .WithUpstreamHttpMethod(new List { "Post", "Get" }) - .WithDownstreamScheme("http") - .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder().WithOriginalValue("/uc/Authorized/{server}/{action}").Build()) + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue("/uc/Authorized/{servak}/{action}").Build()) + .WithDownstreamPathTemplate("/Authorized/{action}?server={servak}") + .WithUpstreamHttpMethod(methods) + .WithDownstreamScheme(Uri.UriSchemeHttp) .Build(); var config = new ServiceProviderConfigurationBuilder() @@ -361,23 +363,22 @@ public void issue_473_should_not_remove_additional_query_string() new List { new("{action}", "1"), - new("{server}", "2"), + new("{servak}", "2"), }, - new RouteBuilder() - .WithDownstreamRoute(downstreamRoute) - .WithUpstreamHttpMethod(new List { "Post", "Get" }) + new RouteBuilder().WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(methods) .Build()))) - .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000/uc/Authorized/2/1/refresh?refreshToken=2288356cfb1338fdc5ff4ca558ec785118dfe1ff2864340937da8226863ff66d")) + .And(x => x.GivenTheDownstreamRequestUriIs("http://localhost:5000/uc/Authorized/2/1/refresh?refreshToken=123456789")) .And(x => GivenTheServiceProviderConfigIs(config)) .And(x => x.GivenTheUrlReplacerWillReturn("/Authorized/1?server=2")) .When(x => x.WhenICallTheMiddleware()) - .Then(x => x.ThenTheDownstreamRequestUriIs("http://localhost:5000/Authorized/1?refreshToken=2288356cfb1338fdc5ff4ca558ec785118dfe1ff2864340937da8226863ff66d&server=2")) - .And(x => ThenTheQueryStringIs("?refreshToken=2288356cfb1338fdc5ff4ca558ec785118dfe1ff2864340937da8226863ff66d&server=2")) + .Then(x => x.ThenTheDownstreamRequestUriIs("http://localhost:5000/Authorized/1?refreshToken=123456789&server=2")) + .And(x => ThenTheQueryStringIs("?refreshToken=123456789&server=2")) .BDDfy(); } [Fact] - public void should_not_replace_by_empty_scheme() + public void Should_not_replace_by_empty_scheme() { var downstreamRoute = new DownstreamRouteBuilder() .WithDownstreamScheme(string.Empty) @@ -406,6 +407,100 @@ public void should_not_replace_by_empty_scheme() .BDDfy(); } + [Fact(DisplayName = "952: " + nameof(Should_map_query_parameters_with_different_names))] + public void Should_map_query_parameters_with_different_names() + { + var methods = new List { "Post", "Get" }; + var downstreamRoute = new DownstreamRouteBuilder() + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue("/users?userId={userId}").Build()) + .WithDownstreamPathTemplate("/persons?personId={userId}") + .WithUpstreamHttpMethod(methods) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + var config = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenTheDownStreamRouteIs( + new DownstreamRouteHolder( + new List + { + new("{userId}", "webley"), + }, + new RouteBuilder().WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(methods) + .Build()))) + .And(x => x.GivenTheDownstreamRequestUriIs($"http://localhost:5000/users?userId=webley")) + .And(x => GivenTheServiceProviderConfigIs(config)) + .And(x => x.GivenTheUrlReplacerWillReturn("/persons?personId=webley")) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheDownstreamRequestUriIs($"http://localhost:5000/persons?personId=webley")) + .And(x => ThenTheQueryStringIs($"?personId=webley")) + .BDDfy(); + } + + [Fact(DisplayName = "952: " + nameof(Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ))] + public void Should_map_query_parameters_with_different_names_and_save_old_param_if_placeholder_and_param_names_differ() + { + var methods = new List { "Post", "Get" }; + var downstreamRoute = new DownstreamRouteBuilder() + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue("/users?userId={uid}").Build()) + .WithDownstreamPathTemplate("/persons?personId={uid}") + .WithUpstreamHttpMethod(methods) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + var config = new ServiceProviderConfigurationBuilder().Build(); + + this.Given(x => x.GivenTheDownStreamRouteIs( + new DownstreamRouteHolder( + new List + { + new("{uid}", "webley"), + }, + new RouteBuilder().WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(methods) + .Build()))) + .And(x => x.GivenTheDownstreamRequestUriIs($"http://localhost:5000/users?userId=webley")) + .And(x => GivenTheServiceProviderConfigIs(config)) + .And(x => x.GivenTheUrlReplacerWillReturn("/persons?personId=webley")) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheDownstreamRequestUriIs($"http://localhost:5000/persons?personId=webley&userId=webley")) + .And(x => ThenTheQueryStringIs($"?personId=webley&userId=webley")) + .BDDfy(); + } + + [Theory(DisplayName = "1174: " + nameof(Should_forward_query_parameters_without_duplicates))] + [InlineData("projectNumber=45&startDate=2019-12-12&endDate=2019-12-12", "endDate=2019-12-12&projectNumber=45&startDate=2019-12-12")] + [InlineData("$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z", "$filter=ProjectNumber eq 45 and DateOfSale ge 2020-03-01T00:00:00z and DateOfSale le 2020-03-15T00:00:00z")] + public void Should_forward_query_parameters_without_duplicates(string everythingelse, string expectedOrdered) + { + var methods = new List { "Get" }; + var downstreamRoute = new DownstreamRouteBuilder() + .WithUpstreamPathTemplate(new UpstreamPathTemplateBuilder() + .WithOriginalValue("/contracts?{everythingelse}").Build()) + .WithDownstreamPathTemplate("/api/contracts?{everythingelse}") + .WithUpstreamHttpMethod(methods) + .WithDownstreamScheme(Uri.UriSchemeHttp) + .Build(); + var config = new ServiceProviderConfigurationBuilder().Build(); + this.Given(x => x.GivenTheDownStreamRouteIs( + new DownstreamRouteHolder( + new List + { + new("{everythingelse}", everythingelse), + }, + new RouteBuilder().WithDownstreamRoute(downstreamRoute) + .WithUpstreamHttpMethod(methods) + .Build()))) + .And(x => x.GivenTheDownstreamRequestUriIs($"http://localhost:5000//contracts?{everythingelse}")) + .And(x => GivenTheServiceProviderConfigIs(config)) + .And(x => x.GivenTheUrlReplacerWillReturn($"/api/contracts?{everythingelse}")) + .When(x => x.WhenICallTheMiddleware()) + .Then(x => x.ThenTheDownstreamRequestUriIs($"http://localhost:5000/api/contracts?{expectedOrdered}")) + .And(x => ThenTheQueryStringIs($"?{expectedOrdered}")) + .BDDfy(); + } + private void GivenTheServiceProviderConfigIs(ServiceProviderConfiguration config) { var configuration = new InternalConfiguration(null, null, config, null, null, null, null, null, null); From dabb4b5e19f4b7bee428b8a4e992663ddc0a6d7d Mon Sep 17 00:00:00 2001 From: Guillaume Gnaegi <58469901+ggnaegi@users.noreply.github.com> Date: Sat, 4 Nov 2023 09:27:51 +0100 Subject: [PATCH 05/12] #1550 #1706 Addressing the QoS options ExceptionsAllowedBeforeBreaking issue (#1753) * When using the QoS option "ExceptionsAllowedBeforeBreaking" the circuit breaker never opens the circuit. * merge issue, PortFinder * some code improvements, using httpresponsemessage status codes as a base for circuit breaker * Adding more unit tests, and trying to mitigate the test issues with the method "GivenThereIsAPossiblyBrokenServiceRunningOn" * fixing some test issues * setting timeout value to 5000 to avoid side effects * again timing issues * timing issues again * ok, first one ok * Revert "ok, first one ok" This reverts commit 2e4a673c28894a39f7e057907badb448247ff9d7. * inline method * putting back logging for http request exception * removing logger configuration, back to default * adding a bit more tests to check the policy wrap * Removing TimeoutStrategy from parameters, it's set by default to pessimistic, at least one policy will be returned, so using First() in circuit breaker and removing the branch Policy == null from delegating handler. * Fix StyleCop warnings * Format parameters * Sort usings * since we might have two policies wrapped, timeout and circuit breaker, we can't use the name CircuitBreaker for polly qos provider, it's not right. Using PollyPolicyWrapper and AsnycPollyPolicy instead. * modifying circuit breaker delegating handler name, usin Polly policies instead * renaming CircuitBreakerFactory to PolicyWrapperFactory in tests * DRY for FileConfiguration, using FileConfigurationFactory * Add copy constructor * Refactor setup * Use expression body for method * Fix acceptance test * IDE1006 Naming rule violation: These words must begin with upper case characters * CA1816 Change ReturnsErrorTests.Dispose() to call GC.SuppressFinalize(object) * Sort usings * Use expression body for method * Return back named arguments --------- Co-authored-by: raman-m --- .gitignore | 1 + src/Ocelot.Provider.Polly/CircuitBreaker.cs | 12 - .../Interfaces/IPollyQoSProvider.cs | 9 +- .../OcelotBuilderExtensions.cs | 49 +- .../PollyCircuitBreakingDelegatingHandler.cs | 48 -- .../PollyPoliciesDelegatingHandler.cs | 56 +++ .../PollyPolicyWrapper.cs | 23 + src/Ocelot.Provider.Polly/PollyQoSProvider.cs | 126 +++-- .../File/FileAuthenticationOptions.cs | 6 + .../Configuration/File/FileCacheOptions.cs | 20 +- .../Configuration/File/FileHostAndPort.cs | 16 +- .../File/FileHttpHandlerOptions.cs | 18 +- .../File/FileLoadBalancerOptions.cs | 18 +- .../Configuration/File/FileQoSOptions.cs | 25 +- .../Configuration/File/FileRateLimitRule.cs | 9 + src/Ocelot/Configuration/File/FileRoute.cs | 139 ++++-- .../Configuration/File/FileSecurityOptions.cs | 7 + src/Ocelot/Configuration/QoSOptions.cs | 42 +- .../Requester/HttpExceptionToErrorMapper.cs | 4 +- src/Ocelot/Requester/QoS/QosFactory.cs | 11 +- .../Requester/QosDelegatingHandlerDelegate.cs | 7 +- test/Ocelot.AcceptanceTests/PollyQoSTests.cs | 458 +++++++----------- .../ReturnsErrorTests.cs | 11 +- test/Ocelot.AcceptanceTests/Steps.cs | 8 +- test/Ocelot.ManualTest/Program.cs | 2 +- .../FileConfigurationFluentValidatorTests.cs | 9 +- .../FileQoSOptionsFluentValidatorTests.cs | 14 +- .../Polly/OcelotBuilderExtensionsTests.cs | 8 +- ...lyCircuitBreakingDelegatingHandlerTests.cs | 153 ------ .../PollyPoliciesDelegatingHandlerTests.cs | 255 ++++++++++ .../Polly/PollyQoSProviderTests.cs | 259 +++++++++- ...atingHandlerHandlerProviderFactoryTests.cs | 2 +- .../Requester/QoSFactoryTests.cs | 15 +- 33 files changed, 1139 insertions(+), 701 deletions(-) delete mode 100644 src/Ocelot.Provider.Polly/CircuitBreaker.cs delete mode 100644 src/Ocelot.Provider.Polly/PollyCircuitBreakingDelegatingHandler.cs create mode 100644 src/Ocelot.Provider.Polly/PollyPoliciesDelegatingHandler.cs create mode 100644 src/Ocelot.Provider.Polly/PollyPolicyWrapper.cs delete mode 100644 test/Ocelot.UnitTests/Polly/PollyCircuitBreakingDelegatingHandlerTests.cs create mode 100644 test/Ocelot.UnitTests/Polly/PollyPoliciesDelegatingHandlerTests.cs diff --git a/.gitignore b/.gitignore index 220172830..7948d182c 100644 --- a/.gitignore +++ b/.gitignore @@ -255,3 +255,4 @@ _templates/ # Test Results *.trx +/Ocelot.sln.DotSettings diff --git a/src/Ocelot.Provider.Polly/CircuitBreaker.cs b/src/Ocelot.Provider.Polly/CircuitBreaker.cs deleted file mode 100644 index c9be6d924..000000000 --- a/src/Ocelot.Provider.Polly/CircuitBreaker.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Ocelot.Provider.Polly -{ - public class CircuitBreaker - { - public CircuitBreaker(params IAsyncPolicy[] policies) - { - Policies = policies.Where(p => p != null).ToArray(); - } - - public IAsyncPolicy[] Policies { get; } - } -} diff --git a/src/Ocelot.Provider.Polly/Interfaces/IPollyQoSProvider.cs b/src/Ocelot.Provider.Polly/Interfaces/IPollyQoSProvider.cs index 4b693ede0..214597531 100644 --- a/src/Ocelot.Provider.Polly/Interfaces/IPollyQoSProvider.cs +++ b/src/Ocelot.Provider.Polly/Interfaces/IPollyQoSProvider.cs @@ -1,6 +1,9 @@ -namespace Ocelot.Provider.Polly.Interfaces; +using Ocelot.Configuration; -public interface IPollyQoSProvider +namespace Ocelot.Provider.Polly.Interfaces; + +public interface IPollyQoSProvider + where TResult : class { - CircuitBreaker CircuitBreaker { get; } + PollyPolicyWrapper GetPollyPolicyWrapper(DownstreamRoute route); } diff --git a/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs index 229b2171e..14e3750fb 100644 --- a/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs @@ -1,32 +1,35 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.DependencyInjection; using Ocelot.Errors; using Ocelot.Logging; +using Ocelot.Provider.Polly.Interfaces; using Ocelot.Requester; using Polly.CircuitBreaker; using Polly.Timeout; - -namespace Ocelot.Provider.Polly -{ - public static class OcelotBuilderExtensions - { - public static IOcelotBuilder AddPolly(this IOcelotBuilder builder) - { - var errorMapping = new Dictionary> - { - {typeof(TaskCanceledException), e => new RequestTimedOutError(e)}, - {typeof(TimeoutRejectedException), e => new RequestTimedOutError(e)}, - {typeof(BrokenCircuitException), e => new RequestTimedOutError(e)}, - }; - - builder.Services - .AddSingleton(errorMapping) - .AddSingleton(GetDelegatingHandler); - return builder; - } - private static DelegatingHandler GetDelegatingHandler(DownstreamRoute route, IOcelotLoggerFactory logger) - => new PollyCircuitBreakingDelegatingHandler(new PollyQoSProvider(route, logger), logger); - } +namespace Ocelot.Provider.Polly; + +public static class OcelotBuilderExtensions +{ + public static IOcelotBuilder AddPolly(this IOcelotBuilder builder) + { + var errorMapping = new Dictionary> + { + { typeof(TaskCanceledException), e => new RequestTimedOutError(e) }, + { typeof(TimeoutRejectedException), e => new RequestTimedOutError(e) }, + { typeof(BrokenCircuitException), e => new RequestTimedOutError(e) }, + { typeof(BrokenCircuitException), e => new RequestTimedOutError(e) }, + }; + + builder.Services + .AddSingleton(errorMapping) + .AddSingleton, PollyQoSProvider>() + .AddSingleton(GetDelegatingHandler); + return builder; + } + + private static DelegatingHandler GetDelegatingHandler(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory) + => new PollyPoliciesDelegatingHandler(route, contextAccessor, loggerFactory); } diff --git a/src/Ocelot.Provider.Polly/PollyCircuitBreakingDelegatingHandler.cs b/src/Ocelot.Provider.Polly/PollyCircuitBreakingDelegatingHandler.cs deleted file mode 100644 index 94d66962f..000000000 --- a/src/Ocelot.Provider.Polly/PollyCircuitBreakingDelegatingHandler.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Ocelot.Logging; -using Ocelot.Provider.Polly.Interfaces; -using Polly.CircuitBreaker; - -namespace Ocelot.Provider.Polly -{ - public class PollyCircuitBreakingDelegatingHandler : DelegatingHandler - { - private readonly IPollyQoSProvider _qoSProvider; - private readonly IOcelotLogger _logger; - - public PollyCircuitBreakingDelegatingHandler( - IPollyQoSProvider qoSProvider, - IOcelotLoggerFactory loggerFactory) - { - _qoSProvider = qoSProvider; - _logger = loggerFactory.CreateLogger(); - } - - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - try - { - var policies = _qoSProvider.CircuitBreaker.Policies; - if (!policies.Any()) - { - return await base.SendAsync(request, cancellationToken); - } - - var policy = policies.Length > 1 - ? Policy.WrapAsync(policies) - : policies[0]; - - return await policy.ExecuteAsync(() => base.SendAsync(request, cancellationToken)); - } - catch (BrokenCircuitException ex) - { - _logger.LogError("Reached to allowed number of exceptions. Circuit is open", ex); - throw; - } - catch (HttpRequestException ex) - { - _logger.LogError($"Error in {nameof(PollyCircuitBreakingDelegatingHandler)}.{nameof(SendAsync)}", ex); - throw; - } - } - } -} diff --git a/src/Ocelot.Provider.Polly/PollyPoliciesDelegatingHandler.cs b/src/Ocelot.Provider.Polly/PollyPoliciesDelegatingHandler.cs new file mode 100644 index 000000000..fbf5e4a57 --- /dev/null +++ b/src/Ocelot.Provider.Polly/PollyPoliciesDelegatingHandler.cs @@ -0,0 +1,56 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration; +using Ocelot.Logging; +using Ocelot.Provider.Polly.Interfaces; +using Polly.CircuitBreaker; +using System.Diagnostics; + +namespace Ocelot.Provider.Polly; + +public class PollyPoliciesDelegatingHandler : DelegatingHandler +{ + private readonly DownstreamRoute _route; + private readonly IHttpContextAccessor _contextAccessor; + private readonly IOcelotLogger _logger; + + public PollyPoliciesDelegatingHandler( + DownstreamRoute route, + IHttpContextAccessor contextAccessor, + IOcelotLoggerFactory loggerFactory) + { + _route = route; + _contextAccessor = contextAccessor; + _logger = loggerFactory.CreateLogger(); + } + + private IPollyQoSProvider GetQoSProvider() + { + Debug.Assert(_contextAccessor.HttpContext != null, "_contextAccessor.HttpContext != null"); + return _contextAccessor.HttpContext.RequestServices.GetService>(); + } + + protected override async Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) + { + var qoSProvider = GetQoSProvider(); + try + { + // at least one policy (timeout) will be returned + // AsyncPollyPolicy can't be null + // AsyncPollyPolicy constructor will throw if no policy is provided + var policy = qoSProvider.GetPollyPolicyWrapper(_route).AsyncPollyPolicy; + return await policy.ExecuteAsync(async () => await base.SendAsync(request, cancellationToken)); + } + catch (BrokenCircuitException ex) + { + _logger.LogError("Reached to allowed number of exceptions. Circuit is open", ex); + throw; + } + catch (HttpRequestException ex) + { + _logger.LogError($"Error in {nameof(PollyPoliciesDelegatingHandler)}.{nameof(SendAsync)}", ex); + throw; + } + } +} diff --git a/src/Ocelot.Provider.Polly/PollyPolicyWrapper.cs b/src/Ocelot.Provider.Polly/PollyPolicyWrapper.cs new file mode 100644 index 000000000..508803976 --- /dev/null +++ b/src/Ocelot.Provider.Polly/PollyPolicyWrapper.cs @@ -0,0 +1,23 @@ +namespace Ocelot.Provider.Polly; + +public class PollyPolicyWrapper + where TResult : class +{ + /// + /// Initializes a new instance of the class. + /// We expect at least one policy to be passed in, default can't be null. + /// + /// The policies with at least a policy. + public PollyPolicyWrapper(params IAsyncPolicy[] policies) + { + var allPolicies = policies.Where(p => p != null).ToArray(); + AsyncPollyPolicy = allPolicies.First(); + + if (allPolicies.Length > 1) + { + AsyncPollyPolicy = Policy.WrapAsync(allPolicies); + } + } + + public IAsyncPolicy AsyncPollyPolicy { get; } +} diff --git a/src/Ocelot.Provider.Polly/PollyQoSProvider.cs b/src/Ocelot.Provider.Polly/PollyQoSProvider.cs index 58a918162..edd460e77 100644 --- a/src/Ocelot.Provider.Polly/PollyQoSProvider.cs +++ b/src/Ocelot.Provider.Polly/PollyQoSProvider.cs @@ -1,54 +1,78 @@ -using Ocelot.Configuration; -using Ocelot.Logging; -using Ocelot.Provider.Polly.Interfaces; -using Polly.CircuitBreaker; -using Polly.Timeout; - -namespace Ocelot.Provider.Polly -{ - public class PollyQoSProvider : IPollyQoSProvider - { - public PollyQoSProvider(DownstreamRoute route, IOcelotLoggerFactory loggerFactory) - { - AsyncCircuitBreakerPolicy circuitBreakerPolicy = null; - if (route.QosOptions.ExceptionsAllowedBeforeBreaking > 0) +using Ocelot.Configuration; +using Ocelot.Logging; +using Ocelot.Provider.Polly.Interfaces; +using Polly.CircuitBreaker; +using Polly.Timeout; +using System.Net; + +namespace Ocelot.Provider.Polly; + +public class PollyQoSProvider : IPollyQoSProvider +{ + private readonly Dictionary> _policyWrappers = new(); + private readonly object _lockObject = new(); + private readonly IOcelotLogger _logger; + + private readonly HashSet _serverErrorCodes = new() + { + HttpStatusCode.InternalServerError, + HttpStatusCode.NotImplemented, + HttpStatusCode.BadGateway, + HttpStatusCode.ServiceUnavailable, + HttpStatusCode.GatewayTimeout, + HttpStatusCode.HttpVersionNotSupported, + HttpStatusCode.VariantAlsoNegotiates, + HttpStatusCode.InsufficientStorage, + HttpStatusCode.LoopDetected, + }; + + public PollyQoSProvider(IOcelotLoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(); + } + + private static string GetRouteName(DownstreamRoute route) + => string.IsNullOrWhiteSpace(route.ServiceName) + ? route.UpstreamPathTemplate?.Template ?? route.DownstreamPathTemplate?.Value ?? string.Empty + : route.ServiceName; + + public PollyPolicyWrapper GetPollyPolicyWrapper(DownstreamRoute route) + { + lock (_lockObject) + { + var currentRouteName = GetRouteName(route); + if (!_policyWrappers.ContainsKey(currentRouteName)) { - var info = $"Route: {GetRouteName(route)}; Breaker logging in {nameof(PollyQoSProvider)}: "; - var logger = loggerFactory.CreateLogger(); - circuitBreakerPolicy = Policy - .Handle() - .Or() - .Or() - .CircuitBreakerAsync( - exceptionsAllowedBeforeBreaking: route.QosOptions.ExceptionsAllowedBeforeBreaking, - durationOfBreak: TimeSpan.FromMilliseconds(route.QosOptions.DurationOfBreak), - onBreak: (ex, breakDelay) => - logger.LogError(info + $"Breaking the circuit for {breakDelay.TotalMilliseconds} ms!", ex), - onReset: () => - logger.LogDebug(info + "Call OK! Closed the circuit again."), - onHalfOpen: () => - logger.LogDebug(info + "Half-open; Next call is a trial.") - ); - } - - _ = Enum.TryParse(route.QosOptions.TimeoutStrategy, out TimeoutStrategy strategy); - var timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromMilliseconds(route.QosOptions.TimeoutValue), strategy); - CircuitBreaker = new CircuitBreaker(circuitBreakerPolicy, timeoutPolicy); + _policyWrappers.Add(currentRouteName, PollyPolicyWrapperFactory(route)); + } + + return _policyWrappers[currentRouteName]; } + } + + private PollyPolicyWrapper PollyPolicyWrapperFactory(DownstreamRoute route) + { + AsyncCircuitBreakerPolicy exceptionsAllowedBeforeBreakingPolicy = null; + if (route.QosOptions.ExceptionsAllowedBeforeBreaking > 0) + { + var info = $"Route: {GetRouteName(route)}; Breaker logging in {nameof(PollyQoSProvider)}: "; + + exceptionsAllowedBeforeBreakingPolicy = Policy + .HandleResult(r => _serverErrorCodes.Contains(r.StatusCode)) + .Or() + .Or() + .CircuitBreakerAsync(route.QosOptions.ExceptionsAllowedBeforeBreaking, + durationOfBreak: TimeSpan.FromMilliseconds(route.QosOptions.DurationOfBreak), + onBreak: (ex, breakDelay) => _logger.LogError(info + $"Breaking the circuit for {breakDelay.TotalMilliseconds} ms!", ex.Exception), + onReset: () => _logger.LogDebug(info + "Call OK! Closed the circuit again."), + onHalfOpen: () => _logger.LogDebug(info + "Half-open; Next call is a trial.")); + } + + var timeoutPolicy = Policy + .TimeoutAsync( + TimeSpan.FromMilliseconds(route.QosOptions.TimeoutValue), + TimeoutStrategy.Pessimistic); - private const string ObsoleteConstructorMessage = $"Use the constructor {nameof(PollyQoSProvider)}({nameof(DownstreamRoute)} route, {nameof(IOcelotLoggerFactory)} loggerFactory)!"; - - [Obsolete(ObsoleteConstructorMessage)] - public PollyQoSProvider(AsyncCircuitBreakerPolicy circuitBreakerPolicy, AsyncTimeoutPolicy timeoutPolicy, IOcelotLogger logger) - { - throw new NotSupportedException(ObsoleteConstructorMessage); - } - - public CircuitBreaker CircuitBreaker { get; } - - private static string GetRouteName(DownstreamRoute route) - => string.IsNullOrWhiteSpace(route.ServiceName) - ? route.UpstreamPathTemplate?.Template ?? route.DownstreamPathTemplate?.Value ?? string.Empty - : route.ServiceName; - } -} + return new PollyPolicyWrapper(exceptionsAllowedBeforeBreakingPolicy, timeoutPolicy); + } +} diff --git a/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs index afac5d922..eebce214c 100644 --- a/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs +++ b/src/Ocelot/Configuration/File/FileAuthenticationOptions.cs @@ -6,6 +6,12 @@ public FileAuthenticationOptions() { AllowedScopes = new List(); } + + public FileAuthenticationOptions(FileAuthenticationOptions from) + { + AllowedScopes = new(from.AllowedScopes); + AuthenticationProviderKey = from.AuthenticationProviderKey; + } public string AuthenticationProviderKey { get; set; } public List AllowedScopes { get; set; } diff --git a/src/Ocelot/Configuration/File/FileCacheOptions.cs b/src/Ocelot/Configuration/File/FileCacheOptions.cs index e1438bbab..79cb81da8 100644 --- a/src/Ocelot/Configuration/File/FileCacheOptions.cs +++ b/src/Ocelot/Configuration/File/FileCacheOptions.cs @@ -1,9 +1,23 @@ namespace Ocelot.Configuration.File { public class FileCacheOptions - { - public int TtlSeconds { get; set; } - public string Region { get; set; } + { + public FileCacheOptions() + { + Header = string.Empty; + Region = string.Empty; + TtlSeconds = 0; + } + + public FileCacheOptions(FileCacheOptions from) + { + Header = from.Header; + Region = from.Region; + TtlSeconds = from.TtlSeconds; + } + public string Header { get; set; } + public string Region { get; set; } + public int TtlSeconds { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileHostAndPort.cs b/src/Ocelot/Configuration/File/FileHostAndPort.cs index 23c745e09..a3eeaae83 100644 --- a/src/Ocelot/Configuration/File/FileHostAndPort.cs +++ b/src/Ocelot/Configuration/File/FileHostAndPort.cs @@ -1,7 +1,21 @@ namespace Ocelot.Configuration.File { public class FileHostAndPort - { + { + public FileHostAndPort() { } + + public FileHostAndPort(FileHostAndPort from) + { + Host = from.Host; + Port = from.Port; + } + + public FileHostAndPort(string host, int port) + { + Host = host; + Port = port; + } + public string Host { get; set; } public int Port { get; set; } } diff --git a/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs b/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs index b83be428b..33e1a15cb 100644 --- a/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs +++ b/src/Ocelot/Configuration/File/FileHttpHandlerOptions.cs @@ -5,19 +5,23 @@ public class FileHttpHandlerOptions public FileHttpHandlerOptions() { AllowAutoRedirect = false; + MaxConnectionsPerServer = int.MaxValue; UseCookieContainer = false; UseProxy = true; - MaxConnectionsPerServer = int.MaxValue; + } + + public FileHttpHandlerOptions(FileHttpHandlerOptions from) + { + AllowAutoRedirect = from.AllowAutoRedirect; + MaxConnectionsPerServer = from.MaxConnectionsPerServer; + UseCookieContainer = from.UseCookieContainer; + UseProxy = from.UseProxy; } public bool AllowAutoRedirect { get; set; } - + public int MaxConnectionsPerServer { get; set; } public bool UseCookieContainer { get; set; } - - public bool UseTracing { get; set; } - public bool UseProxy { get; set; } - - public int MaxConnectionsPerServer { get; set; } + public bool UseTracing { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs b/src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs index 105ccc7a5..45ea8a0a5 100644 --- a/src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs +++ b/src/Ocelot/Configuration/File/FileLoadBalancerOptions.cs @@ -2,8 +2,22 @@ namespace Ocelot.Configuration.File { public class FileLoadBalancerOptions { - public string Type { get; set; } - public string Key { get; set; } + public FileLoadBalancerOptions() + { + Expiry = int.MaxValue; + Key = string.Empty; + Type = string.Empty; + } + + public FileLoadBalancerOptions(FileLoadBalancerOptions from) + { + Expiry = from.Expiry; + Key = from.Key; + Type = from.Type; + } + public int Expiry { get; set; } + public string Key { get; set; } + public string Type { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileQoSOptions.cs b/src/Ocelot/Configuration/File/FileQoSOptions.cs index 4e12b4a3a..85b51fa43 100644 --- a/src/Ocelot/Configuration/File/FileQoSOptions.cs +++ b/src/Ocelot/Configuration/File/FileQoSOptions.cs @@ -1,11 +1,30 @@ namespace Ocelot.Configuration.File { public class FileQoSOptions - { - public int ExceptionsAllowedBeforeBreaking { get; set; } + { + public FileQoSOptions() + { + DurationOfBreak = 1; + ExceptionsAllowedBeforeBreaking = 0; + TimeoutValue = int.MaxValue; + } + + public FileQoSOptions(FileQoSOptions from) + { + DurationOfBreak = from.DurationOfBreak; + ExceptionsAllowedBeforeBreaking = from.ExceptionsAllowedBeforeBreaking; + TimeoutValue = from.TimeoutValue; + } + + public FileQoSOptions(QoSOptions from) + { + DurationOfBreak = from.DurationOfBreak; + ExceptionsAllowedBeforeBreaking = from.ExceptionsAllowedBeforeBreaking; + TimeoutValue = from.TimeoutValue; + } public int DurationOfBreak { get; set; } - + public int ExceptionsAllowedBeforeBreaking { get; set; } public int TimeoutValue { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileRateLimitRule.cs b/src/Ocelot/Configuration/File/FileRateLimitRule.cs index 907e5341c..ffbc0c994 100644 --- a/src/Ocelot/Configuration/File/FileRateLimitRule.cs +++ b/src/Ocelot/Configuration/File/FileRateLimitRule.cs @@ -7,6 +7,15 @@ public FileRateLimitRule() ClientWhitelist = new List(); } + public FileRateLimitRule(FileRateLimitRule from) + { + ClientWhitelist = new(from.ClientWhitelist); + EnableRateLimiting = from.EnableRateLimiting; + Limit = from.Limit; + Period = from.Period; + PeriodTimespan = from.PeriodTimespan; + } + /// /// The list of allowed clients. /// diff --git a/src/Ocelot/Configuration/File/FileRoute.cs b/src/Ocelot/Configuration/File/FileRoute.cs index e99ed2bc1..5823113ad 100644 --- a/src/Ocelot/Configuration/File/FileRoute.cs +++ b/src/Ocelot/Configuration/File/FileRoute.cs @@ -1,59 +1,110 @@ namespace Ocelot.Configuration.File { - public class FileRoute : IRoute + public class FileRoute : IRoute, ICloneable { public FileRoute() { - UpstreamHttpMethod = new List(); - AddHeadersToRequest = new Dictionary(); AddClaimsToRequest = new Dictionary(); - RouteClaimsRequirement = new Dictionary(); + AddHeadersToRequest = new Dictionary(); AddQueriesToRequest = new Dictionary(); + AuthenticationOptions = new FileAuthenticationOptions(); ChangeDownstreamPathTemplate = new Dictionary(); + DelegatingHandlers = new List(); DownstreamHeaderTransform = new Dictionary(); + DownstreamHostAndPorts = new List(); FileCacheOptions = new FileCacheOptions(); - QoSOptions = new FileQoSOptions(); - RateLimitOptions = new FileRateLimitRule(); - AuthenticationOptions = new FileAuthenticationOptions(); HttpHandlerOptions = new FileHttpHandlerOptions(); - UpstreamHeaderTransform = new Dictionary(); - DownstreamHostAndPorts = new List(); - DelegatingHandlers = new List(); LoadBalancerOptions = new FileLoadBalancerOptions(); - SecurityOptions = new FileSecurityOptions(); Priority = 1; - } - - public string DownstreamPathTemplate { get; set; } - public string UpstreamPathTemplate { get; set; } - public List UpstreamHttpMethod { get; set; } - public string DownstreamHttpMethod { get; set; } - public Dictionary AddHeadersToRequest { get; set; } - public Dictionary UpstreamHeaderTransform { get; set; } - public Dictionary DownstreamHeaderTransform { get; set; } - public Dictionary AddClaimsToRequest { get; set; } - public Dictionary RouteClaimsRequirement { get; set; } - public Dictionary AddQueriesToRequest { get; set; } - public Dictionary ChangeDownstreamPathTemplate { get; set; } - public string RequestIdKey { get; set; } - public FileCacheOptions FileCacheOptions { get; set; } - public bool RouteIsCaseSensitive { get; set; } - public string ServiceName { get; set; } - public string ServiceNamespace { get; set; } - public string DownstreamScheme { get; set; } - public FileQoSOptions QoSOptions { get; set; } - public FileLoadBalancerOptions LoadBalancerOptions { get; set; } - public FileRateLimitRule RateLimitOptions { get; set; } - public FileAuthenticationOptions AuthenticationOptions { get; set; } - public FileHttpHandlerOptions HttpHandlerOptions { get; set; } - public List DownstreamHostAndPorts { get; set; } - public string UpstreamHost { get; set; } - public string Key { get; set; } - public List DelegatingHandlers { get; set; } - public int Priority { get; set; } - public int Timeout { get; set; } - public bool DangerousAcceptAnyServerCertificateValidator { get; set; } - public FileSecurityOptions SecurityOptions { get; set; } - public string DownstreamHttpVersion { get; set; } + QoSOptions = new FileQoSOptions(); + RateLimitOptions = new FileRateLimitRule(); + RouteClaimsRequirement = new Dictionary(); + SecurityOptions = new FileSecurityOptions(); + UpstreamHeaderTransform = new Dictionary(); + UpstreamHttpMethod = new List(); + } + + public FileRoute(FileRoute from) + { + DeepCopy(from, this); + } + + public Dictionary AddClaimsToRequest { get; set; } + public Dictionary AddHeadersToRequest { get; set; } + public Dictionary AddQueriesToRequest { get; set; } + public FileAuthenticationOptions AuthenticationOptions { get; set; } + public Dictionary ChangeDownstreamPathTemplate { get; set; } + public bool DangerousAcceptAnyServerCertificateValidator { get; set; } + public List DelegatingHandlers { get; set; } + public Dictionary DownstreamHeaderTransform { get; set; } + public List DownstreamHostAndPorts { get; set; } + public string DownstreamHttpMethod { get; set; } + public string DownstreamHttpVersion { get; set; } + public string DownstreamPathTemplate { get; set; } + public string DownstreamScheme { get; set; } + public FileCacheOptions FileCacheOptions { get; set; } + public FileHttpHandlerOptions HttpHandlerOptions { get; set; } + public string Key { get; set; } + public FileLoadBalancerOptions LoadBalancerOptions { get; set; } + public int Priority { get; set; } + public FileQoSOptions QoSOptions { get; set; } + public FileRateLimitRule RateLimitOptions { get; set; } + public string RequestIdKey { get; set; } + public Dictionary RouteClaimsRequirement { get; set; } + public bool RouteIsCaseSensitive { get; set; } + public FileSecurityOptions SecurityOptions { get; set; } + public string ServiceName { get; set; } + public string ServiceNamespace { get; set; } + public int Timeout { get; set; } + public Dictionary UpstreamHeaderTransform { get; set; } + public string UpstreamHost { get; set; } + public List UpstreamHttpMethod { get; set; } + public string UpstreamPathTemplate { get; set; } + + /// + /// Clones this object by making a deep copy. + /// + /// A deeply copied object. + public object Clone() + { + var other = (FileRoute)MemberwiseClone(); + DeepCopy(this, other); + return other; + } + + public static void DeepCopy(FileRoute from, FileRoute to) + { + to.AddClaimsToRequest = new(from.AddClaimsToRequest); + to.AddHeadersToRequest = new(from.AddHeadersToRequest); + to.AddQueriesToRequest = new(from.AddQueriesToRequest); + to.AuthenticationOptions = new(from.AuthenticationOptions); + to.ChangeDownstreamPathTemplate = new(from.ChangeDownstreamPathTemplate); + to.DangerousAcceptAnyServerCertificateValidator = from.DangerousAcceptAnyServerCertificateValidator; + to.DelegatingHandlers = new(from.DelegatingHandlers); + to.DownstreamHeaderTransform = new(from.DownstreamHeaderTransform); + to.DownstreamHostAndPorts = from.DownstreamHostAndPorts.Select(x => new FileHostAndPort(x)).ToList(); + to.DownstreamHttpMethod = from.DownstreamHttpMethod; + to.DownstreamHttpVersion = from.DownstreamHttpVersion; + to.DownstreamPathTemplate = from.DownstreamPathTemplate; + to.DownstreamScheme = from.DownstreamScheme; + to.FileCacheOptions = new(from.FileCacheOptions); + to.HttpHandlerOptions = new(from.HttpHandlerOptions); + to.Key = from.Key; + to.LoadBalancerOptions = new(from.LoadBalancerOptions); + to.Priority = from.Priority; + to.QoSOptions = new(from.QoSOptions); + to.RateLimitOptions = new(from.RateLimitOptions); + to.RequestIdKey = from.RequestIdKey; + to.RouteClaimsRequirement = new(from.RouteClaimsRequirement); + to.RouteIsCaseSensitive = from.RouteIsCaseSensitive; + to.SecurityOptions = new(from.SecurityOptions); + to.ServiceName = from.ServiceName; + to.ServiceNamespace = from.ServiceNamespace; + to.Timeout = from.Timeout; + to.UpstreamHeaderTransform = new(from.UpstreamHeaderTransform); + to.UpstreamHost = from.UpstreamHost; + to.UpstreamHttpMethod = new(from.UpstreamHttpMethod); + to.UpstreamPathTemplate = from.UpstreamPathTemplate; + } } } diff --git a/src/Ocelot/Configuration/File/FileSecurityOptions.cs b/src/Ocelot/Configuration/File/FileSecurityOptions.cs index 77d762b11..ffd595cb4 100644 --- a/src/Ocelot/Configuration/File/FileSecurityOptions.cs +++ b/src/Ocelot/Configuration/File/FileSecurityOptions.cs @@ -9,6 +9,13 @@ public FileSecurityOptions() ExcludeAllowedFromBlocked = false; } + public FileSecurityOptions(FileSecurityOptions from) + { + IPAllowedList = new(from.IPAllowedList); + IPBlockedList = new(from.IPBlockedList); + ExcludeAllowedFromBlocked = from.ExcludeAllowedFromBlocked; + } + public FileSecurityOptions(string allowedIPs = null, string blockedIPs = null, bool? excludeAllowedFromBlocked = null) : this() { diff --git a/src/Ocelot/Configuration/QoSOptions.cs b/src/Ocelot/Configuration/QoSOptions.cs index 461ff292a..915f07e2a 100644 --- a/src/Ocelot/Configuration/QoSOptions.cs +++ b/src/Ocelot/Configuration/QoSOptions.cs @@ -1,31 +1,41 @@ -namespace Ocelot.Configuration +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration { public class QoSOptions { + public QoSOptions(QoSOptions from) + { + DurationOfBreak = from.DurationOfBreak; + ExceptionsAllowedBeforeBreaking = from.ExceptionsAllowedBeforeBreaking; + Key = from.Key; + TimeoutValue = from.TimeoutValue; + } + + public QoSOptions(FileQoSOptions from) + { + DurationOfBreak = from.DurationOfBreak; + ExceptionsAllowedBeforeBreaking = from.ExceptionsAllowedBeforeBreaking; + Key = string.Empty; + TimeoutValue = from.TimeoutValue; + } + public QoSOptions( int exceptionsAllowedBeforeBreaking, - int durationofBreak, - int timeoutValue, - string key, - string timeoutStrategy = "Pessimistic") + int durationOfBreak, + int timeoutValue, + string key) { + DurationOfBreak = durationOfBreak; ExceptionsAllowedBeforeBreaking = exceptionsAllowedBeforeBreaking; - DurationOfBreak = durationofBreak; - TimeoutValue = timeoutValue; - TimeoutStrategy = timeoutStrategy; Key = key; + TimeoutValue = timeoutValue; } - public int ExceptionsAllowedBeforeBreaking { get; } - public int DurationOfBreak { get; } - + public int ExceptionsAllowedBeforeBreaking { get; } + public string Key { get; } public int TimeoutValue { get; } - - public string TimeoutStrategy { get; } - public bool UseQos => ExceptionsAllowedBeforeBreaking > 0 || TimeoutValue > 0; - - public string Key { get; } } } diff --git a/src/Ocelot/Requester/HttpExceptionToErrorMapper.cs b/src/Ocelot/Requester/HttpExceptionToErrorMapper.cs index 80b7b15e8..62b03cfa9 100644 --- a/src/Ocelot/Requester/HttpExceptionToErrorMapper.cs +++ b/src/Ocelot/Requester/HttpExceptionToErrorMapper.cs @@ -16,9 +16,9 @@ public Error Map(Exception exception) { var type = exception.GetType(); - if (_mappers != null && _mappers.ContainsKey(type)) + if (_mappers != null && _mappers.TryGetValue(type, out var mapper)) { - return _mappers[type](exception); + return mapper(exception); } if (type == typeof(OperationCanceledException) || type.IsSubclassOf(typeof(OperationCanceledException))) diff --git a/src/Ocelot/Requester/QoS/QosFactory.cs b/src/Ocelot/Requester/QoS/QosFactory.cs index 480116a74..be8569938 100644 --- a/src/Ocelot/Requester/QoS/QosFactory.cs +++ b/src/Ocelot/Requester/QoS/QosFactory.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Logging; @@ -8,12 +9,14 @@ namespace Ocelot.Requester.QoS public class QoSFactory : IQoSFactory { private readonly IServiceProvider _serviceProvider; - private readonly IOcelotLoggerFactory _ocelotLoggerFactory; + private readonly IOcelotLoggerFactory _ocelotLoggerFactory; + private readonly IHttpContextAccessor _contextAccessor; - public QoSFactory(IServiceProvider serviceProvider, IOcelotLoggerFactory ocelotLoggerFactory) + public QoSFactory(IServiceProvider serviceProvider, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory ocelotLoggerFactory) { _serviceProvider = serviceProvider; - _ocelotLoggerFactory = ocelotLoggerFactory; + _ocelotLoggerFactory = ocelotLoggerFactory; + _contextAccessor = contextAccessor; } public Response Get(DownstreamRoute request) @@ -22,7 +25,7 @@ public Response Get(DownstreamRoute request) if (handler != null) { - return new OkResponse(handler(request, _ocelotLoggerFactory)); + return new OkResponse(handler(request, _contextAccessor, _ocelotLoggerFactory)); } return new ErrorResponse(new UnableToFindQoSProviderError($"could not find qosProvider for {request.DownstreamScheme}{request.DownstreamAddresses}{request.DownstreamPathTemplate}")); diff --git a/src/Ocelot/Requester/QosDelegatingHandlerDelegate.cs b/src/Ocelot/Requester/QosDelegatingHandlerDelegate.cs index 992601cb0..5af77f60b 100644 --- a/src/Ocelot/Requester/QosDelegatingHandlerDelegate.cs +++ b/src/Ocelot/Requester/QosDelegatingHandlerDelegate.cs @@ -1,7 +1,8 @@ -using Ocelot.Configuration; -using Ocelot.Logging; +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using Ocelot.Logging; namespace Ocelot.Requester { - public delegate DelegatingHandler QosDelegatingHandlerDelegate(DownstreamRoute route, IOcelotLoggerFactory logger); + public delegate DelegatingHandler QosDelegatingHandlerDelegate(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory); } diff --git a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs index a8893a86e..44494da4e 100644 --- a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs +++ b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs @@ -1,276 +1,188 @@ using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; using Ocelot.Configuration.File; - -namespace Ocelot.AcceptanceTests -{ - public class PollyQoSTests : IDisposable - { - private readonly Steps _steps; - private int _requestCount; - private readonly ServiceHandler _serviceHandler; - - public PollyQoSTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } - - [Fact] - public void Should_not_timeout() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Post" }, - QoSOptions = new FileQoSOptions - { - TimeoutValue = 1000, - ExceptionsAllowedBeforeBreaking = 10, - }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, string.Empty, 10)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - [Fact] - public void Should_timeout() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - DownstreamScheme = "http", - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Post" }, - QoSOptions = new FileQoSOptions - { - TimeoutValue = 10, - }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 201, string.Empty, 1000)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .BDDfy(); - } - - [Fact] - public void Should_open_circuit_breaker_then_close() - { - var port = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port, - }, - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - QoSOptions = new FileQoSOptions - { - ExceptionsAllowedBeforeBreaking = 1, - TimeoutValue = 500, - DurationOfBreak = 1000, - }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port}", "Hello from Laura")) - .Given(x => _steps.GivenThereIsAConfiguration(configuration)) - .Given(x => _steps.GivenOcelotIsRunningWithPolly()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => GivenIWaitMilliseconds(3000)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void Open_circuit_should_not_effect_different_route() - { - var port1 = PortFinder.GetRandomPort(); - var port2 = PortFinder.GetRandomPort(); - - var configuration = new FileConfiguration - { - Routes = new List - { - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port1, - }, - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new List { "Get" }, - QoSOptions = new FileQoSOptions - { - ExceptionsAllowedBeforeBreaking = 1, - TimeoutValue = 500, - DurationOfBreak = 1000, - }, - }, - new() - { - DownstreamPathTemplate = "/", - DownstreamScheme = "http", - DownstreamHostAndPorts = new List - { - new() - { - Host = "localhost", - Port = port2, - }, - }, - UpstreamPathTemplate = "/working", - UpstreamHttpMethod = new List { "Get" }, - }, - }, - }; - - this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port1}", "Hello from Laura")) - .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port2}/", 200, "Hello from Tom", 0)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/working")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => GivenIWaitMilliseconds(3000)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - private static void GivenIWaitMilliseconds(int ms) - { - Thread.Sleep(ms); - } - - private void GivenThereIsAPossiblyBrokenServiceRunningOn(string url, string responseBody) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - //circuit starts closed - if (_requestCount == 0) - { - _requestCount++; - context.Response.StatusCode = 200; - await context.Response.WriteAsync(responseBody); - return; - } - - //request one times out and polly throws exception, circuit opens - if (_requestCount == 1) - { - _requestCount++; - await Task.Delay(1000); - context.Response.StatusCode = 200; - return; - } - - //after break closes we return 200 OK - if (_requestCount == 2) - { - context.Response.StatusCode = 200; - await context.Response.WriteAsync(responseBody); - } - }); - } - - private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, int timeout) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - Thread.Sleep(timeout); - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - } - - public void Dispose() - { - _serviceHandler?.Dispose(); - _steps.Dispose(); - GC.SuppressFinalize(this); - } - } + +namespace Ocelot.AcceptanceTests; + +public class PollyQoSTests : IDisposable +{ + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + + public PollyQoSTests() + { + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + private static FileConfiguration FileConfigurationFactory(int port, QoSOptions options, string httpMethod = nameof(HttpMethods.Get)) => new() + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = Uri.UriSchemeHttp, + DownstreamHostAndPorts = new() + { + new("localhost", port), + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new() { httpMethod }, + QoSOptions = new FileQoSOptions(options), + }, + }, + }; + + [Fact] + public void Should_not_timeout() + { + var port = PortFinder.GetRandomPort(); + var configuration = FileConfigurationFactory(port, new QoSOptions(10, 0, 1000, null), HttpMethods.Post); + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, string.Empty, 10)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithPolly()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void Should_timeout() + { + var port = PortFinder.GetRandomPort(); + var configuration = FileConfigurationFactory(port, new QoSOptions(0, 0, 10, null), HttpMethods.Post); + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 201, string.Empty, 1000)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithPolly()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .BDDfy(); + } + + [Fact] + public void Should_open_circuit_breaker_after_two_exceptions() + { + var port = PortFinder.GetRandomPort(); + var configuration = FileConfigurationFactory(port, new QoSOptions(2, 5000, 100000, null)); + + this.Given(x => x.GivenThereIsABrokenServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithPolly()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .BDDfy(); + } + + [Fact] + public void Should_open_circuit_breaker_then_close() + { + var port = PortFinder.GetRandomPort(); + var configuration = FileConfigurationFactory(port, new QoSOptions(1, 500, 1000, null)); + + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port}", "Hello from Laura")) + .Given(x => _steps.GivenThereIsAConfiguration(configuration)) + .Given(x => _steps.GivenOcelotIsRunningWithPolly()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => GivenIWaitMilliseconds(3000)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void Open_circuit_should_not_effect_different_route() + { + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var qos1 = new QoSOptions(1, 1000, 500, null); + + var configuration = FileConfigurationFactory(port1, qos1); + var route2 = configuration.Routes[0].Clone() as FileRoute; + route2.DownstreamHostAndPorts[0].Port = port2; + route2.UpstreamPathTemplate = "/working"; + route2.QoSOptions = new(); + configuration.Routes.Add(route2); + + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port1}", "Hello from Laura")) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port2}/", 200, "Hello from Tom", 0)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithPolly()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/working")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => GivenIWaitMilliseconds(3000)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + private static void GivenIWaitMilliseconds(int ms) => Thread.Sleep(ms); + + private void GivenThereIsABrokenServiceRunningOn(string url) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + context.Response.StatusCode = 500; + await context.Response.WriteAsync("this is an exception"); + }); + } + + private void GivenThereIsAPossiblyBrokenServiceRunningOn(string url, string responseBody) + { + var requestCount = 0; + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + if (requestCount == 1) + { + await Task.Delay(1000); + } + + requestCount++; + context.Response.StatusCode = 200; + await context.Response.WriteAsync(responseBody); + }); + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, int timeout) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + Thread.Sleep(timeout); + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + GC.SuppressFinalize(this); + } } diff --git a/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs b/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs index c60ae243d..454b41455 100644 --- a/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs +++ b/test/Ocelot.AcceptanceTests/ReturnsErrorTests.cs @@ -14,7 +14,7 @@ public ReturnsErrorTests() } [Fact] - public void should_return_bad_gateway_error_if_downstream_service_doesnt_respond() + public void Should_return_bad_gateway_error_if_downstream_service_doesnt_respond() { var configuration = new FileConfiguration { @@ -46,7 +46,7 @@ public void should_return_bad_gateway_error_if_downstream_service_doesnt_respond } [Fact] - public void should_return_internal_server_error_if_downstream_service_returns_internal_server_error() + public void Should_return_internal_server_error_if_downstream_service_returns_internal_server_error() { var port = PortFinder.GetRandomPort(); @@ -81,7 +81,7 @@ public void should_return_internal_server_error_if_downstream_service_returns_in } [Fact] - public void should_log_warning_if_downstream_service_returns_internal_server_error() + public void Should_log_warning_if_downstream_service_returns_internal_server_error() { var port = PortFinder.GetRandomPort(); @@ -111,7 +111,7 @@ public void should_log_warning_if_downstream_service_returns_internal_server_err .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunningWithLogger()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenWarningShouldBeLogged()) + .Then(x => _steps.ThenWarningShouldBeLogged(2)) .BDDfy(); } @@ -123,7 +123,8 @@ private void GivenThereIsAServiceRunningOn(string url) public void Dispose() { _serviceHandler?.Dispose(); - _steps.Dispose(); + _steps.Dispose(); + GC.SuppressFinalize(this); } } } diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 0686dd036..fb78fa6e7 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -1243,10 +1243,10 @@ internal void GivenOcelotIsRunningUsingOpenTracing(OpenTracing.ITracer fakeTrace _ocelotClient = _ocelotServer.CreateClient(); } - public void ThenWarningShouldBeLogged() + public void ThenWarningShouldBeLogged(int howMany) { var loggerFactory = (MockLoggerFactory)_ocelotServer.Host.Services.GetService(); - loggerFactory.Verify(); + loggerFactory.Verify(Times.Exactly(howMany)); } internal class MockLoggerFactory : IOcelotLoggerFactory @@ -1264,9 +1264,9 @@ public IOcelotLogger CreateLogger() return _logger.Object; } - public void Verify() + public void Verify(Times howMany) { - _logger.Verify(x => x.LogWarning(It.IsAny()), Times.Once); + _logger.Verify(x => x.LogWarning(It.IsAny()), howMany); } } diff --git a/test/Ocelot.ManualTest/Program.cs b/test/Ocelot.ManualTest/Program.cs index 5337f6b7f..135e7e2c7 100644 --- a/test/Ocelot.ManualTest/Program.cs +++ b/test/Ocelot.ManualTest/Program.cs @@ -33,7 +33,7 @@ public static void Main(string[] args) x.Audience = "test"; });*/ - s.AddSingleton((x, t) => new FakeHandler()); + s.AddSingleton((x, t, z) => new FakeHandler()); s.AddOcelot() .AddDelegatingHandler(true); /*.AddCacheManager(x => diff --git a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs index 98bfd1ea0..f51a96068 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/FileConfigurationFluentValidatorTests.cs @@ -1,9 +1,12 @@ using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.Configuration.Validator; +using Ocelot.Logging; using Ocelot.Requester; using Ocelot.Responses; using Ocelot.ServiceDiscovery; @@ -11,7 +14,7 @@ using Ocelot.UnitTests.Requester; using Ocelot.Values; using System.Security.Claims; -using System.Text.Encodings.Web; +using System.Text.Encodings.Web; namespace Ocelot.UnitTests.Configuration.Validation { @@ -1536,8 +1539,8 @@ private void GivenTheAuthSchemeExists(string name) private void GivenAQoSHandler() { var collection = new ServiceCollection(); - QosDelegatingHandlerDelegate del = (a, b) => new FakeDelegatingHandler(); - collection.AddSingleton(del); + DelegatingHandler Del(DownstreamRoute a, IHttpContextAccessor b, IOcelotLoggerFactory c) => new FakeDelegatingHandler(); + collection.AddSingleton((QosDelegatingHandlerDelegate)Del); var provider = collection.BuildServiceProvider(); _configurationValidator = new FileConfigurationFluentValidator(provider, new RouteFluentValidator(_authProvider.Object, new HostAndPortValidator(), new FileQoSOptionsFluentValidator(provider)), new FileGlobalConfigurationFluentValidator(new FileQoSOptionsFluentValidator(provider))); } diff --git a/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs index 9cc3a2d0e..69a7401e9 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/FileQoSOptionsFluentValidatorTests.cs @@ -1,7 +1,10 @@ -using FluentValidation.Results; -using Microsoft.Extensions.DependencyInjection; +using FluentValidation.Results; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.Configuration.Validator; +using Ocelot.Logging; using Ocelot.Requester; namespace Ocelot.UnitTests.Configuration.Validation @@ -73,11 +76,8 @@ private void ThenTheResultIsInValid() private void GivenAQosDelegate() { - QosDelegatingHandlerDelegate fake = (a, b) => - { - return null; - }; - _services.AddSingleton(fake); + DelegatingHandler Fake(DownstreamRoute a, IHttpContextAccessor b, IOcelotLoggerFactory c) => null; + _services.AddSingleton((QosDelegatingHandlerDelegate)Fake); var provider = _services.BuildServiceProvider(); _validator = new FileQoSOptionsFluentValidator(provider); } diff --git a/test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs b/test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs index 4cdd930fa..81147b56a 100644 --- a/test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Polly/OcelotBuilderExtensionsTests.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Configuration; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration.Builder; using Ocelot.DependencyInjection; @@ -13,7 +14,8 @@ public class OcelotBuilderExtensionsTests [Fact] public void Should_build() { - var loggerFactory = new Mock(); + var loggerFactory = new Mock(); + var contextAccessor = new Mock(); var services = new ServiceCollection(); var options = new QoSOptionsBuilder() .WithTimeoutValue(100) @@ -34,7 +36,7 @@ public void Should_build() var handler = provider.GetService(); handler.ShouldNotBeNull(); - var delgatingHandler = handler(route, loggerFactory.Object); + var delgatingHandler = handler(route, contextAccessor.Object, loggerFactory.Object); delgatingHandler.ShouldNotBeNull(); } } diff --git a/test/Ocelot.UnitTests/Polly/PollyCircuitBreakingDelegatingHandlerTests.cs b/test/Ocelot.UnitTests/Polly/PollyCircuitBreakingDelegatingHandlerTests.cs deleted file mode 100644 index 421ee594d..000000000 --- a/test/Ocelot.UnitTests/Polly/PollyCircuitBreakingDelegatingHandlerTests.cs +++ /dev/null @@ -1,153 +0,0 @@ -using Moq; -using Ocelot.Logging; -using Ocelot.Provider.Polly; -using Ocelot.Provider.Polly.Interfaces; -using Polly; -using Polly.Wrap; -using Shouldly; -using System; -using System.Net; -using System.Net.Http; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace Ocelot.UnitTests.Polly; - -public class PollyCircuitBreakingDelegatingHandlerTests -{ - private readonly Mock pollyQoSProviderMock; - private readonly Mock loggerFactoryMock; - private readonly Mock loggerMock; - - private readonly PollyCircuitBreakingDelegatingHandler sut; - - public PollyCircuitBreakingDelegatingHandlerTests() - { - pollyQoSProviderMock = new Mock(); - - loggerFactoryMock = new Mock(); - loggerMock = new Mock(); - - loggerFactoryMock.Setup(x => x.CreateLogger()) - .Returns(loggerMock.Object); - loggerMock.Setup(x => x.LogError(It.IsAny(), It.IsAny())); - - sut = new PollyCircuitBreakingDelegatingHandler(pollyQoSProviderMock.Object, loggerFactoryMock.Object); - } - - [Fact] - public async void SendAsync_OnePolicy_NoWrapping() - { - // Arrange - var fakeResponse = new HttpResponseMessage(HttpStatusCode.NoContent); - fakeResponse.Headers.Add("X-Xunit", nameof(SendAsync_OnePolicy_NoWrapping)); - - MethodInfo method = null; - var onePolicy = new Mock(); - onePolicy.Setup(x => x.ExecuteAsync(It.IsAny>>())) - .Callback((IInvocation x) => method = x.Method) - .ReturnsAsync(fakeResponse); - - pollyQoSProviderMock.SetupGet(x => x.CircuitBreaker) - .Returns(new CircuitBreaker(onePolicy.Object)); - - // Act - var actual = await InvokeAsync("SendAsync"); - - // Assert - ShouldHaveXunitHeaderWithNoContent(actual, nameof(SendAsync_OnePolicy_NoWrapping)); - method.DeclaringType.Name.ShouldBe(nameof(IAsyncPolicy)); - method.DeclaringType.ShouldNotBeOfType(); - } - - [Fact] - public async void SendAsync_TwoPolicies_HaveWrapped() - { - // Arrange - var fakeResponse = new HttpResponseMessage(HttpStatusCode.NoContent); - fakeResponse.Headers.Add("X-Xunit", nameof(SendAsync_TwoPolicies_HaveWrapped)); - - var policy1 = new FakeAsyncPolicy("Policy1", fakeResponse); - var policy2 = new FakeAsyncPolicy("Policy2", fakeResponse) - { - IsLast = true, - }; - - pollyQoSProviderMock.SetupGet(x => x.CircuitBreaker) - .Returns(new CircuitBreaker(policy1, policy2)); - - // Act - var actual = await InvokeAsync("SendAsync"); - - // Assert - ShouldHaveXunitHeaderWithNoContent(actual, nameof(SendAsync_TwoPolicies_HaveWrapped)); - ShouldBeWrappedBy(policy1, typeof(AsyncPolicyWrap).FullName); - ShouldBeWrappedBy(policy2, typeof(AsyncPolicy).FullName); - } - - private static void ShouldHaveXunitHeaderWithNoContent(HttpResponseMessage actual, string headerName) - { - actual.ShouldNotBeNull(); - actual.StatusCode.ShouldBe(HttpStatusCode.NoContent); - actual.Headers.GetValues("X-Xunit").ShouldContain(headerName); - } - - private static void ShouldBeWrappedBy(FakeAsyncPolicy policy, string wrapperName) - { - policy.Called.ShouldBeTrue(); - policy.Times.ShouldBe(1); - policy.Method.ShouldNotBeNull(); - policy.Target.ShouldNotBeNull(); - policy.Method.DeclaringType?.DeclaringType.ShouldNotBeNull(); - policy.Method.DeclaringType.DeclaringType.FullName.ShouldContain(wrapperName); - policy.Target.ToString().ShouldContain(wrapperName); - } - - private async Task InvokeAsync(string methodName) - { - var m = typeof(PollyCircuitBreakingDelegatingHandler).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); - var task = (Task)m.Invoke(sut, new object[] { new HttpRequestMessage(), CancellationToken.None }); - var actual = await task; - return actual; - } - - internal class FakeAsyncPolicy : AsyncPolicy, IAsyncPolicy - { - public object Result { get; private set; } - public string Name { get; private set; } - - public int Times { get; protected set; } - public bool Called => Times > 0; - public MethodInfo Method { get; protected set; } - public object Target { get; protected set; } - - public bool IsLast { get; set; } - - public FakeAsyncPolicy(string name, object result) - { - Name = name; - Result = result; - } - - protected override async Task ImplementationAsync(Func> action, - Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) - { - Times++; - Method = action.Method; - Target = action.Target; - - if (IsLast) - { - TResult r = Result?.GetType() == typeof(TResult) - ? (TResult)Result - : Activator.CreateInstance(); - return r; - } - - TResult result = await action(context, cancellationToken); - return result; - } - } -} diff --git a/test/Ocelot.UnitTests/Polly/PollyPoliciesDelegatingHandlerTests.cs b/test/Ocelot.UnitTests/Polly/PollyPoliciesDelegatingHandlerTests.cs new file mode 100644 index 000000000..872fa635a --- /dev/null +++ b/test/Ocelot.UnitTests/Polly/PollyPoliciesDelegatingHandlerTests.cs @@ -0,0 +1,255 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using Ocelot.Configuration.Builder; +using Ocelot.Logging; +using Ocelot.Provider.Polly; +using Ocelot.Provider.Polly.Interfaces; +using Polly; +using Polly.Wrap; +using System.Reflection; + +namespace Ocelot.UnitTests.Polly; + +public class PollyPoliciesDelegatingHandlerTests +{ + private readonly Mock> _pollyQoSProviderMock; + private readonly Mock _contextAccessorMock; + private readonly PollyPoliciesDelegatingHandler _sut; + + public PollyPoliciesDelegatingHandlerTests() + { + _pollyQoSProviderMock = new Mock>(); + + var loggerFactoryMock = new Mock(); + var loggerMock = new Mock(); + _contextAccessorMock = new Mock(); + + loggerFactoryMock.Setup(x => x.CreateLogger()) + .Returns(loggerMock.Object); + loggerMock.Setup(x => x.LogError(It.IsAny(), It.IsAny())); + + _sut = new PollyPoliciesDelegatingHandler(DownstreamRouteFactory(), _contextAccessorMock.Object, loggerFactoryMock.Object); + } + + [Fact] + public async void SendAsync_OnePolicy_NoWrapping() + { + // Arrange + var fakeResponse = new HttpResponseMessage(HttpStatusCode.NoContent); + fakeResponse.Headers.Add("X-Xunit", nameof(SendAsync_OnePolicy_NoWrapping)); + + MethodInfo method = null; + var onePolicy = new Mock>(); + onePolicy.Setup(x => x.ExecuteAsync(It.IsAny>>())) + .Callback((IInvocation x) => method = x.Method) + .ReturnsAsync(fakeResponse); + + _pollyQoSProviderMock.Setup(x => x.GetPollyPolicyWrapper(It.IsAny())) + .Returns(new PollyPolicyWrapper(onePolicy.Object)); + + var httpContext = new Mock(); + httpContext.Setup(x => x.RequestServices.GetService(typeof(IPollyQoSProvider))) + .Returns(_pollyQoSProviderMock.Object); + + _contextAccessorMock.Setup(x => x.HttpContext).Returns(httpContext.Object); + + // Act + var actual = await InvokeAsync("SendAsync"); + + // Assert + ShouldHaveXunitHeaderWithNoContent(actual, nameof(SendAsync_OnePolicy_NoWrapping)); + method.DeclaringType.Name.ShouldBe("IAsyncPolicy`1"); + method.DeclaringType.ShouldNotBeOfType(); + } + + [Fact] + public async void SendAsync_TwoPolicies_HaveWrapped() + { + // Arrange + var fakeResponse = new HttpResponseMessage(HttpStatusCode.NoContent); + fakeResponse.Headers.Add("X-Xunit", nameof(SendAsync_TwoPolicies_HaveWrapped)); + + var policy1 = new FakeAsyncPolicy("Policy1", fakeResponse); + var policy2 = new FakeAsyncPolicy("Policy2", fakeResponse) + { + IsLast = true, + }; + + _pollyQoSProviderMock.Setup(x => x.GetPollyPolicyWrapper(It.IsAny())) + .Returns(new PollyPolicyWrapper(policy1, policy2)); + + var httpContext = new Mock(); + httpContext.Setup(x => x.RequestServices.GetService(typeof(IPollyQoSProvider))) + .Returns(_pollyQoSProviderMock.Object); + + _contextAccessorMock.Setup(x => x.HttpContext).Returns(httpContext.Object); + + // Act + var actual = await InvokeAsync("SendAsync"); + + // Assert + ShouldHaveXunitHeaderWithNoContent(actual, nameof(SendAsync_TwoPolicies_HaveWrapped)); + ShouldBeWrappedBy(policy1, typeof(AsyncPolicyWrap).FullName); + ShouldBeWrappedBy(policy2, typeof(AsyncPolicy).FullName); + } + + private static DownstreamRoute DownstreamRouteFactory() + { + var options = new QoSOptionsBuilder() + .WithTimeoutValue(100) + .WithExceptionsAllowedBeforeBreaking(2) + .WithDurationOfBreak(200) + .Build(); + + var upstreamPath = new UpstreamPathTemplateBuilder() + .WithTemplate("/") + .WithContainsQueryString(false) + .WithPriority(1) + .WithOriginalValue("/").Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(options) + .WithUpstreamPathTemplate(upstreamPath).Build(); + + return route; + } + + private static void ShouldHaveXunitHeaderWithNoContent(HttpResponseMessage actual, string headerName) + { + actual.ShouldNotBeNull(); + actual.StatusCode.ShouldBe(HttpStatusCode.NoContent); + actual.Headers.GetValues("X-Xunit").ShouldContain(headerName); + } + + private static void ShouldBeWrappedBy(FakeAsyncPolicy policy, string wrapperName) + { + policy.Called.ShouldBeTrue(); + policy.Times.ShouldBe(1); + policy.Method.ShouldNotBeNull(); + policy.Target.ShouldNotBeNull(); + policy.Method.DeclaringType?.DeclaringType.ShouldNotBeNull(); + policy.Method.DeclaringType.DeclaringType.FullName.ShouldContain(wrapperName); + policy.Target.ToString().ShouldContain(wrapperName); + } + + private async Task InvokeAsync(string methodName) + { + var m = typeof(PollyPoliciesDelegatingHandler).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); + var task = (Task)m.Invoke(_sut, new object[] { new HttpRequestMessage(), CancellationToken.None }); + var actual = await task; + return actual; + } + + internal class FakeAsyncPolicy : AsyncPolicy, IAsyncPolicy + where TResult : class + { + public object Result { get; private set; } + public string Name { get; private set; } + + public int Times { get; protected set; } + public bool Called => Times > 0; + public MethodInfo Method { get; protected set; } + public object Target { get; protected set; } + + public bool IsLast { get; set; } + + public FakeAsyncPolicy(string name, object result) + { + Name = name; + Result = result; + } + + protected override async Task ImplementationAsync(Func> action, Context context, CancellationToken cancellationToken, + bool continueOnCapturedContext) + { + Times++; + Method = action.Method; + Target = action.Target; + + if (IsLast) + { + var r = Result?.GetType() == typeof(TResult) + ? (TResult)Result + : Activator.CreateInstance(); + return r; + } + + var result = await action(context, cancellationToken); + return result; + } + + public new IAsyncPolicy WithPolicyKey(string policyKey) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func action) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func action, IDictionary contextData) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func action, Context context) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func action, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func action, IDictionary contextData, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func action, Context context, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func action, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func action, IDictionary contextData, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func> action) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func> action, Context context) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func> action, IDictionary contextData) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func> action, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func> action, IDictionary contextData, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func> action, Context context, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func> action, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func> action, IDictionary contextData, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task ExecuteAsync(Func> action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task ExecuteAndCaptureAsync(Func action) => throw new NotImplementedException(); + + public Task ExecuteAndCaptureAsync(Func action, IDictionary contextData) => throw new NotImplementedException(); + + public Task ExecuteAndCaptureAsync(Func action, Context context) => throw new NotImplementedException(); + + public Task ExecuteAndCaptureAsync(Func action, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ExecuteAndCaptureAsync(Func action, IDictionary contextData, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ExecuteAndCaptureAsync(Func action, Context context, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task ExecuteAndCaptureAsync(Func action, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task ExecuteAndCaptureAsync(Func action, IDictionary contextData, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task ExecuteAndCaptureAsync(Func action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task> ExecuteAndCaptureAsync(Func> action) => throw new NotImplementedException(); + + public Task> ExecuteAndCaptureAsync(Func> action, IDictionary contextData) => throw new NotImplementedException(); + + public Task> ExecuteAndCaptureAsync(Func> action, Context context) => throw new NotImplementedException(); + + public Task> ExecuteAndCaptureAsync(Func> action, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task> ExecuteAndCaptureAsync(Func> action, IDictionary contextData, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task> ExecuteAndCaptureAsync(Func> action, Context context, CancellationToken cancellationToken) => throw new NotImplementedException(); + + public Task> ExecuteAndCaptureAsync(Func> action, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task> ExecuteAndCaptureAsync(Func> action, IDictionary contextData, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + + public Task> ExecuteAndCaptureAsync(Func> action, Context context, CancellationToken cancellationToken, bool continueOnCapturedContext) => throw new NotImplementedException(); + } +} diff --git a/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs b/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs index 537712e01..c1c056e28 100644 --- a/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs +++ b/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs @@ -1,30 +1,243 @@ using Ocelot.Configuration.Builder; using Ocelot.Logging; using Ocelot.Provider.Polly; +using Polly; using Polly.CircuitBreaker; using Polly.Timeout; +using Polly.Wrap; -namespace Ocelot.UnitTests.Polly -{ - public class PollyQoSProviderTests - { - [Fact] - public void Should_build() - { - var options = new QoSOptionsBuilder() - .WithTimeoutValue(100) - .WithExceptionsAllowedBeforeBreaking(1) - .WithDurationOfBreak(200) - .Build(); - var route = new DownstreamRouteBuilder().WithQosOptions(options) - .Build(); - var factory = new Mock(); - var pollyQoSProvider = new PollyQoSProvider(route, factory.Object); - var policies = pollyQoSProvider.CircuitBreaker.ShouldNotBeNull() - .Policies.ShouldNotBeNull(); - policies.Length.ShouldBeGreaterThan(0); - policies.ShouldContain(p => p is AsyncCircuitBreakerPolicy); - policies.ShouldContain(p => p is AsyncTimeoutPolicy); - } - } +namespace Ocelot.UnitTests.Polly; + +public class PollyQoSProviderTests +{ + [Fact] + public void Should_build() + { + var options = new QoSOptionsBuilder() + .WithTimeoutValue(100) + .WithExceptionsAllowedBeforeBreaking(1) + .WithDurationOfBreak(200) + .Build(); + var route = new DownstreamRouteBuilder().WithQosOptions(options) + .Build(); + var factory = new Mock(); + var pollyQoSProvider = new PollyQoSProvider(factory.Object); + var policy = pollyQoSProvider.GetPollyPolicyWrapper(route).ShouldNotBeNull() + .AsyncPollyPolicy.ShouldNotBeNull(); + policy.ShouldNotBeNull(); + } + + [Fact] + public void should_build_and_wrap_contains_two_policies() + { + var pollyQosProvider = PollyQoSProviderFactory(); + var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider); + var policy = pollyPolicyWrapper.AsyncPollyPolicy; + + if (policy is AsyncPolicyWrap policyWrap) + { + policyWrap.ShouldNotBeNull(); + var policies = policyWrap.GetPolicies().ToList(); + + policies.Count.ShouldBe(2); + var circuitBreakerFound = false; + var timeoutPolicyFound = false; + + foreach(var currentPolicy in policies) + { + currentPolicy.ShouldNotBeNull(); + var convertedPolicy = (IAsyncPolicy)currentPolicy; + + switch (convertedPolicy) + { + case AsyncCircuitBreakerPolicy: + circuitBreakerFound = true; + continue; + case AsyncTimeoutPolicy: + timeoutPolicyFound = true; + break; + } + } + + Assert.True(circuitBreakerFound); + Assert.True(timeoutPolicyFound); + + return; + } + + Assert.Fail("policy is not AsyncPolicyWrap"); + } + + [Fact] + public void should_build_and_contains_one_policy_when_with_exceptions_allowed_before_breaking_is_zero() + { + var pollyQosProvider = PollyQoSProviderFactory(); + var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider, true); + var policy = pollyPolicyWrapper.AsyncPollyPolicy; + + if (policy is AsyncTimeoutPolicy convertedPolicy) + { + convertedPolicy.ShouldNotBeNull(); + return; + } + + Assert.Fail("policy is not AsyncTimeoutPolicy"); + } + + [Fact] + public void should_return_same_circuit_breaker_for_given_route() + { + var pollyQosProvider = PollyQoSProviderFactory(); + var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider); + var pollyPolicyWrapper2 = PolicyWrapperFactory("/", pollyQosProvider); + pollyPolicyWrapper.ShouldBe(pollyPolicyWrapper2); + } + + [Fact] + public void should_return_different_circuit_breaker_for_two_different_routes() + { + var pollyQosProvider = PollyQoSProviderFactory(); + var pollyPolicyWrapper = PolicyWrapperFactory("/", pollyQosProvider); + var pollyPolicyWrapper2 = PolicyWrapperFactory("/test", pollyQosProvider); + pollyPolicyWrapper.ShouldNotBe(pollyPolicyWrapper2); + } + + [Theory] + [InlineData(HttpStatusCode.InternalServerError)] + [InlineData(HttpStatusCode.NotImplemented)] + [InlineData(HttpStatusCode.BadGateway)] + [InlineData(HttpStatusCode.ServiceUnavailable)] + [InlineData(HttpStatusCode.GatewayTimeout)] + [InlineData(HttpStatusCode.HttpVersionNotSupported)] + [InlineData(HttpStatusCode.VariantAlsoNegotiates)] + [InlineData(HttpStatusCode.InsufficientStorage)] + [InlineData(HttpStatusCode.LoopDetected)] + public async Task should_throw_broken_circuit_exception_after_two_exceptions(HttpStatusCode errorCode) + { + var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); + + var response = new HttpResponseMessage(errorCode); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await Assert.ThrowsAsync>(async () => + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); + } + + [Fact] + public async Task should_not_throw_broken_circuit_exception_if_status_code_ok() + { + var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); + + var response = new HttpResponseMessage(HttpStatusCode.OK); + Assert.Equal(HttpStatusCode.OK, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); + Assert.Equal(HttpStatusCode.OK, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); + Assert.Equal(HttpStatusCode.OK, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); + } + + [Fact] + public async Task should_throw_and_before_delay_should_not_allow_requests() + { + var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); + + var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await Assert.ThrowsAsync>(async () => + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); + + await Task.Delay(100); + + await Assert.ThrowsAsync>(async () => + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); + } + + [Fact] + public async Task should_throw_but_after_delay_should_allow_one_more_internal_server_error() + { + var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); + + var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await Assert.ThrowsAsync>(async () => + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); + + await Task.Delay(500); + + Assert.Equal(HttpStatusCode.InternalServerError, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); + } + + [Fact] + public async Task should_throw_but_after_delay_should_allow_one_more_internal_server_error_and_throw() + { + var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); + + var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await Assert.ThrowsAsync>(async () => + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); + + await Task.Delay(500); + + Assert.Equal(HttpStatusCode.InternalServerError, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); + await Assert.ThrowsAsync>(async () => + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); + } + + [Fact] + public async Task should_throw_but_after_delay_should_allow_one_more_ok_request_and_put_counter_back_to_zero() + { + var pollyPolicyWrapper = PolicyWrapperFactory("/", PollyQoSProviderFactory()); + + var response = new HttpResponseMessage(HttpStatusCode.InternalServerError); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await Assert.ThrowsAsync>(async () => + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); + + await Task.Delay(500); + + var response2 = new HttpResponseMessage(HttpStatusCode.OK); + Assert.Equal(HttpStatusCode.OK, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response2))).StatusCode); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response)); + await Assert.ThrowsAsync>(async () => + await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); + } + + private PollyQoSProvider PollyQoSProviderFactory() + { + var factory = new Mock(); + factory.Setup(x => x.CreateLogger()) + .Returns(new Mock().Object); + + var pollyQoSProvider = new PollyQoSProvider(factory.Object); + return pollyQoSProvider; + } + + private static PollyPolicyWrapper PolicyWrapperFactory(string routeTemplate, PollyQoSProvider pollyQoSProvider, bool inactiveExceptionsAllowedBeforeBreaking = false) + { + var options = new QoSOptionsBuilder() + .WithTimeoutValue(5000) + .WithExceptionsAllowedBeforeBreaking(inactiveExceptionsAllowedBeforeBreaking ? 0 : 2) + .WithDurationOfBreak(200) + .Build(); + + var upstreamPath = new UpstreamPathTemplateBuilder() + .WithTemplate(routeTemplate) + .WithContainsQueryString(false) + .WithPriority(1) + .WithOriginalValue(routeTemplate).Build(); + + var route = new DownstreamRouteBuilder() + .WithQosOptions(options) + .WithUpstreamPathTemplate(upstreamPath).Build(); + + var pollyPolicyWrapper = pollyQoSProvider.GetPollyPolicyWrapper(route).ShouldNotBeNull(); + pollyPolicyWrapper.ShouldNotBeNull(); + pollyPolicyWrapper.AsyncPollyPolicy.ShouldNotBeNull(); + + return pollyPolicyWrapper; + } } diff --git a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs index de1adc2bf..ee3c592e2 100644 --- a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs @@ -24,7 +24,7 @@ public class DelegatingHandlerHandlerProviderFactoryTests public DelegatingHandlerHandlerProviderFactoryTests() { - _qosDelegate = (a, b) => new FakeQoSHandler(); + _qosDelegate = (a, b, c) => new FakeQoSHandler(); _tracingFactory = new Mock(); _qosFactory = new Mock(); _loggerFactory = new Mock(); diff --git a/test/Ocelot.UnitTests/Requester/QoSFactoryTests.cs b/test/Ocelot.UnitTests/Requester/QoSFactoryTests.cs index 4251befd4..a6a69c28f 100644 --- a/test/Ocelot.UnitTests/Requester/QoSFactoryTests.cs +++ b/test/Ocelot.UnitTests/Requester/QoSFactoryTests.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Ocelot.Configuration; using Ocelot.Configuration.Builder; using Ocelot.Logging; @@ -11,14 +12,16 @@ public class QoSFactoryTests { private QoSFactory _factory; private ServiceCollection _services; - private readonly Mock _loggerFactory; + private readonly Mock _loggerFactory; + private readonly Mock _contextAccessor; public QoSFactoryTests() { _services = new ServiceCollection(); - _loggerFactory = new Mock(); + _loggerFactory = new Mock(); + _contextAccessor = new Mock(); var provider = _services.BuildServiceProvider(); - _factory = new QoSFactory(provider, _loggerFactory.Object); + _factory = new QoSFactory(provider, _contextAccessor.Object, _loggerFactory.Object); } [Fact] @@ -34,10 +37,10 @@ public void should_return_error() public void should_return_handler() { _services = new ServiceCollection(); - DelegatingHandler QosDelegatingHandlerDelegate(DownstreamRoute a, IOcelotLoggerFactory b) => new FakeDelegatingHandler(); + DelegatingHandler QosDelegatingHandlerDelegate(DownstreamRoute a, IHttpContextAccessor b, IOcelotLoggerFactory c) => new FakeDelegatingHandler(); _services.AddSingleton(QosDelegatingHandlerDelegate); var provider = _services.BuildServiceProvider(); - _factory = new QoSFactory(provider, _loggerFactory.Object); + _factory = new QoSFactory(provider, _contextAccessor.Object, _loggerFactory.Object); var downstreamRoute = new DownstreamRouteBuilder().Build(); var handler = _factory.Get(downstreamRoute); handler.IsError.ShouldBeFalse(); From ec555044292ea69e526fb3f01bcf22426d82910d Mon Sep 17 00:00:00 2001 From: Samuel Poirier Date: Sat, 4 Nov 2023 13:15:31 -0400 Subject: [PATCH 06/12] #1179 Add missing documentation for Secured WebSocket #1180 * Add "WebSocket Secure" and "SSL Errors" sections (#1180) Co-authored-by: raman-m --- docs/features/configuration.rst | 2 ++ docs/features/websockets.rst | 49 +++++++++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index feec24721..b965cbd34 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -241,6 +241,8 @@ Use ``HttpHandlerOptions`` in a Route configuration to set up ``HttpHandler`` be * **MaxConnectionsPerServer** This controls how many connections the internal ``HttpClient`` will open. This can be set at Route or global level. +.. _ssl-errors: + SSL Errors ---------- diff --git a/docs/features/websockets.rst b/docs/features/websockets.rst index 8dfc23321..891efba81 100644 --- a/docs/features/websockets.rst +++ b/docs/features/websockets.rst @@ -1,7 +1,8 @@ Websockets ========== - `WebSockets Standard `_ by WHATWG organization + * `WebSockets Standard `_ by WHATWG organization + * `The WebSocket Protocol `_ by Internet Engineering Task Force (IETF) organization Ocelot supports proxying `WebSockets `_ with some extra bits. This functionality was requested in `issue 212 `_. @@ -90,12 +91,56 @@ Note normal Ocelot routing rules apply the main thing is the scheme which is set "UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE", "OPTIONS" ], "UpstreamPathTemplate": "/gateway/{catchAll}", "DownstreamPathTemplate": "/{catchAll}", - "DownstreamScheme": "ws", + "DownstreamScheme": "ws", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5001 } ] } +WebSocket Secure +---------------- + +If you define a route with Secured WebSocket protocol, use the ``wss`` scheme: + +.. code-block:: json + + { + "DownstreamScheme": "wss", + // ... + } + +Keep in mind: you can use WebSocket SSL for both `SignalR <#signalr>`_ and `WebSockets <#websockets>`__. + +To understand ``wss`` scheme, browse to this: + +* Microsoft Learn: `Secure your connection with TLS/SSL `_ +* IETF | The WebSocket Protocol: `WebSocket URIs `_ + +If you have questions, it may be helpful to search for documentation on MS Learn: + +* `Search for "secure websocket" `_ + +SSL Errors +^^^^^^^^^^ + +If you want to ignore SSL warnings (errors), set the following in your Route config: + +.. code-block:: json + + { + "DownstreamScheme": "wss", + "DangerousAcceptAnyServerCertificateValidator": true, + // ... + } + +**But we don't recommend doing this!** Read the official notes regarding :ref:`ssl-errors` in the :doc:`../features/configuration` doc, +where you will also find best practices for your environments. + +**Note**, the ``wss`` scheme fake validator was added by `PR 1377 `_, +as a part of issues `1375 `_, `1237 `_ and etc. +This life hacking feature for self-signed SSL certificates is available in version `20.0 `_. +It will be removed and/or reworked in future releases. See the :ref:`ssl-errors` section for details. + Supported --------- From 04ad9bf87ef1b509120c1f30cbf981b96cd44a30 Mon Sep 17 00:00:00 2001 From: raman-m Date: Tue, 21 Nov 2023 19:56:54 +0300 Subject: [PATCH 07/12] Resolve issues with projects after auto-merging. Format Document --- .../Ocelot.AcceptanceTests.csproj | 1 + test/Ocelot.AcceptanceTests/PollyQoSTests.cs | 301 +++++++++--------- test/Ocelot.Testing/Ocelot.Testing.csproj | 3 +- 3 files changed, 154 insertions(+), 151 deletions(-) diff --git a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj index 760b03bae..29a6d5612 100644 --- a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +++ b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj @@ -38,6 +38,7 @@ + diff --git a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs index 578680950..e243bd8f8 100644 --- a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs +++ b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs @@ -7,151 +7,152 @@ namespace Ocelot.AcceptanceTests public class PollyQoSTests : IDisposable { private readonly Steps _steps; - private readonly ServiceHandler _serviceHandler; - - public PollyQoSTests() - { - _serviceHandler = new ServiceHandler(); - _steps = new Steps(); - } - - private static FileConfiguration FileConfigurationFactory(int port, QoSOptions options, string httpMethod = nameof(HttpMethods.Get)) => new() - { - Routes = new List + private readonly ServiceHandler _serviceHandler; + + public PollyQoSTests() { - new() + _serviceHandler = new ServiceHandler(); + _steps = new Steps(); + } + + private static FileConfiguration FileConfigurationFactory(int port, QoSOptions options, string httpMethod = nameof(HttpMethods.Get)) + => new() { - DownstreamPathTemplate = "/", - DownstreamScheme = Uri.UriSchemeHttp, - DownstreamHostAndPorts = new() + Routes = new List { - new("localhost", port), + new() + { + DownstreamPathTemplate = "/", + DownstreamScheme = Uri.UriSchemeHttp, + DownstreamHostAndPorts = new() + { + new("localhost", port), + }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new() { httpMethod }, + QoSOptions = new FileQoSOptions(options), + }, }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new() { httpMethod }, - QoSOptions = new FileQoSOptions(options), - }, - }, - }; - - [Fact] - public void Should_not_timeout() - { - var port = PortFinder.GetRandomPort(); - var configuration = FileConfigurationFactory(port, new QoSOptions(10, 0, 1000, null), HttpMethods.Post); - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, string.Empty, 10)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .BDDfy(); - } - - [Fact] - public void Should_timeout() - { - var port = PortFinder.GetRandomPort(); - var configuration = FileConfigurationFactory(port, new QoSOptions(0, 0, 10, null), HttpMethods.Post); - - this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 201, string.Empty, 1000)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.GivenThePostHasContent("postContent")) - .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .BDDfy(); - } - - [Fact] - public void Should_open_circuit_breaker_after_two_exceptions() - { - var port = PortFinder.GetRandomPort(); - var configuration = FileConfigurationFactory(port, new QoSOptions(2, 5000, 100000, null)); - - this.Given(x => x.GivenThereIsABrokenServiceRunningOn($"http://localhost:{port}")) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .BDDfy(); - } - - [Fact] - public void Should_open_circuit_breaker_then_close() - { - var port = PortFinder.GetRandomPort(); - var configuration = FileConfigurationFactory(port, new QoSOptions(1, 500, 1000, null)); - - this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port}", "Hello from Laura")) - .Given(x => _steps.GivenThereIsAConfiguration(configuration)) - .Given(x => _steps.GivenOcelotIsRunningWithPolly()) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .Given(x => GivenIWaitMilliseconds(3000)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - [Fact] - public void Open_circuit_should_not_effect_different_route() - { - var port1 = PortFinder.GetRandomPort(); - var port2 = PortFinder.GetRandomPort(); - var qos1 = new QoSOptions(1, 1000, 500, null); - - var configuration = FileConfigurationFactory(port1, qos1); - var route2 = configuration.Routes[0].Clone() as FileRoute; - route2.DownstreamHostAndPorts[0].Port = port2; - route2.UpstreamPathTemplate = "/working"; - route2.QoSOptions = new(); - configuration.Routes.Add(route2); - - this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port1}", "Hello from Laura")) - .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port2}/", 200, "Hello from Tom", 0)) - .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithPolly()) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/working")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) - .And(x => GivenIWaitMilliseconds(3000)) - .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) - .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) - .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) - .BDDfy(); - } - - private static void GivenIWaitMilliseconds(int ms) => Thread.Sleep(ms); + }; - private void GivenThereIsABrokenServiceRunningOn(string url) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => - { - context.Response.StatusCode = 500; - await context.Response.WriteAsync("this is an exception"); - }); - } + [Fact] + public void Should_not_timeout() + { + var port = PortFinder.GetRandomPort(); + var configuration = FileConfigurationFactory(port, new QoSOptions(10, 0, 1000, null), HttpMethods.Post); + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, string.Empty, 10)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithPolly()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Fact] + public void Should_timeout() + { + var port = PortFinder.GetRandomPort(); + var configuration = FileConfigurationFactory(port, new QoSOptions(0, 0, 10, null), HttpMethods.Post); + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 201, string.Empty, 1000)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithPolly()) + .And(x => _steps.GivenThePostHasContent("postContent")) + .When(x => _steps.WhenIPostUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .BDDfy(); + } + + [Fact] + public void Should_open_circuit_breaker_after_two_exceptions() + { + var port = PortFinder.GetRandomPort(); + var configuration = FileConfigurationFactory(port, new QoSOptions(2, 5000, 100000, null)); + + this.Given(x => x.GivenThereIsABrokenServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithPolly()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .BDDfy(); + } + + [Fact] + public void Should_open_circuit_breaker_then_close() + { + var port = PortFinder.GetRandomPort(); + var configuration = FileConfigurationFactory(port, new QoSOptions(1, 500, 1000, null)); + + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port}", "Hello from Laura")) + .Given(x => _steps.GivenThereIsAConfiguration(configuration)) + .Given(x => _steps.GivenOcelotIsRunningWithPolly()) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Given(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .Given(x => GivenIWaitMilliseconds(3000)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + [Fact] + public void Open_circuit_should_not_effect_different_route() + { + var port1 = PortFinder.GetRandomPort(); + var port2 = PortFinder.GetRandomPort(); + var qos1 = new QoSOptions(1, 1000, 500, null); + + var configuration = FileConfigurationFactory(port1, qos1); + var route2 = configuration.Routes[0].Clone() as FileRoute; + route2.DownstreamHostAndPorts[0].Port = port2; + route2.UpstreamPathTemplate = "/working"; + route2.QoSOptions = new(); + configuration.Routes.Add(route2); + + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port1}", "Hello from Laura")) + .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port2}/", 200, "Hello from Tom", 0)) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithPolly()) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/working")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Tom")) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .And(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) + .And(x => GivenIWaitMilliseconds(3000)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) + .BDDfy(); + } + + private static void GivenIWaitMilliseconds(int ms) => Thread.Sleep(ms); + + private void GivenThereIsABrokenServiceRunningOn(string url) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + context.Response.StatusCode = 500; + await context.Response.WriteAsync("this is an exception"); + }); + } private void GivenThereIsAPossiblyBrokenServiceRunningOn(string url, string responseBody) { @@ -167,17 +168,17 @@ private void GivenThereIsAPossiblyBrokenServiceRunningOn(string url, string resp context.Response.StatusCode = 200; await context.Response.WriteAsync(responseBody); }); - } - - private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, int timeout) - { - _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + } + + private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, int timeout) { - Thread.Sleep(timeout); - context.Response.StatusCode = statusCode; - await context.Response.WriteAsync(responseBody); - }); - } + _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => + { + Thread.Sleep(timeout); + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + } public void Dispose() { diff --git a/test/Ocelot.Testing/Ocelot.Testing.csproj b/test/Ocelot.Testing/Ocelot.Testing.csproj index cfadb03dd..fa27745f4 100644 --- a/test/Ocelot.Testing/Ocelot.Testing.csproj +++ b/test/Ocelot.Testing/Ocelot.Testing.csproj @@ -1,7 +1,8 @@ - net7.0 + 0.0.0-dev + net6.0;net7.0;net8.0 enable enable From 388ebc391b1e3dfc3dee22de54ead5567c30d8e9 Mon Sep 17 00:00:00 2001 From: Guillaume Gnaegi <58469901+ggnaegi@users.noreply.github.com> Date: Fri, 24 Nov 2023 21:59:37 +0100 Subject: [PATCH 08/12] #1744 Avoid calls to 'Logger.Log' if LogLevel not enabled in appsettings.json (#1745) * changing string parameter for IOcelotLogger function to Func, modifying asp dot net logger, only one main method and verifying if LogLevel is enabled. If log level isn't enabled, then return. pick 847dac78 changing string parameter for IOcelotLogger function to Func, modifying asp dot net logger, only one main method and verifying if LogLevel is enabled. If log level isn't enabled, then return. pick d7a83971 adding back the logger methods with string as parameter, avoiding calling the factory when plain string are used. pick d4132010 simplify method calls * adding back the logger methods with string as parameter, avoiding calling the factory when plain string are used. * simplify method calls * adding unit test case, If minimum log level not set then no logs are written * adding logging benchmark * code cleanup in steps and naming issues fixes pick c4f6dc9e adding loglevel acceptance tests, verifying that the logs are returned according to the minimum log level set in appsettings pick 478f1398 enhanced unit tests, verifying 1) that the log method is only called when log level enabled 2) that the string function is only invoked when log level enabled * adding loglevel acceptance tests, verifying that the logs are returned according to the minimum log level set in appsettings * enhanced unit tests, verifying 1) that the log method is only called when log level enabled 2) that the string function is only invoked when log level enabled * weird issue with the merge. * adding comment * Update src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs Co-authored-by: Raman Maksimchuk * Update src/Ocelot/Claims/Middleware/ClaimsToClaimsMiddleware.cs Co-authored-by: Raman Maksimchuk * Update src/Ocelot/Configuration/Repository/FileConfigurationPoller.cs Co-authored-by: Raman Maksimchuk * Update src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteProviderFactory.cs Co-authored-by: Raman Maksimchuk * Update src/Ocelot/Logging/AspDotNetLogger.cs Co-authored-by: Raman Maksimchuk * Update test/Ocelot.AcceptanceTests/LogLevelTests.cs Co-authored-by: Raman Maksimchuk * Update src/Ocelot/Configuration/Repository/FileConfigurationPoller.cs Co-authored-by: Raman Maksimchuk * As mentioned, using OcelotLogger instead of AspDotNeLogger as default logger name * Some code refactoring and usage of factories in LogLevelTests * Update src/Ocelot/Claims/Middleware/ClaimsToClaimsMiddleware.cs Co-authored-by: Raman Maksimchuk * using overrided method WriteLog for strings, some changes as requested, * code changes after review 2 pick ad0e060c Update test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs * checking test cases * adding ms logger benchmarks with console provider. Unfortunately, benchmark.net doesn't support "quiet" mode yet. * 2 small adjustments * Adding multi targets support for serilog * Fix warnings * Review new logger * Fix unit tests * The last change but not least * Update logging.rst: Add draft * Update logging.rst: Add RequestId section * Update logging.rst: "Best Practices" section * Update logging.rst: "Top Logging Performance?" subsection * Update logging.rst: Rewrite "Request ID" section * Update requestid.rst: Review and up to date * Update logging.rst: "Run Benchmarks" section --------- Co-authored-by: Raman Maksimchuk --- docs/features/logging.rst | 147 +- docs/features/requestid.rst | 29 +- src/Ocelot.Provider.Consul/Consul.cs | 2 +- src/Ocelot.Provider.Consul/PollConsul.cs | 2 +- .../KubernetesServiceDiscoveryProvider.cs | 2 +- .../Middleware/AuthenticationMiddleware.cs | 8 +- .../Middleware/AuthorizationMiddleware.cs | 8 +- .../Cache/Middleware/OutputCacheMiddleware.cs | 12 +- .../Middleware/ClaimsToClaimsMiddleware.cs | 6 +- .../Creator/ClaimsToThingCreator.cs | 2 +- .../Creator/HeaderFindAndReplaceCreator.cs | 4 +- .../Repository/FileConfigurationPoller.cs | 2 +- .../DependencyInjection/OcelotBuilder.cs | 2 +- .../ClaimsToDownstreamPathMiddleware.cs | 2 +- .../Finder/DownstreamRouteProviderFactory.cs | 7 +- .../DownstreamRouteFinderMiddleware.cs | 7 +- .../DownstreamUrlCreatorMiddleware.cs | 14 +- .../Middleware/ExceptionHandlerMiddleware.cs | 5 +- src/Ocelot/Headers/AddHeadersToRequest.cs | 2 +- src/Ocelot/Headers/AddHeadersToResponse.cs | 2 +- .../Middleware/ClaimsToHeadersMiddleware.cs | 2 +- src/Ocelot/Logging/AspDotNetLogger.cs | 93 - src/Ocelot/Logging/IOcelotLogger.cs | 34 +- .../Logging/OcelotDiagnosticListener.cs | 6 +- src/Ocelot/Logging/OcelotLogger.cs | 95 + ...oggerFactory.cs => OcelotLoggerFactory.cs} | 6 +- .../ClaimsToQueryStringMiddleware.cs | 2 +- .../Middleware/ClientRateLimitMiddleware.cs | 6 +- .../Middleware/RequestIdMiddleware.cs | 13 +- .../DelegatingHandlerHandlerFactory.cs | 2 +- src/Ocelot/Requester/HttpClientBuilder.cs | 2 +- .../Middleware/HttpRequesterMiddleware.cs | 4 +- .../Middleware/ResponderMiddleware.cs | 4 +- .../ServiceDiscoveryProviderFactory.cs | 56 +- .../WebSockets/WebSocketsProxyMiddleware.cs | 4 +- test/Ocelot.AcceptanceTests/AggregateTests.cs | 2 +- .../AuthenticationTests.cs | 2 +- .../AuthorizationTests.cs | 4 +- .../ClaimsToDownstreamPathTests.cs | 2 +- .../ClaimsToHeadersForwardingTests.cs | 2 +- .../ClaimsToQueryStringForwardingTests.cs | 2 +- .../CustomMiddlewareTests.cs | 2 +- .../HttpDelegatingHandlersTests.cs | 2 +- test/Ocelot.AcceptanceTests/LogLevelTests.cs | 175 ++ .../Ocelot.AcceptanceTests.csproj | 4 + test/Ocelot.AcceptanceTests/Steps.cs | 2099 ++++++++--------- test/Ocelot.Benchmarks/MsLoggerBenchmarks.cs | 195 ++ .../Ocelot.Benchmarks.csproj | 12 + test/Ocelot.Benchmarks/Program.cs | 2 + test/Ocelot.Benchmarks/SerilogBenchmarks.cs | 226 ++ .../ClaimsToThingCreatorTests.cs | 2 +- .../HeaderFindAndReplaceCreatorTests.cs | 2 +- .../ConsulServiceDiscoveryProviderTests.cs | 2 +- .../Headers/AddHeadersToRequestPlainTests.cs | 2 +- .../Headers/AddHeadersToResponseTests.cs | 2 +- .../Logging/AspDotNetLoggerTests.cs | 77 - .../Logging/OcelotDiagnosticListenerTests.cs | 2 +- .../Logging/OcelotLoggerTests.cs | 426 ++++ .../Middleware/OcelotPiplineBuilderTests.cs | 36 +- .../Polly/PollyQoSProviderTests.cs | 10 +- ...atingHandlerHandlerProviderFactoryTests.cs | 2 +- .../Requester/HttpClientBuilderTests.cs | 2 +- .../Requester/HttpRequesterMiddlewareTests.cs | 4 +- .../ServiceDiscoveryProviderFactoryTests.cs | 12 +- .../WebSocketsProxyMiddlewareTests.cs | 12 +- test/Ocelot.UnitTests/appsettings.json | 6 +- 66 files changed, 2503 insertions(+), 1421 deletions(-) delete mode 100644 src/Ocelot/Logging/AspDotNetLogger.cs create mode 100644 src/Ocelot/Logging/OcelotLogger.cs rename src/Ocelot/Logging/{AspDotNetLoggerFactory.cs => OcelotLoggerFactory.cs} (66%) create mode 100644 test/Ocelot.AcceptanceTests/LogLevelTests.cs create mode 100644 test/Ocelot.Benchmarks/MsLoggerBenchmarks.cs create mode 100644 test/Ocelot.Benchmarks/SerilogBenchmarks.cs delete mode 100644 test/Ocelot.UnitTests/Logging/AspDotNetLoggerTests.cs create mode 100644 test/Ocelot.UnitTests/Logging/OcelotLoggerTests.cs diff --git a/docs/features/logging.rst b/docs/features/logging.rst index 979b16145..a8bc91336 100644 --- a/docs/features/logging.rst +++ b/docs/features/logging.rst @@ -2,23 +2,154 @@ Logging ======= Ocelot uses the standard logging interfaces ``ILoggerFactory`` and ``ILogger`` at the moment. -This is encapsulated in ``IOcelotLogger`` and ``IOcelotLoggerFactory`` with an implementation for the standard `ASP.NET Core logging `_ stuff at the moment. -This is because Ocelot adds some extra info to the logs such as **request ID** if it is configured. +This is encapsulated in ``IOcelotLogger`` and ``IOcelotLoggerFactory`` with the implementation for the standard `ASP.NET Core logging `_ stuff at the moment. +This is because Ocelot adds some extra info to the logs such as **RequestId** if it is configured. There is a global `error handler middleware `_ that should catch any exceptions thrown and log them as errors. -Finally, if logging is set to **Trace** level, Ocelot will log starting, finishing and any middlewares that throw an exception which can be quite useful. +Finally, if logging is set to ``Trace`` level, Ocelot will log starting, finishing and any middlewares that throw an exception which can be quite useful. + +Request ID +---------- The reason for not just using `bog standard `_ framework logging is that -we could not work out how to override the request id that get's logged when setting **IncludeScopes** to ``true`` for logging settings. +we could not work out how to override the **RequestId** that get's logged when setting **IncludeScopes** to ``true`` for logging settings. Nicely onto the next feature. +Every log record has these 2 properties: + +* **RequestId** represents ID of the current request as plain string, for example ``0HMVD33IIJRFR:00000001`` +* **PreviousRequestId** represents ID of the previous request + +As an ``IOcelotLogger`` interface object being injected to constructors of service classes, current default Ocelot logger (``OcelotLogger`` class) reads these 2 properties from the ``IRequestScopedDataRepository`` interface object. +Find out more about these properties and other details on the *Request ID* logging feature in the :doc:`../features/requestid` chapter. + Warning ------- -If you are logging to `Console `_, you will get terrible performance. -The team has had so many issues about performance issues with Ocelot and it is always logging level **Debug**, logging to `Console `_. +If you are logging to MS `Console `_, you will get terrible performance. +The team has had so many issues about performance issues with Ocelot and it is always logging level ``Debug``, logging to `Console `_. * **Warning!** Make sure you are logging to something proper in production environment! -* Use **Error** and **Critical** levels in production environment! -* Use **Warning** level in testing environment! +* Use ``Error`` and ``Critical`` levels in production environment! +* Use ``Warning`` level in testing & staging environments! + +These and other recommendations are below in the `Best Practices <#best-practices>`_ section. + +Best Practices +-------------- + + | Microsoft Learn сomplete reference: `Logging in .NET Core and ASP.NET Core `_ + +Our recommendations to gain Ocelot best logging are the following. + +First +^^^^^ + +Ensure minimum level while `Configure logging `_. +The minimum log level is set in the application's ``appsettings.json`` file. This level is defined in the **Logging** section, for example: + +.. code-block:: json + + { + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } + } + +Whether using `Serilog `_ or the standard Microsoft providers, the logging configuration will be retrieved from this section. + +.. code-block:: csharp + + .ConfigureAppConfiguration((_, config) => + { + config.AddJsonFile($"appsettings.{env.EnvironmentName}.json", false, false); + // ... + }) + +However, there is one thing to be aware of. It is possible to use the ``SetMinimumLevel()`` method to define the minimum logging level. +Be careful and make sure you set the log level in one place only, like: + +.. code-block:: csharp + + ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.SetMinimumLevel(minLogLevel); + logging.AddConsole(); // MS Console for Development and/or Testing environments only + }) + +Please also use the ``ClearProviders()`` method, so that only the providers you wish to use are taken into account, as in the example above, the console. + +Second +^^^^^^ + +Ensure proper usage of minimum logging level for each environment: development, testing, production, etc. +So, once again, read important notes of the `Warning <#warning>`_ section! + +Third +^^^^^ + +Ocelot's logging has been improved in `22.0 `_ version: +it is now possible to use a factory method for message strings that will only be executed if the minimum log level allows it. + +For example, let's take a message containing information about several variables that should only be generated if the minimum log level is ``Debug``. +If the minimum log level is ``Warning`` then the string is never generated. + +Therefore, when the string contains dynamic information aka ``string.Format``, or string value is generated by `string interpolation `_ expression, +it is recommended to call the log method using anonymous delegate via an ``=>`` expression function: + +.. code-block:: csharp + + Logger.LogDebug(() => $"downstream templates are {string.Join(", ", response.Data.Route.DownstreamRoute.Select(r => r.DownstreamPathTemplate.Value))}"); + +otherwise a constant string is sufficient + +.. code-block:: csharp + + Logger.LogDebug("My const string"); + +Performance Review +------------------ + +Ocelot's logging performance has been improved in version `22.0 `__ (see PR `1745 `_). +These changes were requested as part of issue `1744 `_ after team's `discussion `_. + +Top Logging Performance? +^^^^^^^^^^^^^^^^^^^^^^^^ + +Here is a quick recipe for your Production environment! +You need to ensure the minimal level is ``Critical`` or ``None``. Nothing more! +For sure, having top logging performance means having less log records written by logging provider. So, logs should be pretty empty. + +Anyway, during the first time after a version release to production, we recommend to watch the system and current version app behavior by specifying ``Error`` minimum level. +If release engineer will ensure stability of the version in production then minimum level can be increased to ``Critical`` or ``None`` to gain top performance. +Technically this will switch off the logging feature at all. + +Run Benchmarks +^^^^^^^^^^^^^^ + +We have 2 types of benchmarks currently + +* ``SerilogBenchmarks`` with Serilog logging to a file. See ``ConfigureLogging`` method with ``logging.AddSerilog(_logger);`` +* ``MsLoggerBenchmarks`` with MS default logging to MS Console. See ``ConfigureLogging`` method with ``logging.AddConsole();`` + +Benchmark results largely depend on the environment and hardware on which they run. +We are pleased to invite you to run Logging benchmarks on your machine by the following instructions below. + +1. Open PowerShell or Command Prompt console +2. Build Ocelot solution in Release mode: ``dotnet build --configuration Release`` +3. Go to ``test\Ocelot.Benchmarks\bin\Release\`` folder. +4. Choose .NET version changing the folder, for example to ``net8.0`` +5. Run **Ocelot.Benchmarks.exe**: ``.\Ocelot.Benchmarks.exe`` +6. Run ``SerilogBenchmarks`` or ``MsLoggerBenchmarks`` by pressing appropriate number of a benchmark: ``5`` or ``6``, + Enter +7. Wait for 3+ minutes to complete benchmark, and get final results. +8. Read and analize your benchmark session results. + +Indicators +^^^^^^^^^^ + +``To be developed...`` diff --git a/docs/features/requestid.rst b/docs/features/requestid.rst index 5fc46b936..014aaf3c5 100644 --- a/docs/features/requestid.rst +++ b/docs/features/requestid.rst @@ -1,13 +1,13 @@ Request ID ========== - aka **Correlation ID** + aka **Correlation ID** or `HttpContext.TraceIdentifier `_ Ocelot supports a client sending a *request ID* in the form of a header. -If set, Ocelot will use the **requestId** for logging as soon as it becomes available in the middleware pipeline. -Ocelot will also forward the *request ID* with the specified header to the downstream service. +If set, Ocelot will use the **RequestId** for logging as soon as it becomes available in the middleware pipeline. +Ocelot will also forward the *RequestId* with the specified header to the downstream service. -You can still get the ASP.NET Core *request ID* in the logs if you set **IncludeScopes** ``true`` in your logging config. +You can still get the ASP.NET Core *Request ID* in the logs if you set **IncludeScopes** ``true`` in your logging config. In order to use the *Request ID* feature you have two options. @@ -67,3 +67,24 @@ Below is an example of the logging when set at ``Debug`` level for a normal requ requestId: 1234, previousRequestId: asdf, message: no pipeline errors, setting and returning completed response, dbug: Ocelot.Errors.Middleware.ExceptionHandlerMiddleware[0] requestId: 1234, previousRequestId: asdf, message: ocelot pipeline finished, + +And more practical example from secret production environment in Switzerland: + +.. code-block:: text + + warn: Ocelot.DownstreamRouteFinder.Middleware.DownstreamRouteFinderMiddleware[0] + requestId: 0HMVD33IIJRFR:00000001, previousRequestId: no previous request id, message: DownstreamRouteFinderMiddleware setting pipeline errors. IDownstreamRouteFinder returned Error Code: UnableToFindDownstreamRouteError Message: Failed to match Route configuration for upstream path: /, verb: GET. + warn: Ocelot.Responder.Middleware.ResponderMiddleware[0] + requestId: 0HMVD33IIJRFR:00000001, previousRequestId: no previous request id, message: Error Code: UnableToFindDownstreamRouteError Message: Failed to match Route configuration for upstream path: /, verb: GET. errors found in ResponderMiddleware. Setting error response for request path:/, request method: GET + +Curious? +-------- + +*Request ID* is a part of big :doc:`../features/logging` feature. + +Every log record has these 2 properties: + +* **RequestId** represents ID of the current request as plain string, for example ``0HMVD33IIJRFR:00000001`` +* **PreviousRequestId** represents ID of the previous request + +As an ``IOcelotLogger`` interface object being injected to constructors of service classes, current default Ocelot logger (the ``OcelotLogger`` class) reads these 2 properties from the ``IRequestScopedDataRepository`` interface object. diff --git a/src/Ocelot.Provider.Consul/Consul.cs b/src/Ocelot.Provider.Consul/Consul.cs index 84bc7ceee..273fca2ab 100644 --- a/src/Ocelot.Provider.Consul/Consul.cs +++ b/src/Ocelot.Provider.Consul/Consul.cs @@ -44,7 +44,7 @@ public async Task> GetAsync() else { _logger.LogWarning( - $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."); + () => $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."); } } diff --git a/src/Ocelot.Provider.Consul/PollConsul.cs b/src/Ocelot.Provider.Consul/PollConsul.cs index 5806e60f1..45fd10b19 100644 --- a/src/Ocelot.Provider.Consul/PollConsul.cs +++ b/src/Ocelot.Provider.Consul/PollConsul.cs @@ -50,7 +50,7 @@ public Task> GetAsync() try { - _logger.LogInformation($"Retrieving new client information for service: {ServiceName}..."); + _logger.LogInformation(() => $"Retrieving new client information for service: {ServiceName}..."); _services = _consulServiceDiscoveryProvider.GetAsync().Result; return Task.FromResult(_services); } diff --git a/src/Ocelot.Provider.Kubernetes/KubernetesServiceDiscoveryProvider.cs b/src/Ocelot.Provider.Kubernetes/KubernetesServiceDiscoveryProvider.cs index ce3ca316f..92c7b99cb 100644 --- a/src/Ocelot.Provider.Kubernetes/KubernetesServiceDiscoveryProvider.cs +++ b/src/Ocelot.Provider.Kubernetes/KubernetesServiceDiscoveryProvider.cs @@ -30,7 +30,7 @@ public async Task> GetAsync() } else { - _logger.LogWarning($"namespace:{_kubeRegistryConfiguration.KubeNamespace}service:{_kubeRegistryConfiguration.KeyOfServiceInK8s} Unable to use ,it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"); + _logger.LogWarning(() => $"namespace:{_kubeRegistryConfiguration.KubeNamespace}service:{_kubeRegistryConfiguration.KeyOfServiceInK8s} Unable to use ,it is invalid. Address must contain host only e.g. localhost and port must be greater than 0"); } return services; diff --git a/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs b/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs index 7e94118d2..843c3dac5 100644 --- a/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs +++ b/src/Ocelot/Authentication/Middleware/AuthenticationMiddleware.cs @@ -23,7 +23,7 @@ public async Task Invoke(HttpContext httpContext) if (httpContext.Request.Method.ToUpper() != "OPTIONS" && IsAuthenticatedRoute(downstreamRoute)) { - Logger.LogInformation($"{httpContext.Request.Path} is an authenticated route. {MiddlewareName} checking if client is authenticated"); + Logger.LogInformation(() => $"{httpContext.Request.Path} is an authenticated route. {MiddlewareName} checking if client is authenticated"); var result = await httpContext.AuthenticateAsync(downstreamRoute.AuthenticationOptions.AuthenticationProviderKey); @@ -31,7 +31,7 @@ public async Task Invoke(HttpContext httpContext) if (httpContext.User.Identity.IsAuthenticated) { - Logger.LogInformation($"Client has been authenticated for {httpContext.Request.Path}"); + Logger.LogInformation(() => $"Client has been authenticated for {httpContext.Request.Path}"); await _next.Invoke(httpContext); } else @@ -39,14 +39,14 @@ public async Task Invoke(HttpContext httpContext) var error = new UnauthenticatedError( $"Request for authenticated route {httpContext.Request.Path} by {httpContext.User.Identity.Name} was unauthenticated"); - Logger.LogWarning($"Client has NOT been authenticated for {httpContext.Request.Path} and pipeline error set. {error}"); + Logger.LogWarning(() =>$"Client has NOT been authenticated for {httpContext.Request.Path} and pipeline error set. {error}"); httpContext.Items.SetError(error); } } else { - Logger.LogInformation($"No authentication needed for {httpContext.Request.Path}"); + Logger.LogInformation(() => $"No authentication needed for {httpContext.Request.Path}"); await _next.Invoke(httpContext); } diff --git a/src/Ocelot/Authorization/Middleware/AuthorizationMiddleware.cs b/src/Ocelot/Authorization/Middleware/AuthorizationMiddleware.cs index 45c142631..84ccf526d 100644 --- a/src/Ocelot/Authorization/Middleware/AuthorizationMiddleware.cs +++ b/src/Ocelot/Authorization/Middleware/AuthorizationMiddleware.cs @@ -62,7 +62,7 @@ public async Task Invoke(HttpContext httpContext) if (authorized.IsError) { - Logger.LogWarning($"Error whilst authorizing {httpContext.User.Identity.Name}. Setting pipeline error"); + Logger.LogWarning(() => $"Error whilst authorizing {httpContext.User.Identity.Name}. Setting pipeline error"); httpContext.Items.UpsertErrors(authorized.Errors); return; @@ -70,19 +70,19 @@ public async Task Invoke(HttpContext httpContext) if (IsAuthorized(authorized)) { - Logger.LogInformation($"{httpContext.User.Identity.Name} has succesfully been authorized for {downstreamRoute.UpstreamPathTemplate.OriginalValue}."); + Logger.LogInformation(() => $"{httpContext.User.Identity.Name} has succesfully been authorized for {downstreamRoute.UpstreamPathTemplate.OriginalValue}."); await _next.Invoke(httpContext); } else { - Logger.LogWarning($"{httpContext.User.Identity.Name} is not authorized to access {downstreamRoute.UpstreamPathTemplate.OriginalValue}. Setting pipeline error"); + Logger.LogWarning(() => $"{httpContext.User.Identity.Name} is not authorized to access {downstreamRoute.UpstreamPathTemplate.OriginalValue}. Setting pipeline error"); httpContext.Items.SetError(new UnauthorizedError($"{httpContext.User.Identity.Name} is not authorized to access {downstreamRoute.UpstreamPathTemplate.OriginalValue}")); } } else { - Logger.LogInformation($"{downstreamRoute.DownstreamPathTemplate.Value} route does not require user to be authorized"); + Logger.LogInformation(() => $"{downstreamRoute.DownstreamPathTemplate.Value} route does not require user to be authorized"); await _next.Invoke(httpContext); } } diff --git a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs index 134708881..8c26c5ca6 100644 --- a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs +++ b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs @@ -35,29 +35,29 @@ public async Task Invoke(HttpContext httpContext) var downstreamUrlKey = $"{downstreamRequest.Method}-{downstreamRequest.OriginalString}"; var downStreamRequestCacheKey = await _cacheGenerator.GenerateRequestCacheKey(downstreamRequest, downstreamRoute); - Logger.LogDebug($"Started checking cache for the '{downstreamUrlKey}' key."); + Logger.LogDebug(() => $"Started checking cache for the '{downstreamUrlKey}' key."); var cached = _outputCache.Get(downStreamRequestCacheKey, downstreamRoute.CacheOptions.Region); if (cached != null) { - Logger.LogDebug($"Cache entry exists for the '{downstreamUrlKey}' key."); + Logger.LogDebug(() => $"Cache entry exists for the '{downstreamUrlKey}' key."); var response = CreateHttpResponseMessage(cached); SetHttpResponseMessageThisRequest(httpContext, response); - Logger.LogDebug($"Finished returning of cached response for the '{downstreamUrlKey}' key."); + Logger.LogDebug(() => $"Finished returning of cached response for the '{downstreamUrlKey}' key."); return; } - Logger.LogDebug($"No response cached for the '{downstreamUrlKey}' key."); + Logger.LogDebug(() => $"No response cached for the '{downstreamUrlKey}' key."); await _next.Invoke(httpContext); if (httpContext.Items.Errors().Count > 0) { - Logger.LogDebug($"There was a pipeline error for the '{downstreamUrlKey}' key."); + Logger.LogDebug(() => $"There was a pipeline error for the '{downstreamUrlKey}' key."); return; } @@ -68,7 +68,7 @@ public async Task Invoke(HttpContext httpContext) _outputCache.Add(downStreamRequestCacheKey, cached, TimeSpan.FromSeconds(downstreamRoute.CacheOptions.TtlSeconds), downstreamRoute.CacheOptions.Region); - Logger.LogDebug($"Finished response added to cache for the '{downstreamUrlKey}' key."); + Logger.LogDebug(() => $"Finished response added to cache for the '{downstreamUrlKey}' key."); } private static void SetHttpResponseMessageThisRequest(HttpContext context, diff --git a/src/Ocelot/Claims/Middleware/ClaimsToClaimsMiddleware.cs b/src/Ocelot/Claims/Middleware/ClaimsToClaimsMiddleware.cs index 2b4db0309..8e82d5606 100644 --- a/src/Ocelot/Claims/Middleware/ClaimsToClaimsMiddleware.cs +++ b/src/Ocelot/Claims/Middleware/ClaimsToClaimsMiddleware.cs @@ -1,6 +1,6 @@ -using Microsoft.AspNetCore.Http; -using Ocelot.Logging; -using Ocelot.Middleware; +using Microsoft.AspNetCore.Http; +using Ocelot.Logging; +using Ocelot.Middleware; namespace Ocelot.Claims.Middleware { diff --git a/src/Ocelot/Configuration/Creator/ClaimsToThingCreator.cs b/src/Ocelot/Configuration/Creator/ClaimsToThingCreator.cs index e9ff42f8a..c6c1da38e 100644 --- a/src/Ocelot/Configuration/Creator/ClaimsToThingCreator.cs +++ b/src/Ocelot/Configuration/Creator/ClaimsToThingCreator.cs @@ -25,7 +25,7 @@ public List Create(Dictionary inputToBeParsed) if (claimToThing.IsError) { - _logger.LogDebug($"Unable to extract configuration for key: {input.Key} and value: {input.Value} your configuration file is incorrect"); + _logger.LogDebug(() => $"Unable to extract configuration for key: {input.Key} and value: {input.Value} your configuration file is incorrect"); } else { diff --git a/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs index 3c53b543e..07c558fb0 100644 --- a/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs +++ b/src/Ocelot/Configuration/Creator/HeaderFindAndReplaceCreator.cs @@ -32,7 +32,7 @@ public HeaderTransformations Create(FileRoute fileRoute) } else { - _logger.LogWarning($"Unable to add UpstreamHeaderTransform {input.Key}: {input.Value}"); + _logger.LogWarning(() => $"Unable to add UpstreamHeaderTransform {input.Key}: {input.Value}"); } } else @@ -55,7 +55,7 @@ public HeaderTransformations Create(FileRoute fileRoute) } else { - _logger.LogWarning($"Unable to add DownstreamHeaderTransform {input.Key}: {input.Value}"); + _logger.LogWarning(() => $"Unable to add DownstreamHeaderTransform {input.Key}: {input.Value}"); } } else diff --git a/src/Ocelot/Configuration/Repository/FileConfigurationPoller.cs b/src/Ocelot/Configuration/Repository/FileConfigurationPoller.cs index 61538a5d7..969a34161 100644 --- a/src/Ocelot/Configuration/Repository/FileConfigurationPoller.cs +++ b/src/Ocelot/Configuration/Repository/FileConfigurationPoller.cs @@ -68,7 +68,7 @@ private async Task Poll() if (fileConfig.IsError) { - _logger.LogWarning($"error geting file config, errors are {string.Join(',', fileConfig.Errors.Select(x => x.Message))}"); + _logger.LogWarning(() =>$"error geting file config, errors are {string.Join(',', fileConfig.Errors.Select(x => x.Message))}"); return; } diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 0464f2b1c..4d198711e 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -91,7 +91,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.AddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); - Services.TryAddSingleton(); + Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton(); diff --git a/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddleware.cs b/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddleware.cs index 149a9ea09..e9c54c9dd 100644 --- a/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddleware.cs +++ b/src/Ocelot/DownstreamPathManipulation/Middleware/ClaimsToDownstreamPathMiddleware.cs @@ -25,7 +25,7 @@ public async Task Invoke(HttpContext httpContext) if (downstreamRoute.ClaimsToPath.Any()) { - Logger.LogInformation($"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to path"); + Logger.LogInformation(() => $"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to path"); var templatePlaceholderNameAndValues = httpContext.Items.TemplatePlaceholderNameAndValues(); diff --git a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteProviderFactory.cs b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteProviderFactory.cs index b10ced14f..236941e4b 100644 --- a/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteProviderFactory.cs +++ b/src/Ocelot/DownstreamRouteFinder/Finder/DownstreamRouteProviderFactory.cs @@ -1,6 +1,6 @@ -using Microsoft.Extensions.DependencyInjection; -using Ocelot.Configuration; -using Ocelot.Logging; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration; +using Ocelot.Logging; namespace Ocelot.DownstreamRouteFinder.Finder { @@ -22,6 +22,7 @@ public IDownstreamRouteProvider Get(IInternalConfiguration config) if ((!config.Routes.Any() || config.Routes.All(x => string.IsNullOrEmpty(x.UpstreamTemplatePattern?.OriginalValue))) && IsServiceDiscovery(config.ServiceProviderConfiguration)) { _logger.LogInformation($"Selected {nameof(DownstreamRouteCreator)} as DownstreamRouteProvider for this request"); + return _providers[nameof(DownstreamRouteCreator)]; } diff --git a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs index 678ed7ec2..63c21b76c 100644 --- a/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs +++ b/src/Ocelot/DownstreamRouteFinder/Middleware/DownstreamRouteFinderMiddleware.cs @@ -32,7 +32,7 @@ public async Task Invoke(HttpContext httpContext) ? hostHeader.Split(':')[0] : hostHeader; - Logger.LogDebug($"Upstream url path is {upstreamUrlPath}"); + Logger.LogDebug(() => $"Upstream url path is {upstreamUrlPath}"); var internalConfiguration = httpContext.Items.IInternalConfiguration(); @@ -42,14 +42,13 @@ public async Task Invoke(HttpContext httpContext) if (response.IsError) { - Logger.LogWarning($"{MiddlewareName} setting pipeline errors. IDownstreamRouteFinder returned {response.Errors.ToErrorString()}"); + Logger.LogWarning(() => $"{MiddlewareName} setting pipeline errors. IDownstreamRouteFinder returned {response.Errors.ToErrorString()}"); httpContext.Items.UpsertErrors(response.Errors); return; } - var downstreamPathTemplates = string.Join(", ", response.Data.Route.DownstreamRoute.Select(r => r.DownstreamPathTemplate.Value)); - Logger.LogDebug($"downstream templates are {downstreamPathTemplates}"); + Logger.LogDebug(() => $"downstream templates are {string.Join(", ", response.Data.Route.DownstreamRoute.Select(r => r.DownstreamPathTemplate.Value))}"); // why set both of these on HttpContext httpContext.Items.UpsertTemplatePlaceholderNameAndValues(response.Data.TemplatePlaceholderNameAndValues); diff --git a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs index 7492756c5..f8ae6ed26 100644 --- a/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs +++ b/src/Ocelot/DownstreamUrlCreator/Middleware/DownstreamUrlCreatorMiddleware.cs @@ -1,20 +1,20 @@ using Microsoft.AspNetCore.Http; using Ocelot.Configuration; using Ocelot.DownstreamRouteFinder.UrlMatcher; -using Ocelot.Logging; -using Ocelot.Middleware; +using Ocelot.Logging; +using Ocelot.Middleware; using Ocelot.Request.Middleware; -using Ocelot.Responses; +using Ocelot.Responses; using Ocelot.Values; using System.Web; - + namespace Ocelot.DownstreamUrlCreator.Middleware { public class DownstreamUrlCreatorMiddleware : OcelotMiddleware { private readonly RequestDelegate _next; private readonly IDownstreamPathPlaceholderReplacer _replacer; - + private const char Ampersand = '&'; private const char QuestionMark = '?'; private const char OpeningBrace = '{'; @@ -80,7 +80,7 @@ public async Task Invoke(HttpContext httpContext) } } - Logger.LogDebug($"Downstream url is {downstreamRequest}"); + Logger.LogDebug(() => $"Downstream url is {downstreamRequest}"); await _next.Invoke(httpContext); } @@ -152,7 +152,7 @@ private static string GetQueryString(DownstreamPath dsPath) } private static bool ServiceFabricRequest(IInternalConfiguration config, DownstreamRoute downstreamRoute) - { + { return config.ServiceProviderConfiguration.Type?.ToLower() == "servicefabric" && downstreamRoute.UseServiceDiscovery; } } diff --git a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs index 0d2a961fb..cea6fd2d0 100644 --- a/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs +++ b/src/Ocelot/Errors/Middleware/ExceptionHandlerMiddleware.cs @@ -48,10 +48,7 @@ public async Task Invoke(HttpContext httpContext) catch (Exception e) { Logger.LogDebug("error calling middleware"); - - var message = CreateMessage(httpContext, e); - - Logger.LogError(message, e); + Logger.LogError(() => CreateMessage(httpContext, e), e); SetInternalServerErrorOnResponse(httpContext); } diff --git a/src/Ocelot/Headers/AddHeadersToRequest.cs b/src/Ocelot/Headers/AddHeadersToRequest.cs index d5a8c0629..d4a7bffab 100644 --- a/src/Ocelot/Headers/AddHeadersToRequest.cs +++ b/src/Ocelot/Headers/AddHeadersToRequest.cs @@ -64,7 +64,7 @@ public void SetHeadersOnDownstreamRequest(IEnumerable headers, HttpCo if (value.IsError) { - _logger.LogWarning($"Unable to add header to response {header.Key}: {header.Value}"); + _logger.LogWarning(() => $"Unable to add header to response {header.Key}: {header.Value}"); continue; } diff --git a/src/Ocelot/Headers/AddHeadersToResponse.cs b/src/Ocelot/Headers/AddHeadersToResponse.cs index 5ab5c175c..8260d1051 100644 --- a/src/Ocelot/Headers/AddHeadersToResponse.cs +++ b/src/Ocelot/Headers/AddHeadersToResponse.cs @@ -26,7 +26,7 @@ public void Add(List addHeaders, DownstreamResponse response) if (value.IsError) { - _logger.LogWarning($"Unable to add header to response {add.Key}: {add.Value}"); + _logger.LogWarning(() => $"Unable to add header to response {add.Key}: {add.Value}"); continue; } diff --git a/src/Ocelot/Headers/Middleware/ClaimsToHeadersMiddleware.cs b/src/Ocelot/Headers/Middleware/ClaimsToHeadersMiddleware.cs index 2feb14e0f..df8836409 100644 --- a/src/Ocelot/Headers/Middleware/ClaimsToHeadersMiddleware.cs +++ b/src/Ocelot/Headers/Middleware/ClaimsToHeadersMiddleware.cs @@ -24,7 +24,7 @@ public async Task Invoke(HttpContext httpContext) if (downstreamRoute.ClaimsToHeaders.Any()) { - Logger.LogInformation($"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to headers"); + Logger.LogInformation(() => $"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to headers"); var downstreamRequest = httpContext.Items.DownstreamRequest(); diff --git a/src/Ocelot/Logging/AspDotNetLogger.cs b/src/Ocelot/Logging/AspDotNetLogger.cs deleted file mode 100644 index c3aaf228e..000000000 --- a/src/Ocelot/Logging/AspDotNetLogger.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Microsoft.Extensions.Logging; -using Ocelot.Infrastructure.RequestData; - -namespace Ocelot.Logging -{ - public class AspDotNetLogger : IOcelotLogger - { - private readonly ILogger _logger; - private readonly IRequestScopedDataRepository _scopedDataRepository; - private readonly Func _func; - - public AspDotNetLogger(ILogger logger, IRequestScopedDataRepository scopedDataRepository) - { - _logger = logger; - _scopedDataRepository = scopedDataRepository; - _func = (state, exception) => exception == null ? state : $"{state}, exception: {exception}"; - } - - public void LogTrace(string message) - { - var requestId = GetOcelotRequestId(); - var previousRequestId = GetOcelotPreviousRequestId(); - - var state = $"requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}"; - - _logger.Log(LogLevel.Trace, default, state, null, _func); - } - - public void LogDebug(string message) - { - var requestId = GetOcelotRequestId(); - var previousRequestId = GetOcelotPreviousRequestId(); - - var state = $"requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}"; - - _logger.Log(LogLevel.Debug, default, state, null, _func); - } - - public void LogInformation(string message) - { - var requestId = GetOcelotRequestId(); - var previousRequestId = GetOcelotPreviousRequestId(); - - var state = $"requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}"; - - _logger.Log(LogLevel.Information, default, state, null, _func); - } - - public void LogWarning(string message) - { - var requestId = GetOcelotRequestId(); - var previousRequestId = GetOcelotPreviousRequestId(); - - var state = $"requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}"; - - _logger.Log(LogLevel.Warning, default, state, null, _func); - } - - public void LogError(string message, Exception exception) - { - var requestId = GetOcelotRequestId(); - var previousRequestId = GetOcelotPreviousRequestId(); - - var state = $"requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}"; - - _logger.Log(LogLevel.Error, default, state, exception, _func); - } - - public void LogCritical(string message, Exception exception) - { - var requestId = GetOcelotRequestId(); - var previousRequestId = GetOcelotPreviousRequestId(); - - var state = $"requestId: {requestId}, previousRequestId: {previousRequestId}, message: {message}"; - - _logger.Log(LogLevel.Critical, default, state, exception, _func); - } - - private string GetOcelotRequestId() - { - var requestId = _scopedDataRepository.Get("RequestId"); - - return requestId == null || requestId.IsError ? "no request id" : requestId.Data; - } - - private string GetOcelotPreviousRequestId() - { - var requestId = _scopedDataRepository.Get("PreviousRequestId"); - - return requestId == null || requestId.IsError ? "no previous request id" : requestId.Data; - } - } -} diff --git a/src/Ocelot/Logging/IOcelotLogger.cs b/src/Ocelot/Logging/IOcelotLogger.cs index 4be17052e..5b8a68fd4 100644 --- a/src/Ocelot/Logging/IOcelotLogger.cs +++ b/src/Ocelot/Logging/IOcelotLogger.cs @@ -1,20 +1,28 @@ -namespace Ocelot.Logging +using Ocelot.Configuration; +using Ocelot.Infrastructure.RequestData; + +namespace Ocelot.Logging; + +/// +/// Thin wrapper around the .NET Core logging framework, used to allow the object to be injected giving access to the Ocelot . +/// +public interface IOcelotLogger { - /// - /// Thin wrapper around the DotNet core logging framework, used to allow the scopedDataRepository to be injected giving access to the Ocelot RequestId. - /// - public interface IOcelotLogger - { - void LogTrace(string message); + void LogTrace(string message); + void LogTrace(Func messageFactory); - void LogDebug(string message); + void LogDebug(string message); + void LogDebug(Func messageFactory); - void LogInformation(string message); + void LogInformation(string message); + void LogInformation(Func messageFactory); - void LogWarning(string message); + void LogWarning(string message); + void LogWarning(Func messageFactory); - void LogError(string message, Exception exception); + void LogError(string message, Exception exception); + void LogError(Func messageFactory, Exception exception); - void LogCritical(string message, Exception exception); - } + void LogCritical(string message, Exception exception); + void LogCritical(Func messageFactory, Exception exception); } diff --git a/src/Ocelot/Logging/OcelotDiagnosticListener.cs b/src/Ocelot/Logging/OcelotDiagnosticListener.cs index 156ee1cc9..3cdb64aad 100644 --- a/src/Ocelot/Logging/OcelotDiagnosticListener.cs +++ b/src/Ocelot/Logging/OcelotDiagnosticListener.cs @@ -18,20 +18,20 @@ public OcelotDiagnosticListener(IOcelotLoggerFactory factory, IServiceProvider s [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareStarting")] public virtual void OnMiddlewareStarting(HttpContext httpContext, string name) { - _logger.LogTrace($"MiddlewareStarting: {name}; {httpContext.Request.Path}"); + _logger.LogTrace(() => $"MiddlewareStarting: {name}; {httpContext.Request.Path}"); Event(httpContext, $"MiddlewareStarting: {name}; {httpContext.Request.Path}"); } [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareException")] public virtual void OnMiddlewareException(Exception exception, string name) { - _logger.LogTrace($"MiddlewareException: {name}; {exception.Message};"); + _logger.LogTrace(() => $"MiddlewareException: {name}; {exception.Message};"); } [DiagnosticName("Microsoft.AspNetCore.MiddlewareAnalysis.MiddlewareFinished")] public virtual void OnMiddlewareFinished(HttpContext httpContext, string name) { - _logger.LogTrace($"MiddlewareFinished: {name}; {httpContext.Response.StatusCode}"); + _logger.LogTrace(() => $"MiddlewareFinished: {name}; {httpContext.Response.StatusCode}"); Event(httpContext, $"MiddlewareFinished: {name}; {httpContext.Response.StatusCode}"); } diff --git a/src/Ocelot/Logging/OcelotLogger.cs b/src/Ocelot/Logging/OcelotLogger.cs new file mode 100644 index 000000000..1528f3957 --- /dev/null +++ b/src/Ocelot/Logging/OcelotLogger.cs @@ -0,0 +1,95 @@ +using Microsoft.Extensions.Logging; +using Ocelot.Infrastructure.RequestData; +using Ocelot.RequestId.Middleware; + +namespace Ocelot.Logging; + +/// +/// Default implementation of the interface. +/// +public class OcelotLogger : IOcelotLogger +{ + private readonly ILogger _logger; + private readonly IRequestScopedDataRepository _scopedDataRepository; + private readonly Func _func; + + /// + /// Initializes a new instance of the class. + /// + /// Please note: + /// the log event message is designed to use placeholders ({RequestId}, {PreviousRequestId}, and {Message}). + /// If you're using a logger like Serilog, it will automatically capture these as structured data properties, making it easier to query and analyze the logs later. + /// + /// + /// The main logger type, per default the Microsoft implementation. + /// Repository, saving and getting data to/from HttpContext.Items. + /// The ILogger object is injected in OcelotLoggerFactory, it can't be verified before. + public OcelotLogger(ILogger logger, IRequestScopedDataRepository scopedDataRepository) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _scopedDataRepository = scopedDataRepository; + _func = (state, exception) => exception == null ? state : $"{state}, {nameof(exception)}: {exception}"; + } + + public void LogTrace(string message) => WriteLog(LogLevel.Trace, message); + public void LogTrace(Func messageFactory) => WriteLog(LogLevel.Trace, messageFactory); + + public void LogDebug(string message) => WriteLog(LogLevel.Debug, message); + public void LogDebug(Func messageFactory) => WriteLog(LogLevel.Debug, messageFactory); + + public void LogInformation(string message) => WriteLog(LogLevel.Information, message); + public void LogInformation(Func messageFactory) => WriteLog(LogLevel.Information, messageFactory); + + public void LogWarning(string message) => WriteLog(LogLevel.Warning, message); + public void LogWarning(Func messageFactory) => WriteLog(LogLevel.Warning, messageFactory); + + public void LogError(string message, Exception exception) => WriteLog(LogLevel.Error, message, exception); + public void LogError(Func messageFactory, Exception exception) => WriteLog(LogLevel.Error, messageFactory, exception); + + public void LogCritical(string message, Exception exception) => WriteLog(LogLevel.Critical, message, exception); + public void LogCritical(Func messageFactory, Exception exception) => WriteLog(LogLevel.Critical, messageFactory, exception); + + private string GetOcelotRequestId() + { + var requestId = _scopedDataRepository.Get(RequestIdMiddleware.RequestIdName); + return requestId?.IsError ?? true ? $"No {RequestIdMiddleware.RequestIdName}" : requestId.Data; + } + + private string GetOcelotPreviousRequestId() + { + var requestId = _scopedDataRepository.Get(RequestIdMiddleware.PreviousRequestIdName); + return requestId?.IsError ?? true ? $"No {RequestIdMiddleware.PreviousRequestIdName}" : requestId.Data; + } + + private void WriteLog(LogLevel logLevel, string message, Exception exception = null) + { + WriteLog(logLevel, null, message, exception); + } + + private void WriteLog(LogLevel logLevel, Func messageFactory, Exception exception = null) + { + WriteLog(logLevel, messageFactory, null, exception); + } + + private void WriteLog(LogLevel logLevel, Func messageFactory, string message, Exception exception = null) + { + if (!_logger.IsEnabled(logLevel)) + { + return; + } + + var requestId = GetOcelotRequestId(); + var previousRequestId = GetOcelotPreviousRequestId(); + + if (messageFactory != null) + { + message = messageFactory.Invoke() ?? string.Empty; + } + + _logger.Log(logLevel, + default, + $"{nameof(requestId)}: {requestId}, {nameof(previousRequestId)}: {previousRequestId}, {nameof(message)}: '{message}'", + exception, + _func); + } +} diff --git a/src/Ocelot/Logging/AspDotNetLoggerFactory.cs b/src/Ocelot/Logging/OcelotLoggerFactory.cs similarity index 66% rename from src/Ocelot/Logging/AspDotNetLoggerFactory.cs rename to src/Ocelot/Logging/OcelotLoggerFactory.cs index 8c53f6846..123caf2d2 100644 --- a/src/Ocelot/Logging/AspDotNetLoggerFactory.cs +++ b/src/Ocelot/Logging/OcelotLoggerFactory.cs @@ -3,12 +3,12 @@ namespace Ocelot.Logging { - public class AspDotNetLoggerFactory : IOcelotLoggerFactory + public class OcelotLoggerFactory : IOcelotLoggerFactory { private readonly ILoggerFactory _loggerFactory; private readonly IRequestScopedDataRepository _scopedDataRepository; - public AspDotNetLoggerFactory(ILoggerFactory loggerFactory, IRequestScopedDataRepository scopedDataRepository) + public OcelotLoggerFactory(ILoggerFactory loggerFactory, IRequestScopedDataRepository scopedDataRepository) { _loggerFactory = loggerFactory; _scopedDataRepository = scopedDataRepository; @@ -17,7 +17,7 @@ public AspDotNetLoggerFactory(ILoggerFactory loggerFactory, IRequestScopedDataRe public IOcelotLogger CreateLogger() { var logger = _loggerFactory.CreateLogger(); - return new AspDotNetLogger(logger, _scopedDataRepository); + return new OcelotLogger(logger, _scopedDataRepository); } } } diff --git a/src/Ocelot/QueryStrings/Middleware/ClaimsToQueryStringMiddleware.cs b/src/Ocelot/QueryStrings/Middleware/ClaimsToQueryStringMiddleware.cs index eb5c30fc8..0083fa453 100644 --- a/src/Ocelot/QueryStrings/Middleware/ClaimsToQueryStringMiddleware.cs +++ b/src/Ocelot/QueryStrings/Middleware/ClaimsToQueryStringMiddleware.cs @@ -24,7 +24,7 @@ public async Task Invoke(HttpContext httpContext) if (downstreamRoute.ClaimsToQueries.Any()) { - Logger.LogInformation($"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to queries"); + Logger.LogInformation(() => $"{downstreamRoute.DownstreamPathTemplate.Value} has instructions to convert claims to queries"); var downstreamRequest = httpContext.Items.DownstreamRequest(); diff --git a/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs b/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs index 6b62a14d2..47571046f 100644 --- a/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs +++ b/src/Ocelot/RateLimit/Middleware/ClientRateLimitMiddleware.cs @@ -28,7 +28,7 @@ public async Task Invoke(HttpContext httpContext) // check if rate limiting is enabled if (!downstreamRoute.EnableEndpointEndpointRateLimiting) { - Logger.LogInformation($"EndpointRateLimiting is not enabled for {downstreamRoute.DownstreamPathTemplate.Value}"); + Logger.LogInformation(() => $"EndpointRateLimiting is not enabled for {downstreamRoute.DownstreamPathTemplate.Value}"); await _next.Invoke(httpContext); return; } @@ -39,7 +39,7 @@ public async Task Invoke(HttpContext httpContext) // check white list if (IsWhitelisted(identity, options)) { - Logger.LogInformation($"{downstreamRoute.DownstreamPathTemplate.Value} is white listed from rate limiting"); + Logger.LogInformation(() => $"{downstreamRoute.DownstreamPathTemplate.Value} is white listed from rate limiting"); await _next.Invoke(httpContext); return; } @@ -110,7 +110,7 @@ public bool IsWhitelisted(ClientRequestIdentity requestIdentity, RateLimitOption public virtual void LogBlockedRequest(HttpContext httpContext, ClientRequestIdentity identity, RateLimitCounter counter, RateLimitRule rule, DownstreamRoute downstreamRoute) { Logger.LogInformation( - $"Request {identity.HttpVerb}:{identity.Path} from ClientId {identity.ClientId} has been blocked, quota {rule.Limit}/{rule.Period} exceeded by {counter.TotalRequests}. Blocked by rule {downstreamRoute.UpstreamPathTemplate.OriginalValue}, TraceIdentifier {httpContext.TraceIdentifier}."); + () => $"Request {identity.HttpVerb}:{identity.Path} from ClientId {identity.ClientId} has been blocked, quota {rule.Limit}/{rule.Period} exceeded by {counter.TotalRequests}. Blocked by rule {downstreamRoute.UpstreamPathTemplate.OriginalValue}, TraceIdentifier {httpContext.TraceIdentifier}."); } public virtual DownstreamResponse ReturnQuotaExceededResponse(HttpContext httpContext, RateLimitOptions option, string retryAfter) diff --git a/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs b/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs index 795fd89f9..65cad3252 100644 --- a/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs +++ b/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; using Ocelot.Infrastructure.RequestData; using Ocelot.Logging; using Ocelot.Middleware; @@ -9,8 +10,12 @@ namespace Ocelot.RequestId.Middleware { public class RequestIdMiddleware : OcelotMiddleware { + public const string RequestIdName = nameof(IInternalConfiguration.RequestId); + public const string PreviousRequestIdName = "Previous" + nameof(IInternalConfiguration.RequestId); + private readonly RequestDelegate _next; private readonly IRequestScopedDataRepository _requestScopedDataRepository; + public RequestIdMiddleware(RequestDelegate next, IOcelotLoggerFactory loggerFactory, IRequestScopedDataRepository requestScopedDataRepository) @@ -36,15 +41,15 @@ private void SetOcelotRequestId(HttpContext httpContext) { httpContext.TraceIdentifier = upstreamRequestIds.First(); - var previousRequestId = _requestScopedDataRepository.Get("RequestId"); + var previousRequestId = _requestScopedDataRepository.Get(RequestIdName); if (!previousRequestId.IsError && !string.IsNullOrEmpty(previousRequestId.Data) && previousRequestId.Data != httpContext.TraceIdentifier) { - _requestScopedDataRepository.Add("PreviousRequestId", previousRequestId.Data); - _requestScopedDataRepository.Update("RequestId", httpContext.TraceIdentifier); + _requestScopedDataRepository.Add(PreviousRequestIdName, previousRequestId.Data); + _requestScopedDataRepository.Update(RequestIdName, httpContext.TraceIdentifier); } else { - _requestScopedDataRepository.Add("RequestId", httpContext.TraceIdentifier); + _requestScopedDataRepository.Add(RequestIdName, httpContext.TraceIdentifier); } } diff --git a/src/Ocelot/Requester/DelegatingHandlerHandlerFactory.cs b/src/Ocelot/Requester/DelegatingHandlerHandlerFactory.cs index 02b691c4c..8b7cc7955 100644 --- a/src/Ocelot/Requester/DelegatingHandlerHandlerFactory.cs +++ b/src/Ocelot/Requester/DelegatingHandlerHandlerFactory.cs @@ -71,7 +71,7 @@ public Response>> Get(DownstreamRoute downstreamRou } else { - _logger.LogWarning($"Route {downstreamRoute.UpstreamPathTemplate} specifies use QoS but no QosHandler found in DI container. Will use not use a QosHandler, please check your setup!"); + _logger.LogWarning(() => $"Route {downstreamRoute.UpstreamPathTemplate} specifies use QoS but no QosHandler found in DI container. Will use not use a QosHandler, please check your setup!"); handlers.Add(() => new NoQosDelegatingHandler()); } } diff --git a/src/Ocelot/Requester/HttpClientBuilder.cs b/src/Ocelot/Requester/HttpClientBuilder.cs index 0fd818e66..99b2bec3e 100644 --- a/src/Ocelot/Requester/HttpClientBuilder.cs +++ b/src/Ocelot/Requester/HttpClientBuilder.cs @@ -47,7 +47,7 @@ public IHttpClient Create(DownstreamRoute downstreamRoute) HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; _logger - .LogWarning($"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamRoute, UpstreamPathTemplate: {downstreamRoute.UpstreamPathTemplate}, DownstreamPathTemplate: {downstreamRoute.DownstreamPathTemplate}"); + .LogWarning(() => $"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamRoute, UpstreamPathTemplate: {downstreamRoute.UpstreamPathTemplate}, DownstreamPathTemplate: {downstreamRoute.DownstreamPathTemplate}"); } var timeout = downstreamRoute.QosOptions.TimeoutValue == 0 diff --git a/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs b/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs index 5a56ab7a4..d996d2983 100644 --- a/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs +++ b/src/Ocelot/Requester/Middleware/HttpRequesterMiddleware.cs @@ -46,13 +46,13 @@ private void CreateLogBasedOnResponse(Response response) { if (response.Data?.StatusCode <= HttpStatusCode.BadRequest) { - Logger.LogInformation( + Logger.LogInformation(() => $"{(int)response.Data.StatusCode} ({response.Data.ReasonPhrase}) status code, request uri: {response.Data.RequestMessage?.RequestUri}"); } else if (response.Data?.StatusCode >= HttpStatusCode.BadRequest) { Logger.LogWarning( - $"{(int)response.Data.StatusCode} ({response.Data.ReasonPhrase}) status code, request uri: {response.Data.RequestMessage?.RequestUri}"); + () => $"{(int)response.Data.StatusCode} ({response.Data.ReasonPhrase}) status code, request uri: {response.Data.RequestMessage?.RequestUri}"); } } } diff --git a/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs index 925663f71..76db720a1 100644 --- a/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs +++ b/src/Ocelot/Responder/Middleware/ResponderMiddleware.cs @@ -37,13 +37,13 @@ public async Task Invoke(HttpContext httpContext) // todo check errors is ok if (errors.Count > 0) { - Logger.LogWarning($"{errors.ToErrorString()} errors found in {MiddlewareName}. Setting error response for request path:{httpContext.Request.Path}, request method: {httpContext.Request.Method}"); + Logger.LogWarning(() => $"{errors.ToErrorString()} errors found in {MiddlewareName}. Setting error response for request path:{httpContext.Request.Path}, request method: {httpContext.Request.Method}"); SetErrorResponse(httpContext, errors); } else if (downstreamResponse == null) { - Logger.LogDebug($"Pipeline was terminated early in {MiddlewareName}"); + Logger.LogDebug(() => $"Pipeline was terminated early in {MiddlewareName}"); } else { diff --git a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs index 174c973ab..b47e4d921 100644 --- a/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs +++ b/src/Ocelot/ServiceDiscovery/ServiceDiscoveryProviderFactory.cs @@ -1,51 +1,51 @@ -using Microsoft.Extensions.DependencyInjection; -using Ocelot.Configuration; -using Ocelot.Logging; -using Ocelot.Responses; -using Ocelot.ServiceDiscovery.Configuration; -using Ocelot.ServiceDiscovery.Providers; -using Ocelot.Values; +using Microsoft.Extensions.DependencyInjection; +using Ocelot.Configuration; +using Ocelot.Logging; +using Ocelot.Responses; +using Ocelot.ServiceDiscovery.Configuration; +using Ocelot.ServiceDiscovery.Providers; +using Ocelot.Values; namespace Ocelot.ServiceDiscovery { public class ServiceDiscoveryProviderFactory : IServiceDiscoveryProviderFactory { - private readonly IServiceProvider _provider; - private readonly ServiceDiscoveryFinderDelegate _delegates; + private readonly IServiceProvider _provider; + private readonly ServiceDiscoveryFinderDelegate _delegates; private readonly IOcelotLogger _logger; public ServiceDiscoveryProviderFactory(IOcelotLoggerFactory factory, IServiceProvider provider) { _provider = provider; - _delegates = provider.GetService(); + _delegates = provider.GetService(); _logger = factory.CreateLogger(); } public Response Get(ServiceProviderConfiguration serviceConfig, DownstreamRoute route) { if (route.UseServiceDiscovery) - { - var routeName = route.UpstreamPathTemplate?.Template ?? route.ServiceName ?? string.Empty; - _logger.LogInformation($"The {nameof(DownstreamRoute.UseServiceDiscovery)} mode of the route '{routeName}' is enabled."); + { + var routeName = route.UpstreamPathTemplate?.Template ?? route.ServiceName ?? string.Empty; + _logger.LogInformation(() => $"The {nameof(DownstreamRoute.UseServiceDiscovery)} mode of the route '{routeName}' is enabled."); return GetServiceDiscoveryProvider(serviceConfig, route); } - var services = route.DownstreamAddresses - .Select(address => new Service( - route.ServiceName, - new ServiceHostAndPort(address.Host, address.Port, route.DownstreamScheme), - string.Empty, - string.Empty, - Enumerable.Empty())) - .ToList(); + var services = route.DownstreamAddresses + .Select(address => new Service( + route.ServiceName, + new ServiceHostAndPort(address.Host, address.Port, route.DownstreamScheme), + string.Empty, + string.Empty, + Enumerable.Empty())) + .ToList(); return new OkResponse(new ConfigurationServiceProvider(services)); } private Response GetServiceDiscoveryProvider(ServiceProviderConfiguration config, DownstreamRoute route) - { - _logger.LogInformation($"Getting service discovery provider of {nameof(config.Type)} '{config.Type}'..."); - + { + _logger.LogInformation(() => $"Getting service discovery provider of {nameof(config.Type)} '{config.Type}'..."); + if (config.Type?.ToLower() == "servicefabric") { var sfConfig = new ServiceFabricConfiguration(config.Host, config.Port, route.ServiceName); @@ -61,10 +61,10 @@ private Response GetServiceDiscoveryProvider(ServiceP return new OkResponse(provider); } } - - var message = $"Unable to find service discovery provider for {nameof(config.Type)}: '{config.Type}'!"; - _logger.LogWarning(message); - + + var message = $"Unable to find service discovery provider for {nameof(config.Type)}: '{config.Type}'!"; + _logger.LogWarning(() => $"Unable to find service discovery provider for {nameof(config.Type)}: '{config.Type}'!"); + return new ErrorResponse(new UnableToFindServiceDiscoveryProviderError(message)); } } diff --git a/src/Ocelot/WebSockets/WebSocketsProxyMiddleware.cs b/src/Ocelot/WebSockets/WebSocketsProxyMiddleware.cs index 86b7d963f..385efc831 100644 --- a/src/Ocelot/WebSockets/WebSocketsProxyMiddleware.cs +++ b/src/Ocelot/WebSockets/WebSocketsProxyMiddleware.cs @@ -110,7 +110,7 @@ private async Task Proxy(HttpContext context, DownstreamRequest request, Downstr if (route.DangerousAcceptAnyServerCertificateValidator) { client.Options.RemoteCertificateValidationCallback = (request, certificate, chain, errors) => true; - Logger.LogWarning(string.Format(IgnoredSslWarningFormat, route.UpstreamPathTemplate, route.DownstreamPathTemplate)); + Logger.LogWarning(() => string.Format(IgnoredSslWarningFormat, route.UpstreamPathTemplate, route.DownstreamPathTemplate)); } foreach (var protocol in context.WebSockets.WebSocketRequestedProtocols) @@ -139,7 +139,7 @@ private async Task Proxy(HttpContext context, DownstreamRequest request, Downstr var scheme = request.Scheme; if (!scheme.StartsWith(Uri.UriSchemeWs)) { - Logger.LogWarning(string.Format(InvalidSchemeWarningFormat, scheme, request.ToUri())); + Logger.LogWarning(() => string.Format(InvalidSchemeWarningFormat, scheme, request.ToUri())); request.Scheme = scheme == Uri.UriSchemeHttp ? Uri.UriSchemeWs : scheme == Uri.UriSchemeHttps ? Uri.UriSchemeWss : scheme; } diff --git a/test/Ocelot.AcceptanceTests/AggregateTests.cs b/test/Ocelot.AcceptanceTests/AggregateTests.cs index 758b4295d..7cb4c03e3 100644 --- a/test/Ocelot.AcceptanceTests/AggregateTests.cs +++ b/test/Ocelot.AcceptanceTests/AggregateTests.cs @@ -289,7 +289,7 @@ public void should_return_response_200_with_simple_url_user_defined_aggregate() this.Given(x => x.GivenServiceOneIsRunning($"http://localhost:{port1}", "/", 200, "{Hello from Laura}")) .Given(x => x.GivenServiceTwoIsRunning($"http://localhost:{port2}", "/", 200, "{Hello from Tom}")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithSpecficAggregatorsRegisteredInDi()) + .And(x => _steps.GivenOcelotIsRunningWithSpecificAggregatorsRegisteredInDi()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe(expected)) diff --git a/test/Ocelot.AcceptanceTests/AuthenticationTests.cs b/test/Ocelot.AcceptanceTests/AuthenticationTests.cs index 29656437c..35329847f 100644 --- a/test/Ocelot.AcceptanceTests/AuthenticationTests.cs +++ b/test/Ocelot.AcceptanceTests/AuthenticationTests.cs @@ -364,7 +364,7 @@ private void GivenThereIsAnIdentityServerOn(string url, string apiName, string a _identityServerBuilder.Start(); - _steps.VerifyIdentiryServerStarted(url); + Steps.VerifyIdentityServerStarted(url); } public void Dispose() diff --git a/test/Ocelot.AcceptanceTests/AuthorizationTests.cs b/test/Ocelot.AcceptanceTests/AuthorizationTests.cs index c15ebade7..26dcfb1a4 100644 --- a/test/Ocelot.AcceptanceTests/AuthorizationTests.cs +++ b/test/Ocelot.AcceptanceTests/AuthorizationTests.cs @@ -393,7 +393,7 @@ private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTo _identityServerBuilder.Start(); - _steps.VerifyIdentiryServerStarted(url); + Steps.VerifyIdentityServerStarted(url); } private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTokenType tokenType, List users) @@ -464,7 +464,7 @@ private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTo _identityServerBuilder.Start(); - _steps.VerifyIdentiryServerStarted(url); + Steps.VerifyIdentityServerStarted(url); } public void Dispose() diff --git a/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs index 3fed134a4..b3697f0d6 100644 --- a/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs +++ b/test/Ocelot.AcceptanceTests/ClaimsToDownstreamPathTests.cs @@ -196,7 +196,7 @@ private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTo _identityServerBuilder.Start(); - _steps.VerifyIdentiryServerStarted(url); + Steps.VerifyIdentityServerStarted(url); } public void Dispose() diff --git a/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs index 00aa955c8..18cf8cdd0 100644 --- a/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs +++ b/test/Ocelot.AcceptanceTests/ClaimsToHeadersForwardingTests.cs @@ -190,7 +190,7 @@ private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTo _identityServerBuilder.Start(); - _steps.VerifyIdentiryServerStarted(url); + Steps.VerifyIdentityServerStarted(url); } public void Dispose() diff --git a/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs b/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs index e8325d612..9a89fef62 100644 --- a/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs +++ b/test/Ocelot.AcceptanceTests/ClaimsToQueryStringForwardingTests.cs @@ -277,7 +277,7 @@ private void GivenThereIsAnIdentityServerOn(string url, string apiName, AccessTo _identityServerBuilder.Start(); - _steps.VerifyIdentiryServerStarted(url); + Steps.VerifyIdentityServerStarted(url); } public void Dispose() diff --git a/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs b/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs index 33246f147..5fc91dd8d 100644 --- a/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs +++ b/test/Ocelot.AcceptanceTests/CustomMiddlewareTests.cs @@ -376,7 +376,7 @@ public void should_fix_issue_237() this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 200, "/test")) .And(x => _steps.GivenThereIsAConfiguration(fileConfiguration)) - .And(x => _steps.GivenOcelotIsRunningWithMiddleareBeforePipeline(callback)) + .And(x => _steps.GivenOcelotIsRunningWithMiddlewareBeforePipeline(callback)) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.NotFound)) .BDDfy(); diff --git a/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs b/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs index ac475a75a..83f79e720 100644 --- a/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs +++ b/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs @@ -49,7 +49,7 @@ public void should_call_re_route_ordered_specific_handlers() this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithSpecficHandlersRegisteredInDi()) + .And(x => _steps.GivenOcelotIsRunningWithSpecificHandlersRegisteredInDi()) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) diff --git a/test/Ocelot.AcceptanceTests/LogLevelTests.cs b/test/Ocelot.AcceptanceTests/LogLevelTests.cs new file mode 100644 index 000000000..f4fb67bdc --- /dev/null +++ b/test/Ocelot.AcceptanceTests/LogLevelTests.cs @@ -0,0 +1,175 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Ocelot.Configuration.File; +using Serilog; +using Serilog.Core; + +namespace Ocelot.AcceptanceTests; + +public class LogLevelTests : IDisposable +{ + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + private readonly string _logFileName; + private readonly string _appSettingsFileName; + + private const string AppSettingsFormat = + "{{\"Logging\":{{\"LogLevel\":{{\"Default\":\"{0}\",\"System\":\"{0}\",\"Microsoft\":\"{0}\"}}}}}}"; + + public LogLevelTests() + { + _steps = new Steps(); + _serviceHandler = new ServiceHandler(); + _logFileName = $"ocelot_logs_{Guid.NewGuid()}.log"; + _appSettingsFileName = $"appsettings_{Guid.NewGuid()}.json"; + } + + private void ThenMessagesAreLogged(string[] notAllowedMessageTypes, string[] allowedMessageTypes) + { + var logFilePath = GetLogFilePath(); + var logFileContent = File.ReadAllText(logFilePath); + var logFileLines = logFileContent.Split(Environment.NewLine); + + var logFileLinesWithLogLevel = logFileLines.Where(x => notAllowedMessageTypes.Any(x.Contains)).ToList(); + logFileLinesWithLogLevel.Count.ShouldBe(0); + + var logFileLinesWithAllowedLogLevel = logFileLines.Where(x => allowedMessageTypes.Any(x.Contains)).ToList(); + logFileLinesWithAllowedLogLevel.Count.ShouldBe(2 * allowedMessageTypes.Length); + } + + private void TestFactory(string[] notAllowedMessageTypes, string[] allowedMessageTypes, LogLevel level) + { + var port = PortFinder.GetRandomPort(); + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = port, + }, + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + RequestIdKey = _steps.RequestIdKey, + }, + }, + }; + + var logger = GetLogger(level); + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => _steps.GivenOcelotIsRunningWithMinimumLogLevel(logger, _appSettingsFileName)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .Then(x => _steps.Dispose()) + .Then(x => logger.Dispose()) + .Then(x => ThenMessagesAreLogged(notAllowedMessageTypes, allowedMessageTypes)) + .BDDfy(); + } + + [Fact] + public void if_minimum_log_level_is_critical_then_only_critical_messages_are_logged() => TestFactory(new[] { "TRACE", "INFORMATION", "WARNING", "ERROR" }, new[] { "CRITICAL" }, LogLevel.Critical); + + [Fact] + public void if_minimum_log_level_is_error_then_critical_and_error_are_logged() => TestFactory(new[] { "TRACE", "INFORMATION", "WARNING", "DEBUG" }, new[] { "CRITICAL", "ERROR" }, LogLevel.Error); + + [Fact] + public void if_minimum_log_level_is_warning_then_critical_error_and_warning_are_logged() => TestFactory(new[] { "TRACE", "INFORMATION", "DEBUG" }, new[] { "CRITICAL", "ERROR", "WARNING" }, LogLevel.Warning); + + [Fact] + public void if_minimum_log_level_is_information_then_critical_error_warning_and_information_are_logged() => TestFactory(new[] { "TRACE", "DEBUG" }, new[] { "CRITICAL", "ERROR", "WARNING", "INFORMATION" }, LogLevel.Information); + + [Fact] + public void if_minimum_log_level_is_debug_then_critical_error_warning_information_and_debug_are_logged() => TestFactory(new[] { "TRACE" }, new[] { "DEBUG", "CRITICAL", "ERROR", "WARNING", "INFORMATION" }, LogLevel.Debug); + + [Fact] + public void if_minimum_log_level_is_trace_then_critical_error_warning_information_debug_and_trace_are_logged() => TestFactory(Array.Empty(), new[] { "TRACE", "DEBUG", "CRITICAL", "ERROR", "WARNING", "INFORMATION" }, LogLevel.Trace); + + private Logger GetLogger(LogLevel logLevel) + { + var logFilePath = ResetLogFile(); + UpdateAppSettings(logLevel); + var logger = logLevel switch + { + LogLevel.Information => new LoggerConfiguration().MinimumLevel.Information() + .WriteTo.File(logFilePath) + .CreateLogger(), + LogLevel.Warning => new LoggerConfiguration().MinimumLevel.Warning() + .WriteTo.File(logFilePath) + .CreateLogger(), + LogLevel.Error => new LoggerConfiguration().MinimumLevel.Error() + .WriteTo.File(logFilePath) + .CreateLogger(), + LogLevel.Critical => new LoggerConfiguration().MinimumLevel.Fatal() + .WriteTo.File(logFilePath) + .CreateLogger(), + LogLevel.Debug => new LoggerConfiguration().MinimumLevel.Debug() + .WriteTo.File(logFilePath) + .CreateLogger(), + LogLevel.Trace => new LoggerConfiguration().MinimumLevel.Verbose() + .WriteTo.File(logFilePath) + .CreateLogger(), + LogLevel.None => new LoggerConfiguration() + .WriteTo.File(logFilePath) + .CreateLogger(), + _ => throw new ArgumentOutOfRangeException(nameof(logLevel), logLevel, null), + }; + return logger; + } + + private void UpdateAppSettings(LogLevel logLevel) + { + var appSettingsFilePath = Path.Combine(AppContext.BaseDirectory, _appSettingsFileName); + if (File.Exists(appSettingsFilePath)) + { + File.Delete(appSettingsFilePath); + } + + var appSettings = string.Format(AppSettingsFormat, Enum.GetName(typeof(LogLevel), logLevel)); + File.WriteAllText(appSettingsFilePath, appSettings); + } + + private string ResetLogFile() + { + var logFilePath = GetLogFilePath(); + if (File.Exists(logFilePath)) + { + File.Delete(logFilePath); + } + + return logFilePath; + } + + private string GetLogFilePath() + { + var logFilePath = Path.Combine(AppContext.BaseDirectory, _logFileName); + return logFilePath; + } + + private void GivenThereIsAServiceRunningOn(string baseUrl) + { + _serviceHandler.GivenThereIsAServiceRunningOn(baseUrl, async context => + { + context.Response.StatusCode = 200; + await context.Response.WriteAsync(string.Empty); + }); + } + + public void Dispose() + { + _serviceHandler?.Dispose(); + _steps.Dispose(); + ResetLogFile(); + GC.SuppressFinalize(this); + } +} diff --git a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj index 29a6d5612..390879fc4 100644 --- a/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj +++ b/test/Ocelot.AcceptanceTests/Ocelot.AcceptanceTests.csproj @@ -45,6 +45,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -80,6 +81,7 @@ + @@ -92,6 +94,7 @@ + @@ -104,6 +107,7 @@ + diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index fb78fa6e7..d87391b32 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -26,6 +26,8 @@ using Ocelot.ServiceDiscovery.Providers; using Ocelot.Tracing.Butterfly; using Ocelot.Tracing.OpenTracing; +using Serilog; +using Serilog.Core; using System.IO.Compression; using System.Net.Http.Headers; using System.Text; @@ -34,30 +36,43 @@ using CookieHeaderValue = Microsoft.Net.Http.Headers.CookieHeaderValue; using MediaTypeHeaderValue = System.Net.Http.Headers.MediaTypeHeaderValue; -namespace Ocelot.AcceptanceTests +namespace Ocelot.AcceptanceTests; + +public class Steps : IDisposable { - public class Steps : IDisposable - { - private TestServer _ocelotServer; - private HttpClient _ocelotClient; - private HttpResponseMessage _response; - private HttpContent _postContent; - private BearerToken _token; - public string RequestIdKey = "OcRequestId"; - private readonly Random _random; - private readonly string _ocelotConfigFileName; - private IWebHostBuilder _webHostBuilder; - private WebHostBuilder _ocelotBuilder; - private IWebHost _ocelotHost; - private IOcelotConfigurationChangeTokenSource _changeToken; - - public Steps() - { - _random = new(); - _ocelotConfigFileName = $"{Guid.NewGuid():N}-ocelot.json"; - } + private TestServer _ocelotServer; + private HttpClient _ocelotClient; + private HttpResponseMessage _response; + private HttpContent _postContent; + private BearerToken _token; + public string RequestIdKey = "OcRequestId"; + private readonly Random _random; + private readonly string _ocelotConfigFileName; + private IWebHostBuilder _webHostBuilder; + private WebHostBuilder _ocelotBuilder; + private IWebHost _ocelotHost; + private IOcelotConfigurationChangeTokenSource _changeToken; + + public Steps() + { + _random = new Random(); + _ocelotConfigFileName = $"{Guid.NewGuid():N}-ocelot.json"; + } + + public async Task ThenConfigShouldBe(FileConfiguration fileConfig) + { + var internalConfigCreator = _ocelotServer.Host.Services.GetService(); + var internalConfigRepo = _ocelotServer.Host.Services.GetService(); + + var internalConfig = internalConfigRepo.Get(); + var config = await internalConfigCreator.Create(fileConfig); - public async Task ThenConfigShouldBe(FileConfiguration fileConfig) + internalConfig.Data.RequestId.ShouldBe(config.Data.RequestId); + } + + public async Task ThenConfigShouldBeWithTimeout(FileConfiguration fileConfig, int timeoutMs) + { + var result = await Wait.WaitFor(timeoutMs).Until(async () => { var internalConfigCreator = _ocelotServer.Host.Services.GetService(); var internalConfigRepo = _ocelotServer.Host.Services.GetService(); @@ -65,1242 +80,1184 @@ public async Task ThenConfigShouldBe(FileConfiguration fileConfig) var internalConfig = internalConfigRepo.Get(); var config = await internalConfigCreator.Create(fileConfig); - internalConfig.Data.RequestId.ShouldBe(config.Data.RequestId); - } - - public async Task ThenConfigShouldBeWithTimeout(FileConfiguration fileConfig, int timeoutMs) - { - var result = await Wait.WaitFor(timeoutMs).Until(async () => - { - var internalConfigCreator = _ocelotServer.Host.Services.GetService(); - var internalConfigRepo = _ocelotServer.Host.Services.GetService(); - - var internalConfig = internalConfigRepo.Get(); - var config = await internalConfigCreator.Create(fileConfig); + return internalConfig.Data.RequestId == config.Data.RequestId; + }); - return internalConfig.Data.RequestId == config.Data.RequestId; - }); - - result.ShouldBe(true); - } + result.ShouldBe(true); + } - public async Task StartFakeOcelotWithWebSockets() - { - _ocelotBuilder = new WebHostBuilder(); - _ocelotBuilder.ConfigureServices(s => + public async Task StartFakeOcelotWithWebSockets() + { + _ocelotBuilder = new WebHostBuilder(); + _ocelotBuilder.ConfigureServices(s => + { + s.AddSingleton(_ocelotBuilder); + s.AddOcelot(); + }); + _ocelotBuilder.UseKestrel() + .UseUrls("http://localhost:5000") + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => { - s.AddSingleton(_ocelotBuilder); - s.AddOcelot(); - }); - _ocelotBuilder.UseKestrel() - .UseUrls("http://localhost:5000") - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureLogging((hostingContext, logging) => - { - logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); - logging.AddConsole(); - }) - .Configure(app => - { - app.UseWebSockets(); - app.UseOcelot().Wait(); - }) - .UseIISIntegration(); - _ocelotHost = _ocelotBuilder.Build(); - await _ocelotHost.StartAsync(); - } - - public async Task StartFakeOcelotWithWebSocketsWithConsul() - { - _ocelotBuilder = new WebHostBuilder(); - _ocelotBuilder.ConfigureServices(s => + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureLogging((hostingContext, logging) => { - s.AddSingleton(_ocelotBuilder); - s.AddOcelot().AddConsul(); - }); - _ocelotBuilder.UseKestrel() - .UseUrls("http://localhost:5000") - .UseContentRoot(Directory.GetCurrentDirectory()) - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureLogging((hostingContext, logging) => - { - logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); - logging.AddConsole(); - }) - .Configure(app => - { - app.UseWebSockets(); - app.UseOcelot().Wait(); - }) - .UseIISIntegration(); - _ocelotHost = _ocelotBuilder.Build(); - await _ocelotHost.StartAsync(); - } - - public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) - { - var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); - File.WriteAllText(_ocelotConfigFileName, jsonConfiguration); - } - - private void DeleteOcelotConfig() - { - if (!File.Exists(_ocelotConfigFileName)) + logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + logging.AddConsole(); + }) + .Configure(app => { - return; - } + app.UseWebSockets(); + app.UseOcelot().Wait(); + }) + .UseIISIntegration(); + _ocelotHost = _ocelotBuilder.Build(); + await _ocelotHost.StartAsync(); + } - try + public async Task StartFakeOcelotWithWebSocketsWithConsul() + { + _ocelotBuilder = new WebHostBuilder(); + _ocelotBuilder.ConfigureServices(s => + { + s.AddSingleton(_ocelotBuilder); + s.AddOcelot().AddConsul(); + }); + _ocelotBuilder.UseKestrel() + .UseUrls("http://localhost:5000") + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => { - File.Delete(_ocelotConfigFileName); - } - catch (Exception e) + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureLogging((hostingContext, logging) => { - Console.WriteLine(e); - } - } + logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging")); + logging.AddConsole(); + }) + .Configure(app => + { + app.UseWebSockets(); + app.UseOcelot().Wait(); + }) + .UseIISIntegration(); + _ocelotHost = _ocelotBuilder.Build(); + await _ocelotHost.StartAsync(); + } + + public void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + { + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration, Formatting.Indented); + File.WriteAllText(_ocelotConfigFileName, jsonConfiguration); + } - public void ThenTheResponseBodyHeaderIs(string key, string value) + private void DeleteOcelotConfig() + { + if (!File.Exists(_ocelotConfigFileName)) { - var header = _response.Content.Headers.GetValues(key); - header.First().ShouldBe(value); + return; } - public void GivenOcelotIsRunningReloadingConfig(bool shouldReload) + try { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: false, reloadOnChange: shouldReload); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); - - _ocelotServer = new TestServer(_webHostBuilder); - - _ocelotClient = _ocelotServer.CreateClient(); + File.Delete(_ocelotConfigFileName); } - - public void GivenIHaveAChangeToken() + catch (Exception e) { - _changeToken = _ocelotServer.Host.Services.GetRequiredService(); + Console.WriteLine(e); } + } - /// - /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. - /// - public void GivenOcelotIsRunning() - { - _webHostBuilder = new WebHostBuilder(); + public void ThenTheResponseBodyHeaderIs(string key, string value) + { + var header = _response.Content.Headers.GetValues(key); + header.First().ShouldBe(value); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + public void GivenOcelotIsRunningReloadingConfig(bool shouldReload) + { + _webHostBuilder = new WebHostBuilder(); - _ocelotServer = new TestServer(_webHostBuilder); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, shouldReload); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => { s.AddOcelot(); }) + .Configure(app => { app.UseOcelot().Wait(); }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } - _ocelotClient = _ocelotServer.CreateClient(); - } + public void GivenIHaveAChangeToken() + { + _changeToken = _ocelotServer.Host.Services.GetRequiredService(); + } - /// - /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. - /// - /// The type. - /// The delegate object to load balancer factory. - public void GivenOcelotIsRunningWithCustomLoadBalancer(Func loadBalancerFactoryFunc) - where T : ILoadBalancer - { - _webHostBuilder = new WebHostBuilder(); + /// + /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. + /// + public void GivenOcelotIsRunning() + { + _webHostBuilder = new WebHostBuilder(); - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddCustomLoadBalancer(loadBalancerFactoryFunc); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => { s.AddOcelot(); }) + .Configure(app => { app.UseOcelot().Wait(); }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } - _ocelotServer = new TestServer(_webHostBuilder); + /// + /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. + /// + /// The type. + /// The delegate object to load balancer factory. + public void GivenOcelotIsRunningWithCustomLoadBalancer( + Func loadBalancerFactoryFunc) + where T : ILoadBalancer + { + _webHostBuilder = new WebHostBuilder(); - _ocelotClient = _ocelotServer.CreateClient(); - } + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot() + .AddCustomLoadBalancer(loadBalancerFactoryFunc); + }) + .Configure(app => { app.UseOcelot().Wait(); }); - public void GivenOcelotIsRunningWithConsul() - { - _webHostBuilder = new WebHostBuilder(); + _ocelotServer = new TestServer(_webHostBuilder); - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot().AddConsul(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + _ocelotClient = _ocelotServer.CreateClient(); + } - _ocelotServer = new TestServer(_webHostBuilder); + public void GivenOcelotIsRunningWithConsul() + { + _webHostBuilder = new WebHostBuilder(); - _ocelotClient = _ocelotServer.CreateClient(); - } + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => { s.AddOcelot().AddConsul(); }) + .Configure(app => { app.UseOcelot().Wait(); }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } - public void ThenTheTraceHeaderIsSet(string key) - { - var header = _response.Headers.GetValues(key); - header.First().ShouldNotBeNullOrEmpty(); - } + public void ThenTheTraceHeaderIsSet(string key) + { + var header = _response.Headers.GetValues(key); + header.First().ShouldNotBeNullOrEmpty(); + } - internal void GivenOcelotIsRunningUsingButterfly(string butterflyUrl) - { - _webHostBuilder = new WebHostBuilder(); + internal void GivenOcelotIsRunningUsingButterfly(string butterflyUrl) + { + _webHostBuilder = new WebHostBuilder(); - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot() .AddButterfly(option => { //this is the url that the butterfly collector server is running on... option.CollectorUrl = butterflyUrl; option.Service = "Ocelot"; }); - }) - .Configure(app => - { - app.Use(async (context, next) => - { - await next.Invoke(); - }); - app.UseOcelot().Wait(); - }); + }) + .Configure(app => + { + app.Use(async (_, next) => { await next.Invoke(); }); + app.UseOcelot().Wait(); + }); - _ocelotServer = new TestServer(_webHostBuilder); + _ocelotServer = new TestServer(_webHostBuilder); - _ocelotClient = _ocelotServer.CreateClient(); - } + _ocelotClient = _ocelotServer.CreateClient(); + } - public void GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache() - { - _webHostBuilder = new WebHostBuilder(); + public void GivenOcelotIsRunningUsingConsulToStoreConfigAndJsonSerializedCache() + { + _webHostBuilder = new WebHostBuilder(); - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddCacheManager((x) => - { - x.WithMicrosoftLogging(log => - { - //log.AddConsole(LogLevel.Debug); - }) - .WithJsonSerializer() - .WithHandle(typeof(InMemoryJsonHandle<>)); - }) - .AddConsul() - .AddConfigStoredInConsul(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot() + .AddCacheManager((x) => + { + x.WithMicrosoftLogging(_ => + { + //log.AddConsole(LogLevel.Debug); + }) + .WithJsonSerializer() + .WithHandle(typeof(InMemoryJsonHandle<>)); + }) + .AddConsul() + .AddConfigStoredInConsul(); + }) + .Configure(app => { app.UseOcelot().Wait(); }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } - _ocelotServer = new TestServer(_webHostBuilder); + public void GivenOcelotIsRunningUsingConsulToStoreConfig() + { + _webHostBuilder = new WebHostBuilder(); - _ocelotClient = _ocelotServer.CreateClient(); - } + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => { s.AddOcelot().AddConsul().AddConfigStoredInConsul(); }) + .Configure(app => { app.UseOcelot().Wait(); }); + + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + Thread.Sleep(1000); + } - public void GivenOcelotIsRunningUsingConsulToStoreConfig() + public void WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk(string url) + { + var result = Wait.WaitFor(2000).Until(() => { - _webHostBuilder = new WebHostBuilder(); - - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot().AddConsul().AddConfigStoredInConsul(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + try + { + _response = _ocelotClient.GetAsync(url).Result; + _response.EnsureSuccessStatusCode(); + return true; + } + catch (Exception) + { + return false; + } + }); - _ocelotServer = new TestServer(_webHostBuilder); + result.ShouldBeTrue(); + } - _ocelotClient = _ocelotServer.CreateClient(); - Thread.Sleep(1000); - } + public void GivenOcelotIsRunningUsingJsonSerializedCache() + { + _webHostBuilder = new WebHostBuilder(); - public void WhenIGetUrlOnTheApiGatewayWaitingForTheResponseToBeOk(string url) - { - var result = Wait.WaitFor(2000).Until(() => + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => { - try - { - _response = _ocelotClient.GetAsync(url).Result; - _response.EnsureSuccessStatusCode(); - return true; - } - catch (Exception) - { - return false; - } - }); + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot() + .AddCacheManager((x) => + { + x.WithMicrosoftLogging(_ => + { + //log.AddConsole(LogLevel.Debug); + }) + .WithJsonSerializer() + .WithHandle(typeof(InMemoryJsonHandle<>)); + }); + }) + .Configure(app => { app.UseOcelot().Wait(); }); - result.ShouldBeTrue(); - } + _ocelotServer = new TestServer(_webHostBuilder); - public void GivenOcelotIsRunningUsingJsonSerializedCache() - { - _webHostBuilder = new WebHostBuilder(); + _ocelotClient = _ocelotServer.CreateClient(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddCacheManager((x) => - { - x.WithMicrosoftLogging(log => - { - //log.AddConsole(LogLevel.Debug); - }) - .WithJsonSerializer() - .WithHandle(typeof(InMemoryJsonHandle<>)); - }); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + public void GivenOcelotIsRunningWithFakeHttpClientCache(IHttpClientCache cache) + { + _webHostBuilder = new WebHostBuilder(); - _ocelotServer = new TestServer(_webHostBuilder); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(cache); + s.AddOcelot(); + }) + .Configure(app => { app.UseOcelot().Wait(); }); - _ocelotClient = _ocelotServer.CreateClient(); - } + _ocelotServer = new TestServer(_webHostBuilder); - public void GivenOcelotIsRunningWithFakeHttpClientCache(IHttpClientCache cache) - { - _webHostBuilder = new WebHostBuilder(); + _ocelotClient = _ocelotServer.CreateClient(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(cache); - s.AddOcelot(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + internal void GivenIWait(int wait) + { + Thread.Sleep(wait); + } - _ocelotServer = new TestServer(_webHostBuilder); + public void GivenOcelotIsRunningWithMiddlewareBeforePipeline(Func callback) + { + _webHostBuilder = new WebHostBuilder(); - _ocelotClient = _ocelotServer.CreateClient(); - } + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => { s.AddOcelot(); }) + .Configure(app => + { + app.UseMiddleware(callback); + app.UseOcelot().Wait(); + }); - internal void GivenIWait(int wait) - { - Thread.Sleep(wait); - } + _ocelotServer = new TestServer(_webHostBuilder); - public void GivenOcelotIsRunningWithMiddleareBeforePipeline(Func callback) - { - _webHostBuilder = new WebHostBuilder(); + _ocelotClient = _ocelotServer.CreateClient(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot(); - }) - .Configure(app => - { - app.UseMiddleware(callback); - app.UseOcelot().Wait(); - }); + public void GivenOcelotIsRunningWithSpecificHandlersRegisteredInDi() + where TOne : DelegatingHandler + where TWo : DelegatingHandler + { + _webHostBuilder = new WebHostBuilder(); - _ocelotServer = new TestServer(_webHostBuilder); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddOcelot() + .AddDelegatingHandler() + .AddDelegatingHandler(); + }) + .Configure(a => { a.UseOcelot().Wait(); }); - _ocelotClient = _ocelotServer.CreateClient(); - } + _ocelotServer = new TestServer(_webHostBuilder); - public void GivenOcelotIsRunningWithSpecficHandlersRegisteredInDi() - where TOne : DelegatingHandler - where TWo : DelegatingHandler - { - _webHostBuilder = new WebHostBuilder(); + _ocelotClient = _ocelotServer.CreateClient(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - s.AddOcelot() - .AddDelegatingHandler() - .AddDelegatingHandler(); - }) - .Configure(a => - { - a.UseOcelot().Wait(); - }); + public void GivenOcelotIsRunningWithSpecificAggregatorsRegisteredInDi() + where TAggregator : class, IDefinedAggregator + where TDependency : class + { + _webHostBuilder = new WebHostBuilder(); - _ocelotServer = new TestServer(_webHostBuilder); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddSingleton(); + s.AddOcelot() + .AddSingletonDefinedAggregator(); + }) + .Configure(a => { a.UseOcelot().Wait(); }); - _ocelotClient = _ocelotServer.CreateClient(); - } + _ocelotServer = new TestServer(_webHostBuilder); - public void GivenOcelotIsRunningWithSpecficAggregatorsRegisteredInDi() - where TAggregator : class, IDefinedAggregator - where TDepedency : class - { - _webHostBuilder = new WebHostBuilder(); + _ocelotClient = _ocelotServer.CreateClient(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - s.AddSingleton(); - s.AddOcelot() - .AddSingletonDefinedAggregator(); - }) - .Configure(a => - { - a.UseOcelot().Wait(); - }); + public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi() + where TOne : DelegatingHandler + where TWo : DelegatingHandler + { + _webHostBuilder = new WebHostBuilder(); - _ocelotServer = new TestServer(_webHostBuilder); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddOcelot() + .AddDelegatingHandler(true) + .AddDelegatingHandler(true); + }) + .Configure(a => { a.UseOcelot().Wait(); }); - _ocelotClient = _ocelotServer.CreateClient(); - } + _ocelotServer = new TestServer(_webHostBuilder); - public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi() - where TOne : DelegatingHandler - where TWo : DelegatingHandler - { - _webHostBuilder = new WebHostBuilder(); + _ocelotClient = _ocelotServer.CreateClient(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - s.AddOcelot() - .AddDelegatingHandler(true) - .AddDelegatingHandler(true); - }) - .Configure(a => - { - a.UseOcelot().Wait(); - }); + public void GivenOcelotIsRunningWithGlobalHandlerRegisteredInDi() + where TOne : DelegatingHandler + { + _webHostBuilder = new WebHostBuilder(); - _ocelotServer = new TestServer(_webHostBuilder); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddOcelot() + .AddDelegatingHandler(true); + }) + .Configure(a => { a.UseOcelot().Wait(); }); - _ocelotClient = _ocelotServer.CreateClient(); - } + _ocelotServer = new TestServer(_webHostBuilder); - public void GivenOcelotIsRunningWithGlobalHandlerRegisteredInDi() - where TOne : DelegatingHandler - { - _webHostBuilder = new WebHostBuilder(); + _ocelotClient = _ocelotServer.CreateClient(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - s.AddOcelot() - .AddDelegatingHandler(true); - }) - .Configure(a => - { - a.UseOcelot().Wait(); - }); + public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi(FakeDependency dependency) + where TOne : DelegatingHandler + { + _webHostBuilder = new WebHostBuilder(); - _ocelotServer = new TestServer(_webHostBuilder); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(_webHostBuilder); + s.AddSingleton(dependency); + s.AddOcelot() + .AddDelegatingHandler(true); + }) + .Configure(a => { a.UseOcelot().Wait(); }); - _ocelotClient = _ocelotServer.CreateClient(); - } + _ocelotServer = new TestServer(_webHostBuilder); - public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi(FakeDependency dependency) - where TOne : DelegatingHandler - { - _webHostBuilder = new WebHostBuilder(); + _ocelotClient = _ocelotServer.CreateClient(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - s.AddSingleton(dependency); - s.AddOcelot() - .AddDelegatingHandler(true); - }) - .Configure(a => - { - a.UseOcelot().Wait(); - }); + internal void GivenIAddCookieToMyRequest(string cookie) + { + _ocelotClient.DefaultRequestHeaders.Add("Set-Cookie", cookie); + } - _ocelotServer = new TestServer(_webHostBuilder); + /// + /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. + /// + public void GivenOcelotIsRunning(Action options, + string authenticationProviderKey) + { + _webHostBuilder = new WebHostBuilder(); - _ocelotClient = _ocelotServer.CreateClient(); - } + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot(); + s.AddAuthentication() + .AddIdentityServerAuthentication(authenticationProviderKey, options); + }) + .Configure(app => { app.UseOcelot().Wait(); }); - internal void GivenIAddCookieToMyRequest(string cookie) - { - _ocelotClient.DefaultRequestHeaders.Add("Set-Cookie", cookie); - } + _ocelotServer = new TestServer(_webHostBuilder); - /// - /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. - /// - public void GivenOcelotIsRunning(Action options, string authenticationProviderKey) - { - _webHostBuilder = new WebHostBuilder(); + _ocelotClient = _ocelotServer.CreateClient(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot(); - s.AddAuthentication() - .AddIdentityServerAuthentication(authenticationProviderKey, options); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + public void ThenTheResponseHeaderIs(string key, string value) + { + var header = _response.Headers.GetValues(key); + header.First().ShouldBe(value); + } - _ocelotServer = new TestServer(_webHostBuilder); + public void ThenTheReasonPhraseIs(string expected) + { + _response.ReasonPhrase.ShouldBe(expected); + } - _ocelotClient = _ocelotServer.CreateClient(); - } + /// + /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. + /// + public void GivenOcelotIsRunning(OcelotPipelineConfiguration ocelotPipelineConfig) + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", true, false) + .AddJsonFile(_ocelotConfigFileName, false, false) + .AddEnvironmentVariables(); + + var configuration = builder.Build(); + _webHostBuilder = new WebHostBuilder(); + _webHostBuilder.ConfigureServices(s => { s.AddSingleton(_webHostBuilder); }); + + _ocelotServer = new TestServer(_webHostBuilder + .UseConfiguration(configuration) + .ConfigureServices(s => { s.AddOcelot(configuration); }) + .ConfigureLogging(l => + { + l.AddConsole(); + l.AddDebug(); + }) + .Configure(a => { a.UseOcelot(ocelotPipelineConfig).Wait(); })); - public void ThenTheResponseHeaderIs(string key, string value) - { - var header = _response.Headers.GetValues(key); - header.First().ShouldBe(value); - } + _ocelotClient = _ocelotServer.CreateClient(); + } - public void ThenTheReasonPhraseIs(string expected) - { - _response.ReasonPhrase.ShouldBe(expected); - } + public void GivenIHaveAddedATokenToMyRequest() + { + _ocelotClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); + } - /// - /// This is annoying cos it should be in the constructor but we need to set up the file before calling startup so its a step. - /// - public void GivenOcelotIsRunning(OcelotPipelineConfiguration ocelotPipelineConfig) - { - var builder = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile(_ocelotConfigFileName, false, false) - .AddEnvironmentVariables(); - - var configuration = builder.Build(); - _webHostBuilder = new WebHostBuilder(); - _webHostBuilder.ConfigureServices(s => - { - s.AddSingleton(_webHostBuilder); - }); + public void GivenIHaveAToken(string url) + { + var tokenUrl = $"{url}/connect/token"; + var formData = new List> + { + new("client_id", "client"), + new("client_secret", "secret"), + new("scope", "api"), + new("username", "test"), + new("password", "test"), + new("grant_type", "password"), + }; + var content = new FormUrlEncodedContent(formData); + + using var httpClient = new HttpClient(); + var response = httpClient.PostAsync(tokenUrl, content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + } - _ocelotServer = new TestServer(_webHostBuilder - .UseConfiguration(configuration) - .ConfigureServices(s => - { - s.AddOcelot(configuration); - }) - .ConfigureLogging(l => - { - l.AddConsole(); - l.AddDebug(); - }) - .Configure(a => - { - a.UseOcelot(ocelotPipelineConfig).Wait(); - })); + public void GivenIHaveATokenForApiReadOnlyScope(string url) + { + var tokenUrl = $"{url}/connect/token"; + var formData = new List> + { + new("client_id", "client"), + new("client_secret", "secret"), + new("scope", "api.readOnly"), + new("username", "test"), + new("password", "test"), + new("grant_type", "password"), + }; + var content = new FormUrlEncodedContent(formData); + + using var httpClient = new HttpClient(); + var response = httpClient.PostAsync(tokenUrl, content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + } - _ocelotClient = _ocelotServer.CreateClient(); - } + public void GivenIHaveATokenForApi2(string url) + { + var tokenUrl = $"{url}/connect/token"; + var formData = new List> + { + new("client_id", "client"), + new("client_secret", "secret"), + new("scope", "api2"), + new("username", "test"), + new("password", "test"), + new("grant_type", "password"), + }; + var content = new FormUrlEncodedContent(formData); + + using var httpClient = new HttpClient(); + var response = httpClient.PostAsync(tokenUrl, content).Result; + var responseContent = response.Content.ReadAsStringAsync().Result; + response.EnsureSuccessStatusCode(); + _token = JsonConvert.DeserializeObject(responseContent); + } - public void GivenIHaveAddedATokenToMyRequest() - { - _ocelotClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _token.AccessToken); - } + public static void VerifyIdentityServerStarted(string url) + { + using var httpClient = new HttpClient(); + var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").GetAwaiter().GetResult(); + response.Content.ReadAsStringAsync().GetAwaiter(); + response.EnsureSuccessStatusCode(); + } - public void GivenIHaveAToken(string url) - { - var tokenUrl = $"{url}/connect/token"; - var formData = new List> + public void GivenOcelotIsRunningWithMinimumLogLevel(Logger logger, string appsettingsFileName) + { + _webHostBuilder = new WebHostBuilder() + .UseKestrel() + .ConfigureAppConfiguration((_, config) => { - new("client_id", "client"), - new("client_secret", "secret"), - new("scope", "api"), - new("username", "test"), - new("password", "test"), - new("grant_type", "password"), - }; - var content = new FormUrlEncodedContent(formData); - - using (var httpClient = new HttpClient()) + config.AddJsonFile(appsettingsFileName, false, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => { s.AddOcelot(); }) + .ConfigureLogging(logging => { - var response = httpClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - } - } + logging.ClearProviders(); + logging.AddSerilog(logger); + }) + .Configure(app => + { + app.Use(async (context, next) => + { + var loggerFactory = context.RequestServices.GetService(); + var ocelotLogger = loggerFactory.CreateLogger(); + ocelotLogger.LogDebug(() => $"DEBUG: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogTrace(() => $"TRACE: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogInformation(() => + $"INFORMATION: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogWarning(() => $"WARNING: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogError(() => $"ERROR: {nameof(ocelotLogger)}, {nameof(loggerFactory)}", + new Exception("test")); + ocelotLogger.LogCritical(() => $"CRITICAL: {nameof(ocelotLogger)}, {nameof(loggerFactory)}", + new Exception("test")); + + await next.Invoke(); + }); + app.UseOcelot().Wait(); + }); - public void GivenIHaveATokenForApiReadOnlyScope(string url) - { - var tokenUrl = $"{url}/connect/token"; - var formData = new List> + _ocelotServer = new TestServer(_webHostBuilder); + _ocelotClient = _ocelotServer.CreateClient(); + } + + public void GivenOcelotIsRunningWithEureka() + { + _webHostBuilder = new WebHostBuilder(); + + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => { - new("client_id", "client"), - new("client_secret", "secret"), - new("scope", "api.readOnly"), - new("username", "test"), - new("password", "test"), - new("grant_type", "password"), - }; - var content = new FormUrlEncodedContent(formData); - - using (var httpClient = new HttpClient()) + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => { - var response = httpClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - } - } + s.AddOcelot() + .AddEureka(); + }) + .Configure(app => { app.UseOcelot().Wait(); }); - public void GivenIHaveATokenForApi2(string url) - { - var tokenUrl = $"{url}/connect/token"; - var formData = new List> + _ocelotServer = new TestServer(_webHostBuilder); + + _ocelotClient = _ocelotServer.CreateClient(); + } + + public void GivenOcelotIsRunningWithPolly() + { + _webHostBuilder = new WebHostBuilder(); + + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => { - new("client_id", "client"), - new("client_secret", "secret"), - new("scope", "api2"), - new("username", "test"), - new("password", "test"), - new("grant_type", "password"), - }; - var content = new FormUrlEncodedContent(formData); - - using (var httpClient = new HttpClient()) + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => { - var response = httpClient.PostAsync(tokenUrl, content).Result; - var responseContent = response.Content.ReadAsStringAsync().Result; - response.EnsureSuccessStatusCode(); - _token = JsonConvert.DeserializeObject(responseContent); - } - } - - public void VerifyIdentiryServerStarted(string url) - { - using (var httpClient = new HttpClient()) + s.AddOcelot() + .AddPolly(); + }) + .Configure(app => { - var response = httpClient.GetAsync($"{url}/.well-known/openid-configuration").GetAwaiter().GetResult(); - var content = response.Content.ReadAsStringAsync().GetAwaiter(); - response.EnsureSuccessStatusCode(); - } - } + app.UseOcelot() + .Wait(); + }); - public void GivenOcelotIsRunningWithEureka() - { - _webHostBuilder = new WebHostBuilder(); + _ocelotServer = new TestServer(_webHostBuilder); - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddEureka(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + _ocelotClient = _ocelotServer.CreateClient(); + } - _ocelotServer = new TestServer(_webHostBuilder); + public void WhenIGetUrlOnTheApiGateway(string url) + { + _response = _ocelotClient.GetAsync(url).Result; + } - _ocelotClient = _ocelotServer.CreateClient(); - } + public void WhenIGetUrlOnTheApiGatewayAndDontWait(string url) + { + _ocelotClient.GetAsync(url); + } - public void GivenOcelotIsRunningWithPolly() - { - _webHostBuilder = new WebHostBuilder(); + public void WhenICancelTheRequest() + { + _ocelotClient.CancelPendingRequests(); + } - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddPolly(); - }) - .Configure(app => - { - app.UseOcelot() - .Wait(); - }); + public void WhenIGetUrlOnTheApiGateway(string url, HttpContent content) + { + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url) { Content = content }; + _response = _ocelotClient.SendAsync(httpRequestMessage).Result; + } - _ocelotServer = new TestServer(_webHostBuilder); + public void WhenIPostUrlOnTheApiGateway(string url, HttpContent content) + { + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, url) { Content = content }; + _response = _ocelotClient.SendAsync(httpRequestMessage).Result; + } - _ocelotClient = _ocelotServer.CreateClient(); - } + public void WhenIGetUrlOnTheApiGateway(string url, string cookie, string value) + { + var request = _ocelotServer.CreateRequest(url); + request.And(x => { x.Headers.Add("Cookie", new CookieHeaderValue(cookie, value).ToString()); }); + var response = request.GetAsync().Result; + _response = response; + } - public void WhenIGetUrlOnTheApiGateway(string url) - { - _response = _ocelotClient.GetAsync(url).Result; - } + public void GivenIAddAHeader(string key, string value) + { + _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(key, value); + } - public void WhenIGetUrlOnTheApiGatewayAndDontWait(string url) - { - _ocelotClient.GetAsync(url); - } + public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times) + { + var tasks = new Task[times]; - public void WhenICancelTheRequest() + for (var i = 0; i < times; i++) { - _ocelotClient.CancelPendingRequests(); + var urlCopy = url; + tasks[i] = GetForServiceDiscoveryTest(urlCopy); + Thread.Sleep(_random.Next(40, 60)); } - public void WhenIGetUrlOnTheApiGateway(string url, HttpContent content) - { - var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, url) { Content = content }; - _response = _ocelotClient.SendAsync(httpRequestMessage).Result; - } + Task.WaitAll(tasks); + } - public void WhenIPostUrlOnTheApiGateway(string url, HttpContent content) - { - var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, url) { Content = content }; - _response = _ocelotClient.SendAsync(httpRequestMessage).Result; - } + public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times, string cookie, string value) + { + var tasks = new Task[times]; - public void WhenIGetUrlOnTheApiGateway(string url, string cookie, string value) + for (var i = 0; i < times; i++) { - var request = _ocelotServer.CreateRequest(url); - request.And(x => { x.Headers.Add("Cookie", new CookieHeaderValue(cookie, value).ToString()); }); - var response = request.GetAsync().Result; - _response = response; + tasks[i] = GetForServiceDiscoveryTest(url, cookie, value); + Thread.Sleep(_random.Next(40, 60)); } - public void GivenIAddAHeader(string key, string value) - { - _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(key, value); - } + Task.WaitAll(tasks); + } - public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times) - { - var tasks = new Task[times]; + private async Task GetForServiceDiscoveryTest(string url, string cookie, string value) + { + var request = _ocelotServer.CreateRequest(url); + request.And(x => { x.Headers.Add("Cookie", new CookieHeaderValue(cookie, value).ToString()); }); + var response = await request.GetAsync(); + var content = await response.Content.ReadAsStringAsync(); + var count = int.Parse(content); + count.ShouldBeGreaterThan(0); + } - for (var i = 0; i < times; i++) - { - var urlCopy = url; - tasks[i] = GetForServiceDiscoveryTest(urlCopy); - Thread.Sleep(_random.Next(40, 60)); - } + private async Task GetForServiceDiscoveryTest(string url) + { + var response = await _ocelotClient.GetAsync(url); + var content = await response.Content.ReadAsStringAsync(); + var count = int.Parse(content); + count.ShouldBeGreaterThan(0); + } - Task.WaitAll(tasks); + public void WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(string url, int times) + { + for (var i = 0; i < times; i++) + { + const string clientId = "ocelotclient1"; + var request = new HttpRequestMessage(new HttpMethod("GET"), url); + request.Headers.Add("ClientId", clientId); + _response = _ocelotClient.SendAsync(request).Result; } + } - public void WhenIGetUrlOnTheApiGatewayMultipleTimes(string url, int times, string cookie, string value) - { - var tasks = new Task[times]; + public void WhenIGetUrlOnTheApiGateway(string url, string requestId) + { + _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId); - for (var i = 0; i < times; i++) - { - var urlCopy = url; - tasks[i] = GetForServiceDiscoveryTest(urlCopy, cookie, value); - Thread.Sleep(_random.Next(40, 60)); - } + _response = _ocelotClient.GetAsync(url).Result; + } - Task.WaitAll(tasks); - } + public void WhenIPostUrlOnTheApiGateway(string url) + { + _response = _ocelotClient.PostAsync(url, _postContent).Result; + } - private async Task GetForServiceDiscoveryTest(string url, string cookie, string value) - { - var request = _ocelotServer.CreateRequest(url); - request.And(x => { x.Headers.Add("Cookie", new CookieHeaderValue(cookie, value).ToString()); }); - var response = await request.GetAsync(); - var content = await response.Content.ReadAsStringAsync(); - var count = int.Parse(content); - count.ShouldBeGreaterThan(0); - } + public void GivenThePostHasContent(string postContent) + { + _postContent = new StringContent(postContent); + } - private async Task GetForServiceDiscoveryTest(string url) - { - var response = await _ocelotClient.GetAsync(url); - var content = await response.Content.ReadAsStringAsync(); - var count = int.Parse(content); - count.ShouldBeGreaterThan(0); - } + public void GivenThePostHasContentType(string postContent) + { + _postContent.Headers.ContentType = new MediaTypeHeaderValue(postContent); + } - public void WhenIGetUrlOnTheApiGatewayMultipleTimesForRateLimit(string url, int times) + public void GivenThePostHasGzipContent(object input) + { + var json = JsonConvert.SerializeObject(input); + var jsonBytes = Encoding.UTF8.GetBytes(json); + var ms = new MemoryStream(); + using (var gzip = new GZipStream(ms, CompressionMode.Compress, true)) { - for (var i = 0; i < times; i++) - { - var clientId = "ocelotclient1"; - var request = new HttpRequestMessage(new HttpMethod("GET"), url); - request.Headers.Add("ClientId", clientId); - _response = _ocelotClient.SendAsync(request).Result; - } + gzip.Write(jsonBytes, 0, jsonBytes.Length); } - public void WhenIGetUrlOnTheApiGateway(string url, string requestId) - { - _ocelotClient.DefaultRequestHeaders.TryAddWithoutValidation(RequestIdKey, requestId); + ms.Position = 0; + var content = new StreamContent(ms); + content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + content.Headers.ContentEncoding.Add("gzip"); + _postContent = content; + } - _response = _ocelotClient.GetAsync(url).Result; - } + public void ThenTheResponseBodyShouldBe(string expectedBody) + { + _response.Content.ReadAsStringAsync().Result.ShouldBe(expectedBody); + } - public void WhenIPostUrlOnTheApiGateway(string url) - { - _response = _ocelotClient.PostAsync(url, _postContent).Result; - } + public void ThenTheContentLengthIs(int expected) + { + _response.Content.Headers.ContentLength.ShouldBe(expected); + } - public void GivenThePostHasContent(string postcontent) - { - _postContent = new StringContent(postcontent); - } + public void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) + { + _response.StatusCode.ShouldBe(expectedHttpStatusCode); + } - public void GivenThePostHasContentType(string postcontent) - { - _postContent.Headers.ContentType = new MediaTypeHeaderValue(postcontent); - } + public void ThenTheStatusCodeShouldBe(int expectedHttpStatusCode) + { + var responseStatusCode = (int)_response.StatusCode; + responseStatusCode.ShouldBe(expectedHttpStatusCode); + } - public void GivenThePostHasGzipContent(object input) - { - var json = JsonConvert.SerializeObject(input); - var jsonBytes = Encoding.UTF8.GetBytes(json); - var ms = new MemoryStream(); - using (var gzip = new GZipStream(ms, CompressionMode.Compress, true)) - { - gzip.Write(jsonBytes, 0, jsonBytes.Length); - } + public void ThenTheRequestIdIsReturned() + { + _response.Headers.GetValues(RequestIdKey).First().ShouldNotBeNullOrEmpty(); + } - ms.Position = 0; - var content = new StreamContent(ms); - content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); - content.Headers.ContentEncoding.Add("gzip"); - _postContent = content; - } + public void ThenTheRequestIdIsReturned(string expected) + { + _response.Headers.GetValues(RequestIdKey).First().ShouldBe(expected); + } - public void ThenTheResponseBodyShouldBe(string expectedBody) - { - _response.Content.ReadAsStringAsync().Result.ShouldBe(expectedBody); - } + public void WhenIMakeLotsOfDifferentRequestsToTheApiGateway() + { + var numberOfRequests = 100; + var aggregateUrl = "/"; + var aggregateExpected = "{\"Laura\":{Hello from Laura},\"Tom\":{Hello from Tom}}"; + var tomUrl = "/tom"; + var tomExpected = "{Hello from Tom}"; + var lauraUrl = "/laura"; + var lauraExpected = "{Hello from Laura}"; + var random = new Random(); - public void ThenTheContentLengthIs(int expected) - { - _response.Content.Headers.ContentLength.ShouldBe(expected); - } + var aggregateTasks = new Task[numberOfRequests]; - public void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) + for (var i = 0; i < numberOfRequests; i++) { - _response.StatusCode.ShouldBe(expectedHttpStatusCode); + aggregateTasks[i] = Fire(aggregateUrl, aggregateExpected, random); } - public void ThenTheStatusCodeShouldBe(int expectedHttpStatusCode) - { - var responseStatusCode = (int)_response.StatusCode; - responseStatusCode.ShouldBe(expectedHttpStatusCode); - } + var tomTasks = new Task[numberOfRequests]; - public void ThenTheRequestIdIsReturned() + for (var i = 0; i < numberOfRequests; i++) { - _response.Headers.GetValues(RequestIdKey).First().ShouldNotBeNullOrEmpty(); + tomTasks[i] = Fire(tomUrl, tomExpected, random); } - public void ThenTheRequestIdIsReturned(string expected) - { - _response.Headers.GetValues(RequestIdKey).First().ShouldBe(expected); - } + var lauraTasks = new Task[numberOfRequests]; - public void WhenIMakeLotsOfDifferentRequestsToTheApiGateway() + for (var i = 0; i < numberOfRequests; i++) { - var numberOfRequests = 100; - var aggregateUrl = "/"; - var aggregateExpected = "{\"Laura\":{Hello from Laura},\"Tom\":{Hello from Tom}}"; - var tomUrl = "/tom"; - var tomExpected = "{Hello from Tom}"; - var lauraUrl = "/laura"; - var lauraExpected = "{Hello from Laura}"; - var random = new Random(); - - var aggregateTasks = new Task[numberOfRequests]; - - for (var i = 0; i < numberOfRequests; i++) - { - aggregateTasks[i] = Fire(aggregateUrl, aggregateExpected, random); - } + lauraTasks[i] = Fire(lauraUrl, lauraExpected, random); + } - var tomTasks = new Task[numberOfRequests]; + Task.WaitAll(lauraTasks); + Task.WaitAll(tomTasks); + Task.WaitAll(aggregateTasks); + } - for (var i = 0; i < numberOfRequests; i++) - { - tomTasks[i] = Fire(tomUrl, tomExpected, random); - } + private async Task Fire(string url, string expectedBody, Random random) + { + var request = new HttpRequestMessage(new HttpMethod("GET"), url); + await Task.Delay(random.Next(0, 2)); + var response = await _ocelotClient.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + content.ShouldBe(expectedBody); + } - var lauraTasks = new Task[numberOfRequests]; + public void GivenOcelotIsRunningWithBlowingUpDiskRepo(IFileConfigurationRepository fake) + { + _webHostBuilder = new WebHostBuilder(); - for (var i = 0; i < numberOfRequests; i++) + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => { - lauraTasks[i] = Fire(lauraUrl, lauraExpected, random); - } - - Task.WaitAll(lauraTasks); - Task.WaitAll(tomTasks); - Task.WaitAll(aggregateTasks); - } - - private async Task Fire(string url, string expectedBody, Random random) - { - var request = new HttpRequestMessage(new HttpMethod("GET"), url); - await Task.Delay(random.Next(0, 2)); - var response = await _ocelotClient.SendAsync(request); - var content = await response.Content.ReadAsStringAsync(); - content.ShouldBe(expectedBody); - } - - public void GivenOcelotIsRunningWithBlowingUpDiskRepo(IFileConfigurationRepository fake) - { - _webHostBuilder = new WebHostBuilder(); + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddSingleton(fake); + s.AddOcelot(); + }) + .Configure(app => { app.UseOcelot().Wait(); }); - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddSingleton(fake); - s.AddOcelot(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + _ocelotServer = new TestServer(_webHostBuilder); - _ocelotServer = new TestServer(_webHostBuilder); + _ocelotClient = _ocelotServer.CreateClient(); + } - _ocelotClient = _ocelotServer.CreateClient(); - } + public void TheChangeTokenShouldBeActive(bool itShouldBeActive) + { + _changeToken.ChangeToken.HasChanged.ShouldBe(itShouldBeActive); + } - public void TheChangeTokenShouldBeActive(bool itShouldBeActive) - { - _changeToken.ChangeToken.HasChanged.ShouldBe(itShouldBeActive); - } + public void GivenOcelotIsRunningWithLogger() + { + _webHostBuilder = new WebHostBuilder(); - public void GivenOcelotIsRunningWithLogger() - { - _webHostBuilder = new WebHostBuilder(); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, false, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot(); + s.AddSingleton(); + }) + .Configure(app => { app.UseOcelot().Wait(); }); - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, false, false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot(); - s.AddSingleton(); - }) - .Configure(app => - { - app.UseOcelot().Wait(); - }); + _ocelotServer = new TestServer(_webHostBuilder); - _ocelotServer = new TestServer(_webHostBuilder); + _ocelotClient = _ocelotServer.CreateClient(); + } - _ocelotClient = _ocelotServer.CreateClient(); - } + internal void GivenOcelotIsRunningUsingOpenTracing(OpenTracing.ITracer fakeTracer) + { + _webHostBuilder = new WebHostBuilder(); - internal void GivenOcelotIsRunningUsingOpenTracing(OpenTracing.ITracer fakeTracer) - { - _webHostBuilder = new WebHostBuilder(); + _webHostBuilder + .ConfigureAppConfiguration((hostingContext, config) => + { + config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); + var env = hostingContext.HostingEnvironment; + config.AddJsonFile("appsettings.json", true, false) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, false); + config.AddJsonFile(_ocelotConfigFileName, true, false); + config.AddEnvironmentVariables(); + }) + .ConfigureServices(s => + { + s.AddOcelot() + .AddOpenTracing(); - _webHostBuilder - .ConfigureAppConfiguration((hostingContext, config) => - { - config.SetBasePath(hostingContext.HostingEnvironment.ContentRootPath); - var env = hostingContext.HostingEnvironment; - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: false) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: false); - config.AddJsonFile(_ocelotConfigFileName, optional: true, reloadOnChange: false); - config.AddEnvironmentVariables(); - }) - .ConfigureServices(s => - { - s.AddOcelot() - .AddOpenTracing(); + s.AddSingleton(fakeTracer); + }) + .Configure(app => + { + app.Use(async (_, next) => { await next.Invoke(); }); + app.UseOcelot().Wait(); + }); - s.AddSingleton(fakeTracer); - }) - .Configure(app => - { - app.Use(async (_, next) => - { - await next.Invoke(); - }); - app.UseOcelot().Wait(); - }); + _ocelotServer = new TestServer(_webHostBuilder); - _ocelotServer = new TestServer(_webHostBuilder); + _ocelotClient = _ocelotServer.CreateClient(); + } - _ocelotClient = _ocelotServer.CreateClient(); - } + public void ThenWarningShouldBeLogged(int howMany) + { + var loggerFactory = (MockLoggerFactory)_ocelotServer.Host.Services.GetService(); + loggerFactory.Verify(Times.Exactly(howMany)); + } - public void ThenWarningShouldBeLogged(int howMany) - { - var loggerFactory = (MockLoggerFactory)_ocelotServer.Host.Services.GetService(); - loggerFactory.Verify(Times.Exactly(howMany)); - } + internal class MockLoggerFactory : IOcelotLoggerFactory + { + private Mock _logger; - internal class MockLoggerFactory : IOcelotLoggerFactory + public IOcelotLogger CreateLogger() { - private Mock _logger; - - public IOcelotLogger CreateLogger() + if (_logger != null) { - if (_logger == null) - { - _logger = new Mock(); - _logger.Setup(x => x.LogWarning(It.IsAny())).Verifiable(); - } - return _logger.Object; } - public void Verify(Times howMany) - { - _logger.Verify(x => x.LogWarning(It.IsAny()), howMany); - } + _logger = new Mock(); + _logger.Setup(x => x.LogWarning(It.IsAny())).Verifiable(); + _logger.Setup(x => x.LogWarning(It.IsAny>())).Verifiable(); + + return _logger.Object; } - /// - /// Public implementation of Dispose pattern callable by consumers. - /// - public void Dispose() + public void Verify(Times howMany) { - Dispose(true); - GC.SuppressFinalize(this); + _logger.Verify(x => x.LogWarning(It.IsAny>()), howMany); } + } - private bool _disposedValue; + /// + /// Public implementation of Dispose pattern callable by consumers. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - /// - /// Protected implementation of Dispose pattern. - /// - /// Flag to trigger actual disposing operation. - protected virtual void Dispose(bool disposing) - { - if (_disposedValue) - { - return; - } + private bool _disposedValue; - if (disposing) - { - _ocelotClient?.Dispose(); - _ocelotServer?.Dispose(); - _ocelotHost?.Dispose(); - DeleteOcelotConfig(); - } + /// + /// Protected implementation of Dispose pattern. + /// + /// Flag to trigger actual disposing operation. + protected virtual void Dispose(bool disposing) + { + if (_disposedValue) + { + return; + } - _disposedValue = true; + if (disposing) + { + _ocelotClient?.Dispose(); + _ocelotServer?.Dispose(); + _ocelotHost?.Dispose(); + DeleteOcelotConfig(); } + + _disposedValue = true; } } diff --git a/test/Ocelot.Benchmarks/MsLoggerBenchmarks.cs b/test/Ocelot.Benchmarks/MsLoggerBenchmarks.cs new file mode 100644 index 000000000..c54693300 --- /dev/null +++ b/test/Ocelot.Benchmarks/MsLoggerBenchmarks.cs @@ -0,0 +1,195 @@ +using BenchmarkDotNet.Order; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Ocelot.Logging; +using Ocelot.Middleware; + +namespace Ocelot.Benchmarks; + +[Config(typeof(MsLoggerBenchmarks))] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +[MaxIterationCount(16)] +public class MsLoggerBenchmarks : ManualConfig +{ + private IWebHost _service; + private IWebHost _webHost; + private HttpClient _httpClient; + + public MsLoggerBenchmarks() + { + AddColumn(StatisticColumn.AllStatistics); + AddDiagnoser(MemoryDiagnoser.Default); + AddValidator(BaselineValidator.FailOnError); + } + + private async Task SendRequest() + { + _httpClient ??= new HttpClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:5000"); + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + } + + [Benchmark(Baseline = true)] + public async Task LogLevelCritical() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelCritical))] + public void SetUpCritical() => OcelotFactory(LogLevel.Critical); + + [Benchmark] + public async Task LogLevelError() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelError))] + public void SetupError() => OcelotFactory(LogLevel.Error); + + [Benchmark] + public async Task LogLevelWarning() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelWarning))] + public void SetUpWarning() => OcelotFactory(LogLevel.Warning); + + [Benchmark] + public async Task LogLevelInformation() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelInformation))] + public void SetUpInformation() => OcelotFactory(LogLevel.Information); + + [Benchmark] + public async Task LogLevelTrace() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelTrace))] + public void SetUpTrace() => OcelotFactory(LogLevel.Trace); + + [GlobalCleanup(Targets = new[] + { + nameof(LogLevelCritical), nameof(LogLevelError), nameof(LogLevelWarning), nameof(LogLevelInformation), + nameof(LogLevelTrace), + })] + public void OcelotCleanup() + { + _webHost?.Dispose(); + _service?.Dispose(); + } + + [GlobalCleanup] + public void Cleanup() + { + _httpClient?.Dispose(); + } + + private void GivenOcelotIsRunning(string url, LogLevel minLogLevel) + { + _webHost = new WebHostBuilder() + .UseKestrel() + .UseUrls(url) + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("ocelot.json", false, false) + .AddEnvironmentVariables(); + }) + .ConfigureServices(s => { s.AddOcelot(); }) + .ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.SetMinimumLevel(minLogLevel); + logging.AddConsole(); + }) + .Configure(app => + { + app.Use(async (context, next) => + { + var loggerFactory = context.RequestServices.GetService(); + var ocelotLogger = loggerFactory.CreateLogger(); + ocelotLogger.LogDebug(() => $"DEBUG: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogTrace(() => $"TRACE: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogInformation(() => $"INFORMATION: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogWarning(() => $"WARNING: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogError(() => $"ERROR: {nameof(ocelotLogger)}, {nameof(loggerFactory)}", + new Exception("test")); + ocelotLogger.LogCritical(() => $"CRITICAL: {nameof(ocelotLogger)}, {nameof(loggerFactory)}", + new Exception("test")); + + await next.Invoke(); + }); + app.UseOcelot().Wait(); + }) + .Build(); + + _webHost.Start(); + } + + private void OcelotFactory(LogLevel minLogLevel) + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51879, + }, + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + GivenThereIsAConfiguration(configuration); + GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 201, string.Empty); + GivenOcelotIsRunning("http://localhost:5000", minLogLevel); + } + + public static void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + { + var configurationPath = Path.Combine(AppContext.BaseDirectory, "ocelot.json"); + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + { + _service = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _service.Start(); + } +} diff --git a/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj index ffbd2d8a4..e2cb80062 100644 --- a/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj +++ b/test/Ocelot.Benchmarks/Ocelot.Benchmarks.csproj @@ -26,6 +26,18 @@ all + + + + + + + + + + + + diff --git a/test/Ocelot.Benchmarks/Program.cs b/test/Ocelot.Benchmarks/Program.cs index a42519606..4d750e0a5 100644 --- a/test/Ocelot.Benchmarks/Program.cs +++ b/test/Ocelot.Benchmarks/Program.cs @@ -12,6 +12,8 @@ public static void Main(string[] args) typeof(AllTheThingsBenchmarks), typeof(ExceptionHandlerMiddlewareBenchmarks), typeof(DownstreamRouteFinderMiddlewareBenchmarks), + typeof(SerilogBenchmarks), + typeof(MsLoggerBenchmarks), }); switcher.Run(args); diff --git a/test/Ocelot.Benchmarks/SerilogBenchmarks.cs b/test/Ocelot.Benchmarks/SerilogBenchmarks.cs new file mode 100644 index 000000000..f3af7e814 --- /dev/null +++ b/test/Ocelot.Benchmarks/SerilogBenchmarks.cs @@ -0,0 +1,226 @@ +using BenchmarkDotNet.Order; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Ocelot.Configuration.File; +using Ocelot.DependencyInjection; +using Ocelot.Logging; +using Ocelot.Middleware; +using Serilog; +using Serilog.Core; + +namespace Ocelot.Benchmarks; + +[Config(typeof(SerilogBenchmarks))] +[Orderer(SummaryOrderPolicy.FastestToSlowest)] +public class SerilogBenchmarks : ManualConfig +{ + private IWebHost _service; + private Logger _logger; + private IWebHost _webHost; + private HttpClient _httpClient; + + public SerilogBenchmarks() + { + AddColumn(StatisticColumn.AllStatistics); + AddDiagnoser(MemoryDiagnoser.Default); + AddValidator(BaselineValidator.FailOnError); + } + + private async Task SendRequest() + { + _httpClient ??= new HttpClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:5000"); + var response = await _httpClient.SendAsync(request); + response.EnsureSuccessStatusCode(); + } + + [Benchmark(Baseline = true)] + public async Task LogLevelCritical() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelCritical))] + public void SetUpCritical() => OcelotFactory(LogLevel.Critical); + + [Benchmark] + public async Task LogLevelError() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelError))] + public void SetupError() => OcelotFactory(LogLevel.Error); + + [Benchmark] + public async Task LogLevelWarning() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelWarning))] + public void SetUpWarning() => OcelotFactory(LogLevel.Warning); + + [Benchmark] + public async Task LogLevelInformation() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelInformation))] + public void SetUpInformation() => OcelotFactory(LogLevel.Information); + + [Benchmark] + public async Task LogLevelTrace() => await SendRequest(); + + [GlobalSetup(Target = nameof(LogLevelTrace))] + public void SetUpTrace() => OcelotFactory(LogLevel.Trace); + + [GlobalCleanup(Targets = new[] + { + nameof(LogLevelCritical), nameof(LogLevelError), nameof(LogLevelWarning), nameof(LogLevelInformation), + nameof(LogLevelTrace), + })] + public void OcelotCleanup() + { + _webHost?.Dispose(); + _service?.Dispose(); + } + + [GlobalCleanup] + public void Cleanup() + { + _httpClient?.Dispose(); + } + + private void GivenOcelotIsRunning(string url, LogLevel minLogLevel) + { + _logger = minLogLevel switch + { + LogLevel.Information => new LoggerConfiguration().MinimumLevel.Information() + .WriteTo.File( + $"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log") + .CreateLogger(), + LogLevel.Warning => new LoggerConfiguration().MinimumLevel.Warning() + .WriteTo.File( + $"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log") + .CreateLogger(), + LogLevel.Error => new LoggerConfiguration().MinimumLevel.Error() + .WriteTo.File( + $"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log") + .CreateLogger(), + LogLevel.Critical => new LoggerConfiguration().MinimumLevel.Fatal() + .WriteTo.File( + $"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log") + .CreateLogger(), + LogLevel.Trace => new LoggerConfiguration().MinimumLevel.Verbose() + .WriteTo.File( + $"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log") + .CreateLogger(), + LogLevel.None => new LoggerConfiguration() + .WriteTo.File( + $"{AppContext.BaseDirectory}/Logs/log_level_test_{minLogLevel}.log") + .CreateLogger(), + _ => throw new ArgumentOutOfRangeException(nameof(minLogLevel), minLogLevel, null), + }; + + _webHost = new WebHostBuilder() + .UseKestrel() + .UseUrls(url) + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureAppConfiguration((hostingContext, config) => + { + config + .SetBasePath(hostingContext.HostingEnvironment.ContentRootPath) + .AddJsonFile("ocelot.json", false, false) + .AddEnvironmentVariables(); + }) + .ConfigureServices(s => { s.AddOcelot(); }) + .ConfigureLogging(logging => + { + logging.ClearProviders(); + logging.SetMinimumLevel(minLogLevel); + logging.AddSerilog(_logger); + }) + .Configure(app => + { + app.Use(async (context, next) => + { + var loggerFactory = context.RequestServices.GetService(); + var ocelotLogger = loggerFactory.CreateLogger(); + ocelotLogger.LogDebug(() => $"DEBUG: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogTrace(() => $"TRACE: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogInformation(() => $"INFORMATION: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogWarning(() => $"WARNING: {nameof(ocelotLogger)}, {nameof(loggerFactory)}"); + ocelotLogger.LogError(() => $"ERROR: {nameof(ocelotLogger)}, {nameof(loggerFactory)}", + new Exception("test")); + ocelotLogger.LogCritical(() => $"CRITICAL: {nameof(ocelotLogger)}, {nameof(loggerFactory)}", + new Exception("test")); + + await next.Invoke(); + }); + app.UseOcelot().Wait(); + }) + .Build(); + + _webHost.Start(); + } + + private void OcelotFactory(LogLevel minLogLevel) + { + var configuration = new FileConfiguration + { + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List + { + new() + { + Host = "localhost", + Port = 51879, + }, + }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + }, + }, + }; + + GivenThereIsAConfiguration(configuration); + GivenThereIsAServiceRunningOn("http://localhost:51879", "/", 201, string.Empty); + GivenOcelotIsRunning("http://localhost:5000", minLogLevel); + } + + public static void GivenThereIsAConfiguration(FileConfiguration fileConfiguration) + { + var configurationPath = Path.Combine(AppContext.BaseDirectory, "ocelot.json"); + + var jsonConfiguration = JsonConvert.SerializeObject(fileConfiguration); + + if (File.Exists(configurationPath)) + { + File.Delete(configurationPath); + } + + File.WriteAllText(configurationPath, jsonConfiguration); + } + + private void GivenThereIsAServiceRunningOn(string baseUrl, string basePath, int statusCode, string responseBody) + { + _service = new WebHostBuilder() + .UseUrls(baseUrl) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .Configure(app => + { + app.UsePathBase(basePath); + app.Run(async context => + { + context.Response.StatusCode = statusCode; + await context.Response.WriteAsync(responseBody); + }); + }) + .Build(); + + _service.Start(); + } +} diff --git a/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs index f9f2420f8..70438aed8 100644 --- a/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/ClaimsToThingCreatorTests.cs @@ -66,7 +66,7 @@ public void should_log_error_if_cannot_parse_claim_to_thing() private void ThenTheLoggerIsCalledCorrectly() { _logger - .Verify(x => x.LogDebug(It.IsAny()), Times.Once); + .Verify(x => x.LogDebug(It.IsAny), Times.Once); } private void ThenClaimsToThingsAreReturned() diff --git a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs index 95e4cd91b..854f2abea 100644 --- a/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/HeaderFindAndReplaceCreatorTests.cs @@ -136,7 +136,7 @@ public void should_log_errors_and_not_add_headers() private void ThenTheLoggerIsCalledCorrectly(string message) { - _logger.Verify(x => x.LogWarning(message), Times.Once); + _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == message)), Times.Once); } [Fact] diff --git a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs index e00c8a0af..bf570142c 100644 --- a/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs +++ b/test/Ocelot.UnitTests/Consul/ConsulServiceDiscoveryProviderTests.cs @@ -158,7 +158,7 @@ private void ThenTheLoggerHasBeenCalledCorrectlyWithValidationWarning(params Ser { var service = entry.Service; var expected = $"Unable to use service address: '{service.Address}' and port: {service.Port} as it is invalid for the service: '{service.Service}'. Address must contain host only e.g. 'localhost', and port must be greater than 0."; - _logger.Verify(x => x.LogWarning(expected), Times.Once); + _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == expected)), Times.Once); } } diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs index 86eec01f0..d4c75ed7e 100644 --- a/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToRequestPlainTests.cs @@ -70,7 +70,7 @@ public void should_overwrite_existing_header_with_added_header() private void ThenAnErrorIsLogged(string key, string value) { - _logger.Verify(x => x.LogWarning($"Unable to add header to response {key}: {value}"), Times.Once); + _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == $"Unable to add header to response {key}: {value}")), Times.Once); } private void GivenHttpRequestWithoutHeaders() diff --git a/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs b/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs index 824c270fa..4563965bb 100644 --- a/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs +++ b/test/Ocelot.UnitTests/Headers/AddHeadersToResponseTests.cs @@ -98,7 +98,7 @@ public void should_do_nothing_and_log_error() private void ThenTheErrorIsLogged() { - _logger.Verify(x => x.LogWarning("Unable to add header to response Trace-Id: {TraceId}"), Times.Once); + _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == "Unable to add header to response Trace-Id: {TraceId}")), Times.Once); } private void ThenTheHeaderIsNotAdded(string key) diff --git a/test/Ocelot.UnitTests/Logging/AspDotNetLoggerTests.cs b/test/Ocelot.UnitTests/Logging/AspDotNetLoggerTests.cs deleted file mode 100644 index 61a941b70..000000000 --- a/test/Ocelot.UnitTests/Logging/AspDotNetLoggerTests.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Microsoft.Extensions.Logging; -using Ocelot.Infrastructure.RequestData; -using Ocelot.Logging; - -namespace Ocelot.UnitTests.Logging -{ - public class AspDotNetLoggerTests - { - private readonly Mock> _coreLogger; - private readonly Mock _repo; - private readonly AspDotNetLogger _logger; - private readonly string _b; - private readonly string _a; - private readonly Exception _ex; - - public AspDotNetLoggerTests() - { - _a = "tom"; - _b = "laura"; - _ex = new Exception("oh no"); - _coreLogger = new Mock>(); - _repo = new Mock(); - _logger = new AspDotNetLogger(_coreLogger.Object, _repo.Object); - } - - [Fact] - public void should_log_trace() - { - _logger.LogTrace($"a message from {_a} to {_b}"); - - ThenLevelIsLogged("requestId: no request id, previousRequestId: no previous request id, message: a message from tom to laura", LogLevel.Trace); - } - - [Fact] - public void should_log_info() - { - _logger.LogInformation($"a message from {_a} to {_b}"); - - ThenLevelIsLogged("requestId: no request id, previousRequestId: no previous request id, message: a message from tom to laura", LogLevel.Information); - } - - [Fact] - public void should_log_warning() - { - _logger.LogWarning($"a message from {_a} to {_b}"); - - ThenLevelIsLogged("requestId: no request id, previousRequestId: no previous request id, message: a message from tom to laura", LogLevel.Warning); - } - - [Fact] - public void should_log_error() - { - _logger.LogError($"a message from {_a} to {_b}", _ex); - - ThenLevelIsLogged("requestId: no request id, previousRequestId: no previous request id, message: a message from tom to laura", LogLevel.Error, _ex); - } - - [Fact] - public void should_log_critical() - { - _logger.LogCritical($"a message from {_a} to {_b}", _ex); - - ThenLevelIsLogged("requestId: no request id, previousRequestId: no previous request id, message: a message from tom to laura", LogLevel.Critical, _ex); - } - - private void ThenLevelIsLogged(string expected, LogLevel expectedLogLevel, Exception ex = null) - { - _coreLogger.Verify( - x => x.Log( - expectedLogLevel, - default(EventId), - expected, - ex, - It.IsAny>()), Times.Once); - } - } -} diff --git a/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs b/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs index 274a102d1..d7ac54b64 100644 --- a/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs +++ b/test/Ocelot.UnitTests/Logging/OcelotDiagnosticListenerTests.cs @@ -82,7 +82,7 @@ private void GivenAMiddlewareName() private void ThenTheLogIs(string expected) { _logger.Verify( - x => x.LogTrace(expected)); + x => x.LogTrace(It.Is>(c => c.Invoke() == expected))); } } } diff --git a/test/Ocelot.UnitTests/Logging/OcelotLoggerTests.cs b/test/Ocelot.UnitTests/Logging/OcelotLoggerTests.cs new file mode 100644 index 000000000..0895e276e --- /dev/null +++ b/test/Ocelot.UnitTests/Logging/OcelotLoggerTests.cs @@ -0,0 +1,426 @@ +using Microsoft.Extensions.Logging; +using Ocelot.Infrastructure.RequestData; +using Ocelot.Logging; + +namespace Ocelot.UnitTests.Logging; + +public class OcelotLoggerTests +{ + private readonly Mock> _coreLogger; + private readonly OcelotLogger _logger; + private readonly string _b; + private readonly string _a; + private readonly Exception _ex; + + public OcelotLoggerTests() + { + _a = "tom"; + _b = "laura"; + _ex = new Exception("oh no"); + _coreLogger = new Mock>(); + _coreLogger.Setup(x => x.IsEnabled(It.IsAny())).Returns(true); + var repo = new Mock(); + _logger = new OcelotLogger(_coreLogger.Object, repo.Object); + } + + [Fact] + public void Should_log_trace() + { + _logger.LogTrace(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged( + "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'", + LogLevel.Trace); + } + + [Fact] + public void Should_log_info() + { + _logger.LogInformation(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged( + "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'", + LogLevel.Information); + } + + [Fact] + public void Should_log_warning() + { + _logger.LogWarning(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged( + "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'", + LogLevel.Warning); + } + + [Fact] + public void Should_log_error() + { + _logger.LogError(() => $"a message from {_a} to {_b}", _ex); + + ThenLevelIsLogged( + "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'", + LogLevel.Error, _ex); + } + + [Fact] + public void Should_log_critical() + { + _logger.LogCritical(() => $"a message from {_a} to {_b}", _ex); + + ThenLevelIsLogged( + "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'", + LogLevel.Critical, _ex); + } + + /// + /// Here mocking the original logger implementation to verify calls. + /// + /// The chosen minimum log level. + /// A mocked object. + private static Mock> MockLogger(LogLevel? minimumLevel) + { + var logger = LoggerFactory.Create(builder => + { + if (minimumLevel.HasValue) + { + builder + .AddSimpleConsole() + .SetMinimumLevel(minimumLevel.Value); + } + else + { + builder.AddSimpleConsole(); + } + }) + .CreateLogger>(); + + var mockedILogger = new Mock>(); + mockedILogger.Setup(x => x.IsEnabled(It.IsAny())) + .Returns(logger.IsEnabled) + .Verifiable(); + + return mockedILogger; + } + + [Fact] + public void If_minimum_log_level_not_set_then_log_is_called_for_information_and_above() + { + var mockedILogger = MockLogger(null); + var repo = new Mock(); + + var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); + + currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); + var expected = "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'"; + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug); + + currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace); + + currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Information); + + currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Warning); + + var testException = new Exception("test"); + + currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException); + + currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException); + } + + [Fact] + public void If_minimum_log_level_set_to_none_then_log_method_is_never_called() + { + var mockedILogger = MockLogger(LogLevel.None); + + var repo = new Mock(); + + var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); + + currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); + var expected = "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'"; + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug); + + currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace); + + currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Information); + + currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Warning); + + var testException = new Exception("test"); + + currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Error, testException); + + currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Critical, testException); + } + + [Fact] + public void If_minimum_log_level_set_to_trace_then_log_is_called_for_trace_and_above() + { + var mockedILogger = MockLogger(LogLevel.Trace); + + var repo = new Mock(); + + var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); + + currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); + var expected = "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'"; + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Debug); + + currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Trace); + + currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Information); + + currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Warning); + + var testException = new Exception("test"); + + currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException); + + currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException); + } + + [Fact] + public void String_func_is_never_called_when_log_level_is_disabled() + { + var mockedFunc = new Mock>(); + mockedFunc.Setup(x => x.Invoke()).Returns("test").Verifiable(); + var mockedILogger = MockLogger(LogLevel.None); + var repo = new Mock(); + var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); + + currentLogger.LogTrace(mockedFunc.Object); + + mockedFunc.Verify(x => x.Invoke(), Times.Never); + } + + [Fact] + public void String_func_is_called_once_when_log_level_is_enabled() + { + var mockedFunc = new Mock>(); + mockedFunc.Setup(x => x.Invoke()).Returns("test").Verifiable(); + var mockedILogger = MockLogger(LogLevel.Information); + var repo = new Mock(); + var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); + + currentLogger.LogInformation(mockedFunc.Object); + + mockedFunc.Verify(x => x.Invoke(), Times.Once); + } + + [Fact] + public void If_minimum_log_level_set_to_debug_then_log_is_called_for_debug_and_above() + { + var mockedILogger = MockLogger(LogLevel.Debug); + + var repo = new Mock(); + + var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); + + currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); + var expected = "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'"; + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Debug); + + currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace); + + currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Information); + + currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Warning); + + var testException = new Exception("test"); + + currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException); + + currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException); + } + + [Fact] + public void If_minimum_log_level_set_to_warning_then_log_is_called_for_warning_and_above() + { + var mockedILogger = MockLogger(LogLevel.Warning); + + var repo = new Mock(); + + var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); + + currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); + var expected = "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'"; + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug); + + currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace); + + currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Information); + + currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Warning); + + var testException = new Exception("test"); + + currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException); + + currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException); + } + + [Fact] + public void If_minimum_log_level_set_to_error_then_log_is_called_for_error_and_above() + { + var mockedILogger = MockLogger(LogLevel.Error); + + var repo = new Mock(); + + var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); + + currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); + var expected = "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'"; + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug); + + currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace); + + currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Information); + + currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Warning); + + var testException = new Exception("test"); + + currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Error, testException); + + currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException); + } + + [Fact] + public void If_minimum_log_level_set_to_critical_then_log_is_called_for_critical_and_above() + { + var mockedILogger = MockLogger(LogLevel.Critical); + + var repo = new Mock(); + + var currentLogger = new OcelotLogger(mockedILogger.Object, repo.Object); + + currentLogger.LogDebug(() => $"a message from {_a} to {_b}"); + var expected = "requestId: No RequestId, previousRequestId: No PreviousRequestId, message: 'a message from tom to laura'"; + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Debug); + + currentLogger.LogTrace(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Trace); + + currentLogger.LogInformation(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Information); + + currentLogger.LogWarning(() => $"a message from {_a} to {_b}"); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Warning); + + var testException = new Exception("test"); + + currentLogger.LogError(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsNotLogged(mockedILogger, expected, LogLevel.Error, testException); + + currentLogger.LogCritical(() => $"a message from {_a} to {_b}", testException); + + ThenLevelIsLogged(mockedILogger, expected, LogLevel.Critical, testException); + } + + private void ThenLevelIsLogged(string expected, LogLevel expectedLogLevel, Exception ex = null) + { + _coreLogger.Verify( + x => x.Log( + expectedLogLevel, + default, + expected, + ex, + It.IsAny>()), Times.Once); + } + + private static void ThenLevelIsLogged(Mock> logger, string expected, LogLevel expectedLogLevel, Exception ex = null) + { + logger.Verify( + x => x.Log( + expectedLogLevel, + default, + expected, + ex, + It.IsAny>()), Times.Once); + } + + private static void ThenLevelIsNotLogged(Mock> logger, string expected, LogLevel expectedLogLevel, Exception ex = null) + { + var result = logger.Object.IsEnabled(expectedLogLevel); + + logger.Verify( + x => x.Log( + expectedLogLevel, + default, + expected, + ex, + It.IsAny>()), Times.Never); + } +} diff --git a/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs b/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs index d3e142479..0cd2147c3 100644 --- a/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs +++ b/test/Ocelot.UnitTests/Middleware/OcelotPiplineBuilderTests.cs @@ -117,28 +117,28 @@ public Task Invoke(HttpContext context, IServiceProvider serviceProvider) internal class FakeLogger : IOcelotLogger { - public void LogCritical(string message, Exception exception) - { - } + public void LogCritical(string message, Exception exception) { } - public void LogDebug(string message) - { - } + public void LogCritical(Func messageFactory, Exception exception) { } - public void LogError(string message, Exception exception) - { - } + public void LogError(string message, Exception exception) { } - public void LogInformation(string message) - { - } + public void LogError(Func messageFactory, Exception exception) { } - public void LogTrace(string message) - { - } + public void LogDebug(string message) { } - public void LogWarning(string message) - { - } + public void LogDebug(Func messageFactory) { } + + public void LogInformation(string message) { } + + public void LogInformation(Func messageFactory) { } + + public void LogWarning(string message) { } + + public void LogTrace(string message) { } + + public void LogTrace(Func messageFactory) { } + + public void LogWarning(Func messageFactory) { } } } diff --git a/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs b/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs index c1c056e28..95b59757e 100644 --- a/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs +++ b/test/Ocelot.UnitTests/Polly/PollyQoSProviderTests.cs @@ -145,7 +145,7 @@ public async Task should_throw_and_before_delay_should_not_allow_requests() await Assert.ThrowsAsync>(async () => await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - await Task.Delay(100); + await Task.Delay(200); await Assert.ThrowsAsync>(async () => await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); @@ -162,7 +162,7 @@ public async Task should_throw_but_after_delay_should_allow_one_more_internal_se await Assert.ThrowsAsync>(async () => await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - await Task.Delay(500); + await Task.Delay(600); Assert.Equal(HttpStatusCode.InternalServerError, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); } @@ -178,7 +178,7 @@ public async Task should_throw_but_after_delay_should_allow_one_more_internal_se await Assert.ThrowsAsync>(async () => await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - await Task.Delay(500); + await Task.Delay(600); Assert.Equal(HttpStatusCode.InternalServerError, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))).StatusCode); await Assert.ThrowsAsync>(async () => @@ -196,7 +196,7 @@ public async Task should_throw_but_after_delay_should_allow_one_more_ok_request_ await Assert.ThrowsAsync>(async () => await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response))); - await Task.Delay(500); + await Task.Delay(600); var response2 = new HttpResponseMessage(HttpStatusCode.OK); Assert.Equal(HttpStatusCode.OK, (await pollyPolicyWrapper.AsyncPollyPolicy.ExecuteAsync(() => Task.FromResult(response2))).StatusCode); @@ -221,7 +221,7 @@ private static PollyPolicyWrapper PolicyWrapperFactory(stri var options = new QoSOptionsBuilder() .WithTimeoutValue(5000) .WithExceptionsAllowedBeforeBreaking(inactiveExceptionsAllowedBeforeBreaking ? 0 : 2) - .WithDurationOfBreak(200) + .WithDurationOfBreak(300) .Build(); var upstreamPath = new UpstreamPathTemplateBuilder() diff --git a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs index ee3c592e2..d6f63f3b9 100644 --- a/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/Requester/DelegatingHandlerHandlerProviderFactoryTests.cs @@ -375,7 +375,7 @@ public void should_log_error_and_return_no_qos_provider_delegate_when_qos_factor private void ThenTheWarningIsLogged() { - _logger.Verify(x => x.LogWarning($"Route {_downstreamRoute.UpstreamPathTemplate} specifies use QoS but no QosHandler found in DI container. Will use not use a QosHandler, please check your setup!"), Times.Once); + _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == $"Route {_downstreamRoute.UpstreamPathTemplate} specifies use QoS but no QosHandler found in DI container. Will use not use a QosHandler, please check your setup!")), Times.Once); } private void ThenHandlerAtPositionIs(int pos) diff --git a/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs b/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs index 505cbd70f..e352ee4a3 100644 --- a/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpClientBuilderTests.cs @@ -284,7 +284,7 @@ private void GivenCacheIsCalledWithExpectedKey(string expectedKey) private void ThenTheDangerousAcceptAnyServerCertificateValidatorWarningIsLogged() { - _logger.Verify(x => x.LogWarning($"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamRoute, UpstreamPathTemplate: {_context.Items.DownstreamRoute().UpstreamPathTemplate}, DownstreamPathTemplate: {_context.Items.DownstreamRoute().DownstreamPathTemplate}"), Times.Once); + _logger.Verify(x => x.LogWarning(It.Is>(y => y.Invoke() == $"You have ignored all SSL warnings by using DangerousAcceptAnyServerCertificateValidator for this DownstreamRoute, UpstreamPathTemplate: {_context.Items.DownstreamRoute().UpstreamPathTemplate}, DownstreamPathTemplate: {_context.Items.DownstreamRoute().DownstreamPathTemplate}")), Times.Once); } private void GivenTheClientIsCached() diff --git a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs index b4ed6535c..3dda1467c 100644 --- a/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Requester/HttpRequesterMiddlewareTests.cs @@ -104,7 +104,7 @@ private void WarningIsLogged() { _logger.Verify( x => x.LogWarning( - It.IsAny() + It.IsAny>() ), Times.Once); } @@ -113,7 +113,7 @@ private void InformationIsLogged() { _logger.Verify( x => x.LogInformation( - It.IsAny() + It.IsAny>() ), Times.Once); } diff --git a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs index fd4fb72e5..465e06a2e 100644 --- a/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs +++ b/test/Ocelot.UnitTests/ServiceDiscovery/ServiceDiscoveryProviderFactoryTests.cs @@ -152,12 +152,12 @@ private void ThenTheResultIsError() _logInformationMessages.ShouldNotBeNull() .Count.ShouldBe(2); - _logger.Verify(x => x.LogInformation(It.IsAny()), + _logger.Verify(x => x.LogInformation(It.IsAny>()), Times.Exactly(2)); _logWarningMessages.ShouldNotBeNull() .Count.ShouldBe(1); - _logger.Verify(x => x.LogWarning(It.IsAny()), + _logger.Verify(x => x.LogWarning(It.IsAny>()), Times.Once()); } @@ -187,10 +187,10 @@ private void GivenTheRoute(ServiceProviderConfiguration serviceConfig, Downstrea private void WhenIGetTheServiceProvider() { - _logger.Setup(x => x.LogInformation(It.IsAny())) - .Callback(message => _logInformationMessages.Add(message)); - _logger.Setup(x => x.LogWarning(It.IsAny())) - .Callback(message => _logWarningMessages.Add(message)); + _logger.Setup(x => x.LogInformation(It.IsAny>())) + .Callback>(myFunc => _logInformationMessages.Add(myFunc.Invoke())); + _logger.Setup(x => x.LogWarning(It.IsAny>())) + .Callback>(myFunc => _logWarningMessages.Add(myFunc.Invoke())); _result = _factory.Get(_serviceConfig, _route); } diff --git a/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs b/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs index 2afd41c2f..d5190ce01 100644 --- a/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/WebSockets/WebSocketsProxyMiddlewareTests.cs @@ -68,8 +68,8 @@ private void GivenPropertyDangerousAcceptAnyServerCertificateValidator(bool enab _client.SetupSet(x => x.Options.RemoteCertificateValidationCallback = It.IsAny()) .Callback(actual.Add); - _logger.Setup(x => x.LogWarning(It.IsAny())) - .Callback(actual.Add); + _logger.Setup(x => x.LogWarning(It.IsAny>())) + .Callback>(y => actual.Add(y.Invoke())); } private void AndDoNotSetupProtocolsAndHeaders() @@ -111,7 +111,7 @@ private void ThenIgnoredAllSslWarnings(List actual) var request = _context.Object.Items.DownstreamRequest(); route.DangerousAcceptAnyServerCertificateValidator.ShouldBeTrue(); - _logger.Verify(x => x.LogWarning(It.IsAny()), Times.Once()); + _logger.Verify(x => x.LogWarning(It.IsAny>()), Times.Once()); var warning = actual.Last() as string; warning.ShouldNotBeNullOrEmpty(); var expectedWarning = string.Format(WebSocketsProxyMiddleware.IgnoredSslWarningFormat, route.UpstreamPathTemplate, route.DownstreamPathTemplate); @@ -153,8 +153,8 @@ private void GivenNonWebsocketScheme(string scheme, List actual) }; _context.SetupGet(x => x.Items).Returns(items); - _logger.Setup(x => x.LogWarning(It.IsAny())) - .Callback(actual.Add); + _logger.Setup(x => x.LogWarning(It.IsAny>())) + .Callback>(myFunc => actual.Add(myFunc.Invoke())); } private void ThenNonWsSchemesAreReplaced(string scheme, string expectedScheme, List actual) @@ -163,7 +163,7 @@ private void ThenNonWsSchemesAreReplaced(string scheme, string expectedScheme, L var request = _context.Object.Items.DownstreamRequest(); route.DangerousAcceptAnyServerCertificateValidator.ShouldBeFalse(); - _logger.Verify(x => x.LogWarning(It.IsAny()), Times.Once()); + _logger.Verify(x => x.LogWarning(It.IsAny>()), Times.Once()); var warning = actual.First() as string; warning.ShouldNotBeNullOrEmpty(); warning.ShouldContain($"'{scheme}'"); diff --git a/test/Ocelot.UnitTests/appsettings.json b/test/Ocelot.UnitTests/appsettings.json index 57566b4e3..e18d6ebb9 100644 --- a/test/Ocelot.UnitTests/appsettings.json +++ b/test/Ocelot.UnitTests/appsettings.json @@ -1,10 +1,8 @@ { "Logging": { - "IncludeScopes": true, "LogLevel": { - "Default": "Error", - "System": "Error", - "Microsoft": "Error" + "Default": "Trace", + "Microsoft.AspNetCore": "Trace" } }, "spring": { From b2246a59891b5675e4681be0c935609e2fa123bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Raynald=20Messi=C3=A9?= Date: Fri, 24 Nov 2023 23:21:11 +0100 Subject: [PATCH 09/12] #1783 Less logs for circuit breakers (Polly exceptions) (#1786) * #1783 More accurate logs for circuit breakers (and other "polly" exceptions) Remove try/catch in PollyPoliciesDelegatingHandler and add a more generic AddPolly to be able to use a specific PollyQoSProvider * fix should_be_invalid_re_route_using_downstream_http_version UT * fix remarks on PR * arrange code * fix UT * merge with release/net8 branch * switch benchmark to Net8 * Fix warnings * Final review --------- Co-authored-by: Ray Co-authored-by: raman-m --- .../OcelotBuilderExtensions.cs | 22 ++++-- .../PollyPoliciesDelegatingHandler.cs | 36 +++++----- .../PollyPolicyWrapper.cs | 8 +-- test/Ocelot.AcceptanceTests/PollyQoSTests.cs | 70 +++++++++---------- test/Ocelot.Testing/PortFinder.cs | 3 +- .../Validation/RouteFluentValidatorTests.cs | 4 +- test/Ocelot.UnitTests/Ocelot.UnitTests.csproj | 1 + 7 files changed, 74 insertions(+), 70 deletions(-) diff --git a/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs b/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs index 14e3750fb..c37c690d8 100644 --- a/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs +++ b/src/Ocelot.Provider.Polly/OcelotBuilderExtensions.cs @@ -13,6 +13,19 @@ namespace Ocelot.Provider.Polly; public static class OcelotBuilderExtensions { + public static IOcelotBuilder AddPolly(this IOcelotBuilder builder, + QosDelegatingHandlerDelegate delegatingHandler, + Dictionary> errorMapping) + where T : class, IPollyQoSProvider + { + builder.Services + .AddSingleton(errorMapping) + .AddSingleton, T>() + .AddSingleton(delegatingHandler); + + return builder; + } + public static IOcelotBuilder AddPolly(this IOcelotBuilder builder) { var errorMapping = new Dictionary> @@ -22,14 +35,9 @@ public static IOcelotBuilder AddPolly(this IOcelotBuilder builder) { typeof(BrokenCircuitException), e => new RequestTimedOutError(e) }, { typeof(BrokenCircuitException), e => new RequestTimedOutError(e) }, }; - - builder.Services - .AddSingleton(errorMapping) - .AddSingleton, PollyQoSProvider>() - .AddSingleton(GetDelegatingHandler); - return builder; + return AddPolly(builder, GetDelegatingHandler, errorMapping); } - private static DelegatingHandler GetDelegatingHandler(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory) + private static DelegatingHandler GetDelegatingHandler(DownstreamRoute route, IHttpContextAccessor contextAccessor, IOcelotLoggerFactory loggerFactory) => new PollyPoliciesDelegatingHandler(route, contextAccessor, loggerFactory); } diff --git a/src/Ocelot.Provider.Polly/PollyPoliciesDelegatingHandler.cs b/src/Ocelot.Provider.Polly/PollyPoliciesDelegatingHandler.cs index fbf5e4a57..03be86495 100644 --- a/src/Ocelot.Provider.Polly/PollyPoliciesDelegatingHandler.cs +++ b/src/Ocelot.Provider.Polly/PollyPoliciesDelegatingHandler.cs @@ -30,27 +30,23 @@ private IPollyQoSProvider GetQoSProvider() return _contextAccessor.HttpContext.RequestServices.GetService>(); } - protected override async Task SendAsync(HttpRequestMessage request, - CancellationToken cancellationToken) + /// + /// Sends an HTTP request to the inner handler to send to the server as an asynchronous operation. + /// + /// Downstream request. + /// Token to cancel the task. + /// A object of a result. + /// Exception thrown when a circuit is broken. + /// Exception thrown by and classes. + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var qoSProvider = GetQoSProvider(); - try - { - // at least one policy (timeout) will be returned - // AsyncPollyPolicy can't be null - // AsyncPollyPolicy constructor will throw if no policy is provided - var policy = qoSProvider.GetPollyPolicyWrapper(_route).AsyncPollyPolicy; - return await policy.ExecuteAsync(async () => await base.SendAsync(request, cancellationToken)); - } - catch (BrokenCircuitException ex) - { - _logger.LogError("Reached to allowed number of exceptions. Circuit is open", ex); - throw; - } - catch (HttpRequestException ex) - { - _logger.LogError($"Error in {nameof(PollyPoliciesDelegatingHandler)}.{nameof(SendAsync)}", ex); - throw; - } + + // At least one policy (timeout) will be returned + // AsyncPollyPolicy can't be null + // AsyncPollyPolicy constructor will throw if no policy is provided + var policy = qoSProvider.GetPollyPolicyWrapper(_route).AsyncPollyPolicy; + + return await policy.ExecuteAsync(async () => await base.SendAsync(request, cancellationToken)); } } diff --git a/src/Ocelot.Provider.Polly/PollyPolicyWrapper.cs b/src/Ocelot.Provider.Polly/PollyPolicyWrapper.cs index 508803976..0b3058cbb 100644 --- a/src/Ocelot.Provider.Polly/PollyPolicyWrapper.cs +++ b/src/Ocelot.Provider.Polly/PollyPolicyWrapper.cs @@ -11,12 +11,10 @@ public class PollyPolicyWrapper public PollyPolicyWrapper(params IAsyncPolicy[] policies) { var allPolicies = policies.Where(p => p != null).ToArray(); - AsyncPollyPolicy = allPolicies.First(); - if (allPolicies.Length > 1) - { - AsyncPollyPolicy = Policy.WrapAsync(allPolicies); - } + AsyncPollyPolicy = allPolicies.Length > 1 ? + Policy.WrapAsync(allPolicies) : + allPolicies[0]; } public IAsyncPolicy AsyncPollyPolicy { get; } diff --git a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs index e243bd8f8..f15838169 100644 --- a/test/Ocelot.AcceptanceTests/PollyQoSTests.cs +++ b/test/Ocelot.AcceptanceTests/PollyQoSTests.cs @@ -7,34 +7,34 @@ namespace Ocelot.AcceptanceTests public class PollyQoSTests : IDisposable { private readonly Steps _steps; - private readonly ServiceHandler _serviceHandler; - + private readonly ServiceHandler _serviceHandler; + public PollyQoSTests() { _serviceHandler = new ServiceHandler(); _steps = new Steps(); } - - private static FileConfiguration FileConfigurationFactory(int port, QoSOptions options, string httpMethod = nameof(HttpMethods.Get)) - => new() + + private static FileConfiguration FileConfigurationFactory(int port, QoSOptions options, + string httpMethod = nameof(HttpMethods.Get)) => new() + { + Routes = new List { - Routes = new List + new() { - new() + DownstreamPathTemplate = "/", + DownstreamScheme = Uri.UriSchemeHttp, + DownstreamHostAndPorts = new() { - DownstreamPathTemplate = "/", - DownstreamScheme = Uri.UriSchemeHttp, - DownstreamHostAndPorts = new() - { - new("localhost", port), - }, - UpstreamPathTemplate = "/", - UpstreamHttpMethod = new() { httpMethod }, - QoSOptions = new FileQoSOptions(options), + new("localhost", port), }, + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new() {httpMethod}, + QoSOptions = new FileQoSOptions(options), }, - }; - + }, + }; + [Fact] public void Should_not_timeout() { @@ -49,13 +49,13 @@ public void Should_not_timeout() .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) .BDDfy(); } - + [Fact] public void Should_timeout() { var port = PortFinder.GetRandomPort(); var configuration = FileConfigurationFactory(port, new QoSOptions(0, 0, 10, null), HttpMethods.Post); - + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", 201, string.Empty, 1000)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunningWithPolly()) @@ -64,13 +64,13 @@ public void Should_timeout() .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.ServiceUnavailable)) .BDDfy(); } - + [Fact] public void Should_open_circuit_breaker_after_two_exceptions() { var port = PortFinder.GetRandomPort(); var configuration = FileConfigurationFactory(port, new QoSOptions(2, 5000, 100000, null)); - + this.Given(x => x.GivenThereIsABrokenServiceRunningOn($"http://localhost:{port}")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) .And(x => _steps.GivenOcelotIsRunningWithPolly()) @@ -86,7 +86,7 @@ public void Should_open_circuit_breaker_then_close() { var port = PortFinder.GetRandomPort(); var configuration = FileConfigurationFactory(port, new QoSOptions(1, 500, 1000, null)); - + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port}", "Hello from Laura")) .Given(x => _steps.GivenThereIsAConfiguration(configuration)) .Given(x => _steps.GivenOcelotIsRunningWithPolly()) @@ -105,21 +105,21 @@ public void Should_open_circuit_breaker_then_close() .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } - + [Fact] public void Open_circuit_should_not_effect_different_route() { var port1 = PortFinder.GetRandomPort(); var port2 = PortFinder.GetRandomPort(); var qos1 = new QoSOptions(1, 1000, 500, null); - + var configuration = FileConfigurationFactory(port1, qos1); var route2 = configuration.Routes[0].Clone() as FileRoute; route2.DownstreamHostAndPorts[0].Port = port2; route2.UpstreamPathTemplate = "/working"; route2.QoSOptions = new(); configuration.Routes.Add(route2); - + this.Given(x => x.GivenThereIsAPossiblyBrokenServiceRunningOn($"http://localhost:{port1}", "Hello from Laura")) .And(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port2}/", 200, "Hello from Tom", 0)) .And(x => _steps.GivenThereIsAConfiguration(configuration)) @@ -142,7 +142,7 @@ public void Open_circuit_should_not_effect_different_route() .And(x => _steps.ThenTheResponseBodyShouldBe("Hello from Laura")) .BDDfy(); } - + private static void GivenIWaitMilliseconds(int ms) => Thread.Sleep(ms); private void GivenThereIsABrokenServiceRunningOn(string url) @@ -168,8 +168,8 @@ private void GivenThereIsAPossiblyBrokenServiceRunningOn(string url, string resp context.Response.StatusCode = 200; await context.Response.WriteAsync(responseBody); }); - } - + } + private void GivenThereIsAServiceRunningOn(string url, int statusCode, string responseBody, int timeout) { _serviceHandler.GivenThereIsAServiceRunningOn(url, async context => @@ -180,11 +180,11 @@ private void GivenThereIsAServiceRunningOn(string url, int statusCode, string re }); } - public void Dispose() - { - _serviceHandler?.Dispose(); + public void Dispose() + { + _serviceHandler?.Dispose(); _steps.Dispose(); - GC.SuppressFinalize(this); - } - } + GC.SuppressFinalize(this); + } + } } diff --git a/test/Ocelot.Testing/PortFinder.cs b/test/Ocelot.Testing/PortFinder.cs index 6eb6b64d4..4b661ada7 100644 --- a/test/Ocelot.Testing/PortFinder.cs +++ b/test/Ocelot.Testing/PortFinder.cs @@ -1,4 +1,5 @@ -using System.Collections.Concurrent; +using System; +using System.Collections.Concurrent; using System.Net; using System.Net.Sockets; diff --git a/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs index 7f1ee7ddd..accc5a94d 100644 --- a/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs @@ -358,7 +358,7 @@ public void should_be_invalid_re_route_using_downstream_http_version(string vers this.Given(_ => GivenThe(fileRoute)) .When(_ => WhenIValidate()) .Then(_ => ThenTheResultIsInvalid()) - .And(_ => ThenTheErrorsContains("'Downstream Http Version' is not in the correct format.")) + .And(_ => ThenTheErrorsContains("'Downstream Http Version'")) // this error message changes depending on the OS language .BDDfy(); } @@ -396,7 +396,7 @@ private void ThenTheResultIsInvalid() private void ThenTheErrorsContains(string expected) { - _result.Errors.ShouldContain(x => x.ErrorMessage == expected); + _result.Errors.ShouldContain(x => x.ErrorMessage.Contains(expected)); } private class FakeAutheHandler : IAuthenticationHandler diff --git a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj index c54b547a4..cb2557562 100644 --- a/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj +++ b/test/Ocelot.UnitTests/Ocelot.UnitTests.csproj @@ -34,6 +34,7 @@ + From 27a5999f48f4205dd80771160807b0f37ce9284b Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Mon, 27 Nov 2023 16:22:33 +0300 Subject: [PATCH 10/12] Revert #1172 feature (#1807) * Revert #1172 * Remove Header * Take actual version of caching.rst and remove Header info --- docs/features/caching.rst | 5 +- src/Ocelot/Cache/CacheKeyGenerator.cs | 39 ++----- src/Ocelot/Cache/ICacheKeyGenerator.cs | 5 +- .../Cache/Middleware/OutputCacheMiddleware.cs | 2 +- src/Ocelot/Configuration/CacheOptions.cs | 9 +- .../Configuration/Creator/RoutesCreator.cs | 2 +- .../Configuration/File/FileCacheOptions.cs | 5 +- .../Request/Middleware/DownstreamRequest.cs | 15 +-- .../Middleware/RequestIdMiddleware.cs | 4 +- .../Cache/CacheKeyGeneratorTests.cs | 108 ++---------------- .../Cache/OutputCacheMiddlewareTests.cs | 2 +- .../OutputCacheMiddlewareRealCacheTests.cs | 4 +- 12 files changed, 36 insertions(+), 164 deletions(-) diff --git a/docs/features/caching.rst b/docs/features/caching.rst index f7118fe83..a7cd4096f 100644 --- a/docs/features/caching.rst +++ b/docs/features/caching.rst @@ -30,14 +30,11 @@ Finally, in order to use caching on a route in your Route configuration add this .. code-block:: json - "FileCacheOptions": { "TtlSeconds": 15, "Region": "europe-central", "Header": "Authorization" } + "FileCacheOptions": { "TtlSeconds": 15, "Region": "europe-central" } In this example **TtlSeconds** is set to 15 which means the cache will expire after 15 seconds. The **Region** represents a region of caching. -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 `_ 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. diff --git a/src/Ocelot/Cache/CacheKeyGenerator.cs b/src/Ocelot/Cache/CacheKeyGenerator.cs index 0b25212aa..e6ae88213 100644 --- a/src/Ocelot/Cache/CacheKeyGenerator.cs +++ b/src/Ocelot/Cache/CacheKeyGenerator.cs @@ -1,43 +1,20 @@ -using Ocelot.Configuration; -using Ocelot.Request.Middleware; +using Ocelot.Request.Middleware; namespace Ocelot.Cache { public class CacheKeyGenerator : ICacheKeyGenerator { - private const char Delimiter = '-'; - - public async ValueTask GenerateRequestCacheKey(DownstreamRequest downstreamRequest, DownstreamRoute downstreamRoute) + public string GenerateRequestCacheKey(DownstreamRequest downstreamRequest) { - 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 downStreamUrlKeyBuilder = new StringBuilder($"{downstreamRequest.Method}-{downstreamRequest.OriginalString}"); + if (downstreamRequest.Content != null) { - return MD5Helper.GenerateMd5(builder.ToString()); + var requestContentString = Task.Run(async () => await downstreamRequest.Content.ReadAsStringAsync()).Result; + downStreamUrlKeyBuilder.Append(requestContentString); } - var requestContentString = await downstreamRequest.ReadContentAsync(); - builder.Append(Delimiter) - .Append(requestContentString); - - return MD5Helper.GenerateMd5(builder.ToString()); + var hashedContent = MD5Helper.GenerateMd5(downStreamUrlKeyBuilder.ToString()); + return hashedContent; } } } diff --git a/src/Ocelot/Cache/ICacheKeyGenerator.cs b/src/Ocelot/Cache/ICacheKeyGenerator.cs index d2ccb0ef5..32a1f989e 100644 --- a/src/Ocelot/Cache/ICacheKeyGenerator.cs +++ b/src/Ocelot/Cache/ICacheKeyGenerator.cs @@ -1,10 +1,9 @@ -using Ocelot.Configuration; -using Ocelot.Request.Middleware; +using Ocelot.Request.Middleware; namespace Ocelot.Cache { public interface ICacheKeyGenerator { - ValueTask GenerateRequestCacheKey(DownstreamRequest downstreamRequest, DownstreamRoute downstreamRoute); + string GenerateRequestCacheKey(DownstreamRequest downstreamRequest); } } diff --git a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs index 8c26c5ca6..5e3158b48 100644 --- a/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs +++ b/src/Ocelot/Cache/Middleware/OutputCacheMiddleware.cs @@ -33,7 +33,7 @@ public async Task Invoke(HttpContext httpContext) var downstreamRequest = httpContext.Items.DownstreamRequest(); var downstreamUrlKey = $"{downstreamRequest.Method}-{downstreamRequest.OriginalString}"; - var downStreamRequestCacheKey = await _cacheGenerator.GenerateRequestCacheKey(downstreamRequest, downstreamRoute); + var downStreamRequestCacheKey = _cacheGenerator.GenerateRequestCacheKey(downstreamRequest); Logger.LogDebug(() => $"Started checking cache for the '{downstreamUrlKey}' key."); diff --git a/src/Ocelot/Configuration/CacheOptions.cs b/src/Ocelot/Configuration/CacheOptions.cs index 5b8d2987b..d509b38e9 100644 --- a/src/Ocelot/Configuration/CacheOptions.cs +++ b/src/Ocelot/Configuration/CacheOptions.cs @@ -2,17 +2,14 @@ { public class CacheOptions { - public CacheOptions(int ttlSeconds, string region, string header) + public CacheOptions(int ttlSeconds, string region) { TtlSeconds = ttlSeconds; - Region = region; - Header = header; + Region = region; } public int TtlSeconds { get; } - public string Region { get; } - - public string Header { get; } + public string Region { get; } } } diff --git a/src/Ocelot/Configuration/Creator/RoutesCreator.cs b/src/Ocelot/Configuration/Creator/RoutesCreator.cs index 8c1f1de63..100c65116 100644 --- a/src/Ocelot/Configuration/Creator/RoutesCreator.cs +++ b/src/Ocelot/Configuration/Creator/RoutesCreator.cs @@ -122,7 +122,7 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf .WithClaimsToDownstreamPath(claimsToDownstreamPath) .WithRequestIdKey(requestIdKey) .WithIsCached(fileRouteOptions.IsCached) - .WithCacheOptions(new CacheOptions(fileRoute.FileCacheOptions.TtlSeconds, region, fileRoute.FileCacheOptions.Header)) + .WithCacheOptions(new CacheOptions(fileRoute.FileCacheOptions.TtlSeconds, region)) .WithDownstreamScheme(fileRoute.DownstreamScheme) .WithLoadBalancerOptions(lbOptions) .WithDownstreamAddresses(downstreamAddresses) diff --git a/src/Ocelot/Configuration/File/FileCacheOptions.cs b/src/Ocelot/Configuration/File/FileCacheOptions.cs index 79cb81da8..65c481344 100644 --- a/src/Ocelot/Configuration/File/FileCacheOptions.cs +++ b/src/Ocelot/Configuration/File/FileCacheOptions.cs @@ -1,22 +1,19 @@ namespace Ocelot.Configuration.File { public class FileCacheOptions - { + { public FileCacheOptions() { - Header = string.Empty; Region = string.Empty; TtlSeconds = 0; } public FileCacheOptions(FileCacheOptions from) { - Header = from.Header; Region = from.Region; TtlSeconds = from.TtlSeconds; } - public string Header { get; set; } public string Region { get; set; } public int TtlSeconds { get; set; } } diff --git a/src/Ocelot/Request/Middleware/DownstreamRequest.cs b/src/Ocelot/Request/Middleware/DownstreamRequest.cs index 03e4134fc..18b8641e1 100644 --- a/src/Ocelot/Request/Middleware/DownstreamRequest.cs +++ b/src/Ocelot/Request/Middleware/DownstreamRequest.cs @@ -6,8 +6,6 @@ public class DownstreamRequest { private readonly HttpRequestMessage _request; - public DownstreamRequest() { } - public DownstreamRequest(HttpRequestMessage request) { _request = request; @@ -19,13 +17,14 @@ public DownstreamRequest(HttpRequestMessage request) Headers = _request.Headers; AbsolutePath = _request.RequestUri.AbsolutePath; Query = _request.RequestUri.Query; + Content = _request.Content; } - public virtual HttpHeaders Headers { get; } + public HttpRequestHeaders Headers { get; } - public virtual string Method { get; } + public string Method { get; } - public virtual string OriginalString { get; } + public string OriginalString { get; } public string Scheme { get; set; } @@ -37,11 +36,7 @@ public DownstreamRequest(HttpRequestMessage request) public string Query { get; set; } - public virtual bool HasContent { get => _request?.Content != null; } - - public virtual Task ReadContentAsync() => HasContent - ? _request.Content.ReadAsStringAsync() - : Task.FromResult(string.Empty); + public HttpContent Content { get; set; } public HttpRequestMessage ToHttpRequestMessage() { diff --git a/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs b/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs index 65cad3252..5fef5a918 100644 --- a/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs +++ b/src/Ocelot/RequestId/Middleware/RequestIdMiddleware.cs @@ -63,14 +63,14 @@ private void SetOcelotRequestId(HttpContext httpContext) } } - private static bool ShouldAddRequestId(RequestId requestId, HttpHeaders headers) + private static bool ShouldAddRequestId(RequestId requestId, HttpRequestHeaders headers) { return !string.IsNullOrEmpty(requestId?.RequestIdKey) && !string.IsNullOrEmpty(requestId.RequestIdValue) && !RequestIdInHeaders(requestId, headers); } - private static bool RequestIdInHeaders(RequestId requestId, HttpHeaders headers) + private static bool RequestIdInHeaders(RequestId requestId, HttpRequestHeaders headers) { return headers.TryGetValues(requestId.RequestIdKey, out var value); } diff --git a/test/Ocelot.UnitTests/Cache/CacheKeyGeneratorTests.cs b/test/Ocelot.UnitTests/Cache/CacheKeyGeneratorTests.cs index c0572cfb2..f71e684af 100644 --- a/test/Ocelot.UnitTests/Cache/CacheKeyGeneratorTests.cs +++ b/test/Ocelot.UnitTests/Cache/CacheKeyGeneratorTests.cs @@ -1,122 +1,32 @@ 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 Mock _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"; + private readonly DownstreamRequest _downstreamRequest; public CacheKeyGeneratorTests() { _cacheKeyGenerator = new CacheKeyGenerator(); - - _downstreamRequest = new Mock(); - _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(); + _cacheKeyGenerator = new CacheKeyGenerator(); + _downstreamRequest = new DownstreamRequest(new HttpRequestMessage(HttpMethod.Get, "https://some.url/blah?abcd=123")); } [Fact] - public void should_generate_cache_key_happy_path() + public void should_generate_cache_key_from_context() { - 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)) + this.Given(x => x.GivenCacheKeyFromContext(_downstreamRequest)) .BDDfy(); } - 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) + private void GivenCacheKeyFromContext(DownstreamRequest downstreamRequest) { - _generatedCacheKey.ShouldBe(expected); + var generatedCacheKey = _cacheKeyGenerator.GenerateRequestCacheKey(downstreamRequest); + var cachekey = MD5Helper.GenerateMd5("GET-https://some.url/blah?abcd=123"); + generatedCacheKey.ShouldBe(cachekey); } } - - internal class HttpHeadersStub : HttpHeaders - { - public HttpHeadersStub() : base() { } - } } diff --git a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs index 6ad948232..b494cbe7f 100644 --- a/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Cache/OutputCacheMiddlewareTests.cs @@ -106,7 +106,7 @@ private void GivenTheDownstreamRouteIs() var route = new RouteBuilder() .WithDownstreamRoute(new DownstreamRouteBuilder() .WithIsCached(true) - .WithCacheOptions(new CacheOptions(100, "kanken", null)) + .WithCacheOptions(new CacheOptions(100, "kanken")) .WithUpstreamHttpMethod(new List { "Get" }) .Build()) .WithUpstreamHttpMethod(new List { "Get" }) diff --git a/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs b/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs index 9a588002e..bb02d98b0 100644 --- a/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs +++ b/test/Ocelot.UnitTests/CacheManager/OutputCacheMiddlewareRealCacheTests.cs @@ -25,7 +25,7 @@ public OutputCacheMiddlewareRealCacheTests() { _httpContext = new DefaultHttpContext(); _loggerFactory = new Mock(); - _logger = new Mock(); + _logger = new Mock(); _loggerFactory.Setup(x => x.CreateLogger()).Returns(_logger.Object); var cacheManagerOutputCache = CacheFactory.Build("OcelotOutputCache", x => { @@ -77,7 +77,7 @@ private void GivenTheDownstreamRouteIs() { var route = new DownstreamRouteBuilder() .WithIsCached(true) - .WithCacheOptions(new CacheOptions(100, "kanken", null)) + .WithCacheOptions(new CacheOptions(100, "kanken")) .WithUpstreamHttpMethod(new List { "Get" }) .Build(); From 22bc5b692a6428bde31045a496251f7a9e4dba6d Mon Sep 17 00:00:00 2001 From: raman-m Date: Mon, 27 Nov 2023 22:05:44 +0300 Subject: [PATCH 11/12] Release 22.0 | +semver: breaking --- ReleaseNotes.md | 44 ++++++++++++++++++++++++++------------- build.cake | 11 +++++----- docs/conf.py | 2 +- docs/features/logging.rst | 8 +++++-- docs/index.rst | 2 +- 5 files changed, 44 insertions(+), 23 deletions(-) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 1193d5db1..e3df12c17 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,17 +1,33 @@ -## Upgrade to .NET 8 (version {0}) aka [.NET 8](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) release -> Read article: [Announcing .NET 8](https://devblogs.microsoft.com/dotnet/announcing-dotnet-8/) by Gaurav Seth, on November 14th, 2023 +## October 2023 (version {0}) aka [Swiss Locomotive](https://en.wikipedia.org/wiki/SBB-CFF-FFS_Ae_6/6) release +> Codenamed as **[Swiss Locomotive](https://www.google.com/search?q=swiss+locomotive)** -### About -We are pleased to announce to you that we can now offer the support of [.NET 8](https://dotnet.microsoft.com/en-us/download). -But that is not all, in this release, we are adopting support of several versions of the .NET framework through [multitargeting](https://learn.microsoft.com/en-us/dotnet/standard/frameworks). -The Ocelot distribution is now compatible with .NET **6**, **7** and **8**. :tada: +### Focused On +
+ Logging feature. Performance review, redesign and improvements with new best practices to log -In the future, we will try to ensure the support of the [.NET SDKs](https://dotnet.microsoft.com/en-us/download/dotnet) that are still actively maintained by the .NET team and community. -Current .NET versions in support are the following: [6, 7, 8](https://dotnet.microsoft.com/en-us/download/dotnet). + - Proposing a centralized `WriteLog` method for the `OcelotLogger` + - Factory methods for computed strings such as `string.Format` or interpolated strings + - Using `ILogger.IsEnabled` before calling the native `WriteLog` implementation and invoking string factory method +
+
+ Quality of Service feature. Redesign and stabilization, and it produces less log records now. + + - Fixing issue with [Polly](https://www.thepollyproject.org/) Circuit Breaker not opening after max number of retries reached + - Removing useless log calls that could have an impact on performance + - Polly [lib](https://www.nuget.org/packages/Polly#versions-body-tab) reference updating to latest `8.2.0` with some code improvements +
+
+ Documentation for Logging, Request ID, Routing and Websockets + + - [Logging](https://ocelot.readthedocs.io/en/latest/features/logging.html) + - [Request ID](https://ocelot.readthedocs.io/en/latest/features/requestid.html) + - [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html) + - [Websockets](https://ocelot.readthedocs.io/en/latest/features/websockets.html) +
+
+ Testing improvements and stabilization aka bug fixing -### Technical info -As an ASP.NET Core app, now Ocelot targets `net6.0`, `net7.0` and `net8.0` frameworks. - -Starting with **v{0}**, the solution's code base supports [Multitargeting](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-multitargeting-overview) as SDK-style projects. -It should be easier for teams to move between (migrate to) .NET 6, 7 and 8 frameworks. Also, new features will be available for all .NET SDKs which we support via multitargeting. -Find out more here: [Target frameworks in SDK-style projects](https://learn.microsoft.com/en-us/dotnet/standard/frameworks) + - [Routing](https://ocelot.readthedocs.io/en/latest/features/routing.html) bug fixing: query string placeholders including **CatchAll** one aka `{{everything}}` and query string duplicates removal + - [QoS](https://ocelot.readthedocs.io/en/latest/features/qualityofservice.html) bug fixing: Polly circuit breaker exceptions + - Testing bug fixing: rare failed builds because of unstable Polly tests. Acceptance common logic for ports +
diff --git a/build.cake b/build.cake index 75f30a755..5047c5acc 100644 --- a/build.cake +++ b/build.cake @@ -161,7 +161,8 @@ Task("CreateReleaseNotes") var releaseHeader = string.Format(System.IO.File.ReadAllText("./ReleaseNotes.md"), releaseVersion, lastRelease); releaseNotes = new List { releaseHeader }; - var shortlogSummary = GitHelper($"shortlog --no-merges --numbered --summary {lastRelease}..HEAD"); + var shortlogSummary = GitHelper($"shortlog --no-merges --numbered --summary {lastRelease}..HEAD") + .ToList(); var re = new Regex(@"^[\s\t]*(?'commits'\d+)[\s\t]+(?'author'.*)$"); var summary = shortlogSummary .Where(x => re.IsMatch(x)) @@ -207,7 +208,6 @@ Task("CreateReleaseNotes") static string HonorForDeletions(string place, string author, int commits, int files, int insertions, int deletions) => HonorForInsertions(place, author, commits, files, insertions, $"and **{deletions}** deletion{Plural(deletions)}"); - var statistics = new List<(string Contributor, int Files, int Insertions, int Deletions)>(); foreach (var group in commitsGrouping) { if (topContributors.Count >= top3) break; @@ -220,6 +220,7 @@ Task("CreateReleaseNotes") } else // multiple candidates with the same number of commits, so, group by files changed { + var statistics = new List<(string Contributor, int Files, int Insertions, int Deletions)>(); var shortstatRegex = new Regex(@"^\s*(?'files'\d+)\s+files?\s+changed(?'ins',\s+(?'insertions'\d+)\s+insertions?\(\+\))?(?'del',\s+(?'deletions'\d+)\s+deletions?\(\-\))?\s*$"); // Collect statistics from git log & shortlog foreach (var author in group.authors) @@ -315,15 +316,15 @@ private void WriteReleaseNotes() Information($"RUN {nameof(WriteReleaseNotes)} ..."); EnsureDirectoryExists(packagesDir); - System.IO.File.WriteAllLines(releaseNotesFile, releaseNotes); + System.IO.File.WriteAllLines(releaseNotesFile, releaseNotes, Encoding.UTF8); - var content = System.IO.File.ReadAllText(releaseNotesFile); + var content = System.IO.File.ReadAllText(releaseNotesFile, Encoding.UTF8); if (string.IsNullOrEmpty(content)) { System.IO.File.WriteAllText(releaseNotesFile, "No commits since last release"); } - Information($"Release notes are >>>\n{content}<<<"); + Information("Release notes are >>>\n{0}<<<", content); Information($"EXITED {nameof(WriteReleaseNotes)}"); } diff --git a/docs/conf.py b/docs/conf.py index 8bd28ecbf..dca77bb40 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ project = 'Ocelot' copyright = ' 2023 ThreeMammals Ocelot team' author = 'Tom Pallister, Ocelot Core team at ThreeMammals' -release = '21.0' +release = '22.0' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/docs/features/logging.rst b/docs/features/logging.rst index a8bc91336..037502db3 100644 --- a/docs/features/logging.rst +++ b/docs/features/logging.rst @@ -24,6 +24,8 @@ Every log record has these 2 properties: As an ``IOcelotLogger`` interface object being injected to constructors of service classes, current default Ocelot logger (``OcelotLogger`` class) reads these 2 properties from the ``IRequestScopedDataRepository`` interface object. Find out more about these properties and other details on the *Request ID* logging feature in the :doc:`../features/requestid` chapter. +.. _logging-warning: + Warning ------- @@ -34,7 +36,9 @@ The team has had so many issues about performance issues with Ocelot and it is a * Use ``Error`` and ``Critical`` levels in production environment! * Use ``Warning`` level in testing & staging environments! -These and other recommendations are below in the `Best Practices <#best-practices>`_ section. +These and other recommendations are below in the :ref:`logging-best-practices` section. + +.. _logging-best-practices: Best Practices -------------- @@ -88,7 +92,7 @@ Second ^^^^^^ Ensure proper usage of minimum logging level for each environment: development, testing, production, etc. -So, once again, read important notes of the `Warning <#warning>`_ section! +So, once again, read important notes of the :ref:`logging-warning` section! Third ^^^^^ diff --git a/docs/index.rst b/docs/index.rst index 3ec283703..87d0afa5f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,4 +1,4 @@ -Welcome to Ocelot 21.0 +Welcome to Ocelot 22.0 ====================== Thanks for taking a look at the Ocelot documentation! Please use the left hand navigation to get around. From 349825f92c8c51fd8973aa20448643ae3d049220 Mon Sep 17 00:00:00 2001 From: raman-m Date: Tue, 28 Nov 2023 15:45:28 +0300 Subject: [PATCH 12/12] Switch off the PublishToNuget task --- build.cake | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/build.cake b/build.cake index 5047c5acc..1e8f4a032 100644 --- a/build.cake +++ b/build.cake @@ -511,10 +511,11 @@ Task("PublishToNuget") .IsDependentOn("DownloadGitHubReleaseArtifacts") .Does(() => { - if (IsRunningOnCircleCI()) - { - PublishPackages(packagesDir, artifactsFile, nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl); - } + Information("Skipping of publishing to NuGet..."); + // if (IsRunningOnCircleCI()) + // { + // PublishPackages(packagesDir, artifactsFile, nugetFeedStableKey, nugetFeedStableUploadUrl, nugetFeedStableSymbolsUploadUrl); + // } }); RunTarget(target);