From 8cf8305f689448a5e89572dd4d33b74c8dc38c08 Mon Sep 17 00:00:00 2001 From: Van Tran Date: Wed, 6 Dec 2023 13:45:29 +0700 Subject: [PATCH 01/32] feat(configuration): adding route metadata --- .../Builder/DownstreamRouteBuilder.cs | 12 ++- .../Configuration/Creator/DynamicsCreator.cs | 11 ++- .../Configuration/Creator/IMetadataCreator.cs | 8 ++ .../Configuration/Creator/MetadataCreator.cs | 31 +++++++ .../Configuration/Creator/RoutesCreator.cs | 8 +- src/Ocelot/Configuration/DownstreamRoute.cs | 5 +- .../Configuration/File/FileDynamicRoute.cs | 1 + .../File/FileGlobalConfiguration.cs | 3 + src/Ocelot/Configuration/File/FileRoute.cs | 7 +- .../DependencyInjection/OcelotBuilder.cs | 1 + .../AdministrationTests.cs | 2 +- .../CustomOcelotMiddleware.cs | 34 +++++++ test/Ocelot.ManualTest/Program.cs | 5 +- test/Ocelot.ManualTest/appsettings.json | 3 +- test/Ocelot.ManualTest/ocelot.json | 15 +++ .../Configuration/DynamicsCreatorTests.cs | 39 +++++++- .../Configuration/MetadataCreatorTests.cs | 93 +++++++++++++++++++ .../Configuration/RoutesCreatorTests.cs | 21 ++++- 18 files changed, 287 insertions(+), 12 deletions(-) create mode 100644 src/Ocelot/Configuration/Creator/IMetadataCreator.cs create mode 100644 src/Ocelot/Configuration/Creator/MetadataCreator.cs create mode 100644 test/Ocelot.ManualTest/CustomOcelotMiddleware.cs create mode 100644 test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs diff --git a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs index 6a4426700..1882555f8 100644 --- a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs @@ -1,5 +1,6 @@ using Ocelot.Configuration.Creator; using Ocelot.Values; +using System.Collections.Generic; namespace Ocelot.Configuration.Builder; @@ -42,6 +43,7 @@ public class DownstreamRouteBuilder private Version _downstreamHttpVersion; private HttpVersionPolicy _downstreamHttpVersionPolicy; private Dictionary _upstreamHeaders; + private Dictionary _metadata; public DownstreamRouteBuilder() { @@ -49,6 +51,7 @@ public DownstreamRouteBuilder() _delegatingHandlers = new(); _addHeadersToDownstream = new(); _addHeadersToUpstream = new(); + _metadata = new(); } public DownstreamRouteBuilder WithDownstreamAddresses(List downstreamAddresses) @@ -275,6 +278,12 @@ public DownstreamRouteBuilder WithDownstreamHttpVersionPolicy(HttpVersionPolicy return this; } + public DownstreamRouteBuilder WithMetadata(Dictionary metadata) + { + _metadata = new(metadata); + return this; + } + public DownstreamRoute Build() { return new DownstreamRoute( @@ -313,6 +322,7 @@ public DownstreamRoute Build() _downstreamHttpMethod, _downstreamHttpVersion, _downstreamHttpVersionPolicy, - _upstreamHeaders); + _upstreamHeaders, + _metadata); } } diff --git a/src/Ocelot/Configuration/Creator/DynamicsCreator.cs b/src/Ocelot/Configuration/Creator/DynamicsCreator.cs index 36d591ab9..803b06b14 100644 --- a/src/Ocelot/Configuration/Creator/DynamicsCreator.cs +++ b/src/Ocelot/Configuration/Creator/DynamicsCreator.cs @@ -8,12 +8,18 @@ public class DynamicsCreator : IDynamicsCreator private readonly IRateLimitOptionsCreator _rateLimitOptionsCreator; private readonly IVersionCreator _versionCreator; private readonly IVersionPolicyCreator _versionPolicyCreator; + private readonly IMetadataCreator _metadataCreator; - public DynamicsCreator(IRateLimitOptionsCreator rateLimitOptionsCreator, IVersionCreator versionCreator, IVersionPolicyCreator versionPolicyCreator) + public DynamicsCreator( + IRateLimitOptionsCreator rateLimitOptionsCreator, + IVersionCreator versionCreator, + IVersionPolicyCreator versionPolicyCreator, + IMetadataCreator metadataCreator) { _rateLimitOptionsCreator = rateLimitOptionsCreator; _versionCreator = versionCreator; _versionPolicyCreator = versionPolicyCreator; + _metadataCreator = metadataCreator; } public List Create(FileConfiguration fileConfiguration) @@ -31,12 +37,15 @@ private Route SetUpDynamicRoute(FileDynamicRoute fileDynamicRoute, FileGlobalCon var version = _versionCreator.Create(fileDynamicRoute.DownstreamHttpVersion); var versionPolicy = _versionPolicyCreator.Create(fileDynamicRoute.DownstreamHttpVersionPolicy); + var metadata = _metadataCreator.Create(fileDynamicRoute.Metadata, globalConfiguration); + var downstreamRoute = new DownstreamRouteBuilder() .WithEnableRateLimiting(rateLimitOption.EnableRateLimiting) .WithRateLimitOptions(rateLimitOption) .WithServiceName(fileDynamicRoute.ServiceName) .WithDownstreamHttpVersion(version) .WithDownstreamHttpVersionPolicy(versionPolicy) + .WithMetadata(metadata) .Build(); var route = new RouteBuilder() diff --git a/src/Ocelot/Configuration/Creator/IMetadataCreator.cs b/src/Ocelot/Configuration/Creator/IMetadataCreator.cs new file mode 100644 index 000000000..246c2a929 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/IMetadataCreator.cs @@ -0,0 +1,8 @@ +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator; + +public interface IMetadataCreator +{ + Dictionary Create(Dictionary metadata, FileGlobalConfiguration fileGlobalConfiguration); +} diff --git a/src/Ocelot/Configuration/Creator/MetadataCreator.cs b/src/Ocelot/Configuration/Creator/MetadataCreator.cs new file mode 100644 index 000000000..6d9ffe520 --- /dev/null +++ b/src/Ocelot/Configuration/Creator/MetadataCreator.cs @@ -0,0 +1,31 @@ +using Ocelot.Configuration.File; + +namespace Ocelot.Configuration.Creator; + +public class MetadataCreator : IMetadataCreator +{ + public Dictionary Create(Dictionary routeMetadata, FileGlobalConfiguration fileGlobalConfiguration) + { + var metadata = fileGlobalConfiguration?.Metadata != null + ? new Dictionary(fileGlobalConfiguration.Metadata) + : new(); + + if (routeMetadata != null) + { + foreach (var (key, value) in routeMetadata) + { + if (metadata.ContainsKey(key)) + { + // Replace the global value by the one in file route + metadata[key] = value; + } + else + { + metadata.Add(key, value); + } + } + } + + return metadata; + } +} diff --git a/src/Ocelot/Configuration/Creator/RoutesCreator.cs b/src/Ocelot/Configuration/Creator/RoutesCreator.cs index cbfbc754b..a5fe6d8a5 100644 --- a/src/Ocelot/Configuration/Creator/RoutesCreator.cs +++ b/src/Ocelot/Configuration/Creator/RoutesCreator.cs @@ -23,6 +23,7 @@ public class RoutesCreator : IRoutesCreator private readonly ISecurityOptionsCreator _securityOptionsCreator; private readonly IVersionCreator _versionCreator; private readonly IVersionPolicyCreator _versionPolicyCreator; + private readonly IMetadataCreator _metadataCreator; public RoutesCreator( IClaimsToThingCreator claimsToThingCreator, @@ -41,7 +42,8 @@ public RoutesCreator( ISecurityOptionsCreator securityOptionsCreator, IVersionCreator versionCreator, IVersionPolicyCreator versionPolicyCreator, - IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator) + IUpstreamHeaderTemplatePatternCreator upstreamHeaderTemplatePatternCreator, + IMetadataCreator metadataCreator) { _routeKeyCreator = routeKeyCreator; _loadBalancerOptionsCreator = loadBalancerOptionsCreator; @@ -61,6 +63,7 @@ public RoutesCreator( _versionCreator = versionCreator; _versionPolicyCreator = versionPolicyCreator; _upstreamHeaderTemplatePatternCreator = upstreamHeaderTemplatePatternCreator; + _metadataCreator = metadataCreator; } public List Create(FileConfiguration fileConfiguration) @@ -114,6 +117,8 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf var downstreamHttpVersionPolicy = _versionPolicyCreator.Create(fileRoute.DownstreamHttpVersionPolicy); + var metadata = _metadataCreator.Create(fileRoute.Metadata, globalConfiguration); + var route = new DownstreamRouteBuilder() .WithKey(fileRoute.Key) .WithDownstreamPathTemplate(fileRoute.DownstreamPathTemplate) @@ -151,6 +156,7 @@ private DownstreamRoute SetUpDownstreamRoute(FileRoute fileRoute, FileGlobalConf .WithDownstreamHttpVersion(downstreamHttpVersion) .WithDownstreamHttpVersionPolicy(downstreamHttpVersionPolicy) .WithDownStreamHttpMethod(fileRoute.DownstreamHttpMethod) + .WithMetadata(metadata) .Build(); return route; diff --git a/src/Ocelot/Configuration/DownstreamRoute.cs b/src/Ocelot/Configuration/DownstreamRoute.cs index ab4b6d5de..a000bd45a 100644 --- a/src/Ocelot/Configuration/DownstreamRoute.cs +++ b/src/Ocelot/Configuration/DownstreamRoute.cs @@ -41,7 +41,8 @@ public DownstreamRoute( string downstreamHttpMethod, Version downstreamHttpVersion, HttpVersionPolicy downstreamHttpVersionPolicy, - Dictionary upstreamHeaders) + Dictionary upstreamHeaders, + Dictionary metadata) { DangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator; AddHeadersToDownstream = addHeadersToDownstream; @@ -79,6 +80,7 @@ public DownstreamRoute( DownstreamHttpVersion = downstreamHttpVersion; DownstreamHttpVersionPolicy = downstreamHttpVersionPolicy; UpstreamHeaders = upstreamHeaders ?? new(); + Metadata = metadata; } public string Key { get; } @@ -128,5 +130,6 @@ public DownstreamRoute( /// public HttpVersionPolicy DownstreamHttpVersionPolicy { get; } public Dictionary UpstreamHeaders { get; } + public Dictionary Metadata { get; } } } diff --git a/src/Ocelot/Configuration/File/FileDynamicRoute.cs b/src/Ocelot/Configuration/File/FileDynamicRoute.cs index 21ad814af..bddea1b98 100644 --- a/src/Ocelot/Configuration/File/FileDynamicRoute.cs +++ b/src/Ocelot/Configuration/File/FileDynamicRoute.cs @@ -19,5 +19,6 @@ public class FileDynamicRoute /// /// public string DownstreamHttpVersionPolicy { get; set; } + public Dictionary Metadata { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs index 761bcdc59..ec28d19fa 100644 --- a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs +++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs @@ -11,6 +11,7 @@ public FileGlobalConfiguration() LoadBalancerOptions = new FileLoadBalancerOptions(); QoSOptions = new FileQoSOptions(); HttpHandlerOptions = new FileHttpHandlerOptions(); + Metadata = new Dictionary(); } public string RequestIdKey { get; set; } @@ -42,5 +43,7 @@ public FileGlobalConfiguration() /// /// public string DownstreamHttpVersionPolicy { get; set; } + + public Dictionary Metadata { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileRoute.cs b/src/Ocelot/Configuration/File/FileRoute.cs index 2d20c2168..e27b3ca52 100644 --- a/src/Ocelot/Configuration/File/FileRoute.cs +++ b/src/Ocelot/Configuration/File/FileRoute.cs @@ -25,6 +25,7 @@ public FileRoute() UpstreamHeaderTemplates = new Dictionary(); UpstreamHeaderTransform = new Dictionary(); UpstreamHttpMethod = new List(); + Metadata = new Dictionary(); } public FileRoute(FileRoute from) @@ -56,7 +57,7 @@ public FileRoute(FileRoute from) /// public string DownstreamHttpVersionPolicy { get; set; } public string DownstreamPathTemplate { get; set; } - public string DownstreamScheme { get; set; } + public string DownstreamScheme { get; set; } public FileCacheOptions FileCacheOptions { get; set; } public FileHttpHandlerOptions HttpHandlerOptions { get; set; } public string Key { get; set; } @@ -76,6 +77,7 @@ public FileRoute(FileRoute from) public List UpstreamHttpMethod { get; set; } public string UpstreamPathTemplate { get; set; } public IDictionary UpstreamHeaderTemplates { get; set; } + public Dictionary Metadata { get; set; } /// /// Clones this object by making a deep copy. @@ -103,7 +105,7 @@ public static void DeepCopy(FileRoute from, FileRoute to) to.DownstreamHttpVersion = from.DownstreamHttpVersion; to.DownstreamHttpVersionPolicy = from.DownstreamHttpVersionPolicy; to.DownstreamPathTemplate = from.DownstreamPathTemplate; - to.DownstreamScheme = from.DownstreamScheme; + to.DownstreamScheme = from.DownstreamScheme; to.FileCacheOptions = new(from.FileCacheOptions); to.HttpHandlerOptions = new(from.HttpHandlerOptions); to.Key = from.Key; @@ -123,6 +125,7 @@ public static void DeepCopy(FileRoute from, FileRoute to) to.UpstreamHost = from.UpstreamHost; to.UpstreamHttpMethod = new(from.UpstreamHttpMethod); to.UpstreamPathTemplate = from.UpstreamPathTemplate; + to.Metadata = new(from.Metadata); } } } diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 3d8cb2756..312302bea 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -117,6 +117,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton, OcelotConfigurationMonitor>(); + Services.TryAddSingleton(); Services.AddOcelotMessageInvokerPool(); // See this for why we register this as singleton: diff --git a/test/Ocelot.IntegrationTests/AdministrationTests.cs b/test/Ocelot.IntegrationTests/AdministrationTests.cs index 8bb263090..22d2d965f 100644 --- a/test/Ocelot.IntegrationTests/AdministrationTests.cs +++ b/test/Ocelot.IntegrationTests/AdministrationTests.cs @@ -118,7 +118,7 @@ public void Should_return_OK_status_and_multiline_indented_json_response_with_js .Then(x => ThenTheResultHaveMultiLineIndentedJson()) .BDDfy(); } - + [Fact] public void Should_be_able_to_use_token_from_ocelot_a_on_ocelot_b() { diff --git a/test/Ocelot.ManualTest/CustomOcelotMiddleware.cs b/test/Ocelot.ManualTest/CustomOcelotMiddleware.cs new file mode 100644 index 000000000..6370956d7 --- /dev/null +++ b/test/Ocelot.ManualTest/CustomOcelotMiddleware.cs @@ -0,0 +1,34 @@ +using Ocelot.Logging; +using Ocelot.Middleware; +using System.Text.Json; +using System.Threading.Tasks; + +namespace Ocelot.ManualTest; + +public static class CustomOcelotMiddleware +{ + public static Task Invoke(HttpContext context, Func next) + { + var logger = GetLogger(context); + var downstreamRoute = context.Items.DownstreamRoute(); + + if (downstreamRoute?.Metadata is { } metadata) + { + logger.LogInformation(() => + { + var metadataInJson = JsonSerializer.Serialize(metadata); + var message = $"My custom middleware found some metadata: {metadataInJson}"; + return message; + }); + } + + return next(); + } + + private static IOcelotLogger GetLogger(HttpContext context) + { + var loggerFactory = context.RequestServices.GetRequiredService(); + var logger = loggerFactory.CreateLogger(); + return logger; + } +} diff --git a/test/Ocelot.ManualTest/Program.cs b/test/Ocelot.ManualTest/Program.cs index 135e7e2c7..eaab89ec9 100644 --- a/test/Ocelot.ManualTest/Program.cs +++ b/test/Ocelot.ManualTest/Program.cs @@ -55,7 +55,10 @@ public static void Main(string[] args) .UseIISIntegration() .Configure(app => { - app.UseOcelot().Wait(); + app.UseOcelot(options => + { + options.PreAuthenticationMiddleware = CustomOcelotMiddleware.Invoke; + }).Wait(); }) .Build() .Run(); diff --git a/test/Ocelot.ManualTest/appsettings.json b/test/Ocelot.ManualTest/appsettings.json index e2192fccf..87d3f31b3 100644 --- a/test/Ocelot.ManualTest/appsettings.json +++ b/test/Ocelot.ManualTest/appsettings.json @@ -4,7 +4,8 @@ "LogLevel": { "Default": "Error", "System": "Error", - "Microsoft": "Error" + "Microsoft": "Error", + "Ocelot": "Information" } }, "eureka": { diff --git a/test/Ocelot.ManualTest/ocelot.json b/test/Ocelot.ManualTest/ocelot.json index df8b1f976..8eb947163 100644 --- a/test/Ocelot.ManualTest/ocelot.json +++ b/test/Ocelot.ManualTest/ocelot.json @@ -336,6 +336,21 @@ ], "UpstreamPathTemplate": "/bbc/", "UpstreamHttpMethod": [ "Get" ] + }, + { + "DownstreamPathTemplate": "/posts", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "jsonplaceholder.typicode.com", + "Port": 443 + } + ], + "UpstreamPathTemplate": "/list-post", + "UpstreamHttpMethod": [ "GET" ], + "Metadata": { + "api_id": "e99d7ce0-d918-443e-b243-1960a8212b5d" + } } ], diff --git a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs index 63c7c5237..b0965584f 100644 --- a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs @@ -2,7 +2,7 @@ using Ocelot.Configuration.Builder; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; - + namespace Ocelot.UnitTests.Configuration { public class DynamicsCreatorTests : UnitTest @@ -11,17 +11,20 @@ public class DynamicsCreatorTests : UnitTest private readonly Mock _rloCreator; private readonly Mock _versionCreator; private readonly Mock _versionPolicyCreator; + private readonly Mock _metadataCreator; private List _result; private FileConfiguration _fileConfig; private RateLimitOptions _rlo1; private RateLimitOptions _rlo2; private Version _version; private HttpVersionPolicy _versionPolicy; + private Dictionary _expectedMetadata; public DynamicsCreatorTests() { _versionCreator = new Mock(); _versionPolicyCreator = new Mock(); + _metadataCreator = new Mock(); _rloCreator = new Mock(); _creator = new DynamicsCreator(_rloCreator.Object, _versionCreator.Object, _versionPolicyCreator.Object); } @@ -35,6 +38,7 @@ public void should_return_nothing() .When(_ => WhenICreate()) .Then(_ => ThenNothingIsReturned()) .And(_ => ThenTheRloCreatorIsNotCalled()) + .And(_ => ThenTheMetadataCreatorIsNotCalled()) .BDDfy(); } @@ -54,6 +58,10 @@ public void should_return_re_routes() }, DownstreamHttpVersion = "1.1", DownstreamHttpVersionPolicy = VersionPolicies.RequestVersionOrLower, + Metadata = new() + { + ["foo"] = "bar", + }, }, new() { @@ -64,6 +72,10 @@ public void should_return_re_routes() }, DownstreamHttpVersion = "2.0", DownstreamHttpVersionPolicy = VersionPolicies.RequestVersionOrHigher, + Metadata = new() + { + ["foo"] = "baz", + }, }, }, }; @@ -72,10 +84,12 @@ public void should_return_re_routes() .And(_ => GivenTheRloCreatorReturns()) .And(_ => GivenTheVersionCreatorReturns()) .And(_ => GivenTheVersionPolicyCreatorReturns()) + .And(_ => GivenTheMetadataCreatorReturns()) .When(_ => WhenICreate()) .Then(_ => ThenTheRoutesAreReturned()) .And(_ => ThenTheRloCreatorIsCalledCorrectly()) .And(_ => ThenTheVersionCreatorIsCalledCorrectly()) + .And(_ => ThenTheMetadataCreatorIsCalledCorrectly()) .BDDfy(); } @@ -97,6 +111,12 @@ private void ThenTheVersionCreatorIsCalledCorrectly() _versionPolicyCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[1].DownstreamHttpVersionPolicy), Times.Once); } + private void ThenTheMetadataCreatorIsCalledCorrectly() + { + _metadataCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].Metadata, It.IsAny()), Times.Once); + _metadataCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[1].Metadata, It.IsAny()), Times.Once); + } + private void ThenTheRoutesAreReturned() { _result.Count.ShouldBe(2); @@ -118,13 +138,23 @@ private void GivenTheVersionCreatorReturns() _version = new Version("1.1"); _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_version); } - + private void GivenTheVersionPolicyCreatorReturns() { _versionPolicy = HttpVersionPolicy.RequestVersionOrLower; _versionPolicyCreator.Setup(x => x.Create(It.IsAny())).Returns(_versionPolicy); } + private void GivenTheMetadataCreatorReturns() + { + _expectedMetadata = new() + { + ["foo"] = "bar", + }; + _metadataCreator.Setup(x => x.Create(It.IsAny>(), It.IsAny())) + .Returns(_expectedMetadata); + } + private void GivenTheRloCreatorReturns() { _rlo1 = new RateLimitOptionsBuilder().Build(); @@ -141,6 +171,11 @@ private void ThenTheRloCreatorIsNotCalled() _rloCreator.Verify(x => x.Create(It.IsAny(), It.IsAny()), Times.Never); } + private void ThenTheMetadataCreatorIsNotCalled() + { + _metadataCreator.Verify(x => x.Create(It.IsAny>(), It.IsAny()), Times.Never); + } + private void ThenNothingIsReturned() { _result.Count.ShouldBe(0); diff --git a/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs new file mode 100644 index 000000000..2aa054c56 --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs @@ -0,0 +1,93 @@ +using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; + +namespace Ocelot.UnitTests.Configuration; + +public class MetadataCreatorTests +{ + private FileGlobalConfiguration _globalConfiguration; + private Dictionary _metadataInRoute; + private Dictionary _result; + private readonly MetadataCreator _sut = new(); + + [Fact] + public void should_return_empty_metadata() + { + this.Given(_ => GivenEmptyMetadataInGlobalConfiguration()) + .Given(_ => GivenEmptyMetadataInRoute()) + .When(_ => WhenICreate()) + .Then(_ => ThenDownstreamRouteMetadataMustBeEmpty()); + } + + [Fact] + public void should_return_global_metadata() + { + this.Given(_ => GivenSomeMetadataInGlobalConfiguration()) + .Given(_ => GivenEmptyMetadataInRoute()) + .When(_ => WhenICreate()) + .Then(_ => ThenDownstreamMetadataMustContain("foo", "bar")); + } + + [Fact] + public void should_return_route_metadata() + { + this.Given(_ => GivenEmptyMetadataInGlobalConfiguration()) + .Given(_ => GivenSomeMetadataInRoute()) + .When(_ => WhenICreate()) + .Then(_ => ThenDownstreamMetadataMustContain("foo", "baz")); + } + + [Fact] + public void should_overwrite_global_metadata() + { + this.Given(_ => GivenSomeMetadataInGlobalConfiguration()) + .Given(_ => GivenSomeMetadataInRoute()) + .When(_ => WhenICreate()) + .Then(_ => ThenDownstreamMetadataMustContain("foo", "baz")); + } + + private void WhenICreate() + { + _result = _sut.Create(_metadataInRoute, _globalConfiguration); + } + + private void GivenEmptyMetadataInGlobalConfiguration() + { + _globalConfiguration = new FileGlobalConfiguration(); + } + + private void GivenSomeMetadataInGlobalConfiguration() + { + _globalConfiguration = new FileGlobalConfiguration() + { + Metadata = new() + { + ["foo"] = "bar", + }, + }; + } + + private void GivenEmptyMetadataInRoute() + { + _metadataInRoute = new(); + } + + private void GivenSomeMetadataInRoute() + { + _metadataInRoute = new() + { + ["foo"] = "baz", + }; + } + + private void ThenDownstreamRouteMetadataMustBeEmpty() + { + _result.Keys.ShouldBeEmpty(); + } + + private void ThenDownstreamMetadataMustContain(string key, string value) + { + _result.Keys.ShouldContain(key); + _result[key].ShouldBeEquivalentTo(value); + } +} diff --git a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs index 9a4aab834..b7342aa63 100644 --- a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs @@ -27,6 +27,7 @@ public class RoutesCreatorTests : UnitTest private readonly Mock _soCreator; private readonly Mock _versionCreator; private readonly Mock _versionPolicyCreator; + private readonly Mock _metadataCreator; private FileConfiguration _fileConfig; private RouteOptions _rro; private string _requestId; @@ -45,6 +46,7 @@ public class RoutesCreatorTests : UnitTest private Version _expectedVersion; private HttpVersionPolicy _expectedVersionPolicy; private Dictionary _uht; + private Dictionary _expectedMetadata; public RoutesCreatorTests() { @@ -65,6 +67,7 @@ public RoutesCreatorTests() _versionCreator = new Mock(); _versionPolicyCreator = new Mock(); _uhtpCreator = new Mock(); + _metadataCreator = new Mock(); _creator = new RoutesCreator( _cthCreator.Object, @@ -83,7 +86,8 @@ public RoutesCreatorTests() _soCreator.Object, _versionCreator.Object, _versionPolicyCreator.Object, - _uhtpCreator.Object); + _uhtpCreator.Object, + _metadataCreator.Object); } [Fact] @@ -121,6 +125,10 @@ public void should_return_re_routes() { "e","f" }, }, UpstreamHttpMethod = new List { "GET", "POST" }, + Metadata = new() + { + ["foo"] = "bar", + }, }, new() { @@ -139,6 +147,10 @@ public void should_return_re_routes() { "k","l" }, }, UpstreamHttpMethod = new List { "PUT", "DELETE" }, + Metadata = new() + { + ["foo"] = "baz", + }, }, }, }; @@ -175,6 +187,10 @@ private void GivenTheDependenciesAreSetUpCorrectly() _dhp = new List(); _lbo = new LoadBalancerOptionsBuilder().Build(); _uht = new Dictionary(); + _expectedMetadata = new Dictionary() + { + ["foo"] = "bar", + }; _rroCreator.Setup(x => x.Create(It.IsAny())).Returns(_rro); _ridkCreator.Setup(x => x.Create(It.IsAny(), It.IsAny())).Returns(_requestId); @@ -192,6 +208,7 @@ private void GivenTheDependenciesAreSetUpCorrectly() _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_expectedVersion); _versionPolicyCreator.Setup(x => x.Create(It.IsAny())).Returns(_expectedVersionPolicy); _uhtpCreator.Setup(x => x.Create(It.IsAny())).Returns(_uht); + _metadataCreator.Setup(x => x.Create(It.IsAny>(), It.IsAny())).Returns(_expectedMetadata); } private void ThenTheRoutesAreCreated() @@ -251,6 +268,7 @@ private void ThenTheRouteIsSet(FileRoute expected, int routeIndex) _result[routeIndex].DownstreamRoute[0].RouteClaimsRequirement.ShouldBe(expected.RouteClaimsRequirement); _result[routeIndex].DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe(expected.DownstreamPathTemplate); _result[routeIndex].DownstreamRoute[0].Key.ShouldBe(expected.Key); + _result[routeIndex].DownstreamRoute[0].Metadata.ShouldBe(_expectedMetadata); _result[routeIndex].UpstreamHttpMethod .Select(x => x.Method) .ToList() @@ -283,6 +301,7 @@ private void ThenTheDepsAreCalledFor(FileRoute fileRoute, FileGlobalConfiguratio _daCreator.Verify(x => x.Create(fileRoute), Times.Once); _lboCreator.Verify(x => x.Create(fileRoute.LoadBalancerOptions), Times.Once); _soCreator.Verify(x => x.Create(fileRoute.SecurityOptions), Times.Once); + _metadataCreator.Verify(x => x.Create(fileRoute.Metadata, globalConfig), Times.Once); } } } From d5eae05ea9c81990d35cfd2b6bfce1247a138f44 Mon Sep 17 00:00:00 2001 From: Van Tran Date: Thu, 7 Dec 2023 23:29:25 +0700 Subject: [PATCH 02/32] feat(configuration): update docs --- docs/features/configuration.rst | 61 ++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/docs/features/configuration.rst b/docs/features/configuration.rst index 6fd253404..bf575e12c 100644 --- a/docs/features/configuration.rst +++ b/docs/features/configuration.rst @@ -70,7 +70,8 @@ Here is an example Route configuration. You don't need to set all of these thing "IPAllowedList": [], "IPBlockedList": [], "ExcludeAllowedFromBlocked": false - } + }, + "Metadata": {} } The actual Route schema for properties can be found in the C# `FileRoute `_ class. @@ -485,6 +486,64 @@ You can utilize these methods in the ``ConfigureAppConfiguration`` method (locat You can find additional details in the dedicated :ref:`di-configuration-overview` section and in subsequent sections related to the :doc:`../features/dependencyinjection` chapter. +Route Metadata +-------------- + +Ocelot provides various features such as routing, authentication, caching, load balancing, and more. However, some users may encounter situations where Ocelot does not meet their specific needs or they want to customize its behavior. In such cases, Ocelot allows users to add metadata to the route configuration. This property can store any arbitrary data that users can access in middlewares or delegating handlers. By using the metadata, users can implement their own logic and extend the functionality of Ocelot. + +Here is an example: + +.. code-block:: json + + { + "Routes": [ + { + "UpstreamHttpMethod": [ "GET" ], + "UpstreamPathTemplate": "/posts/{postId}", + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamHostAndPorts": [ + { "Host": "localhost", "Port": 80 } + ], + "Metadata": { + "api-id": "FindPost", + "my-extension/param1": "overwritten-value", + "other-extension/param1": "value1", + "other-extension/param2": "value2", + "tags": "tag1, tag2, area1, area2, func1", + "json": "[1, 2, 3, 4, 5]" + } + } + ], + "GlobalConfiguration": { + "Metadata": { + "instance_name": "dc-1-54abcz", + "my-extension/param1": "default-value" + } + } + } + +Now, the route metadata can be accessed through the `DownstreamRoute` object: + +.. code-block:: csharp + + public static class OcelotMiddlewares + { + public static Task PreAuthenticationMiddleware(HttpContext context, Func next) + { + var downstreamRoute = context.Items.DownstreamRoute(); + + if(downstreamRoute?.Metadata is {} metadata) + { + var param1 = metadata.GetValueOrDefault("my-extension/param1") ?? throw new MyExtensionException("Param 1 is null"); + var param2 = metadata.GetValueOrDefault("my-extension/param2", "custom-value"); + + // working with metadata + } + + return next(); + } + } + """" .. [#f1] ":ref:`config-merging-files`" feature was requested in `issue 296 `_, since then we extended it in `issue 1216 `_ (PR `1227 `_) as ":ref:`config-merging-tomemory`" subfeature which was released as a part of version `23.2`_. From 2bd8d759947448e484b57f94ca2e867d5b4f146a Mon Sep 17 00:00:00 2001 From: Van Tran Date: Mon, 11 Dec 2023 21:38:17 +0700 Subject: [PATCH 03/32] feat(configuration): replace Dictionary<> by IDictionary<>, code cleaning --- .../Builder/DownstreamRouteBuilder.cs | 6 ++--- .../Configuration/Creator/IMetadataCreator.cs | 2 +- .../Configuration/Creator/MetadataCreator.cs | 24 +++++++------------ src/Ocelot/Configuration/DownstreamRoute.cs | 8 +++---- .../Configuration/MetadataCreatorTests.cs | 2 +- 5 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs index 1882555f8..eca7ed353 100644 --- a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs @@ -43,7 +43,7 @@ public class DownstreamRouteBuilder private Version _downstreamHttpVersion; private HttpVersionPolicy _downstreamHttpVersionPolicy; private Dictionary _upstreamHeaders; - private Dictionary _metadata; + private IDictionary _metadata; public DownstreamRouteBuilder() { @@ -278,9 +278,9 @@ public DownstreamRouteBuilder WithDownstreamHttpVersionPolicy(HttpVersionPolicy return this; } - public DownstreamRouteBuilder WithMetadata(Dictionary metadata) + public DownstreamRouteBuilder WithMetadata(IDictionary metadata) { - _metadata = new(metadata); + _metadata = new Dictionary(metadata); return this; } diff --git a/src/Ocelot/Configuration/Creator/IMetadataCreator.cs b/src/Ocelot/Configuration/Creator/IMetadataCreator.cs index 246c2a929..d188c55c9 100644 --- a/src/Ocelot/Configuration/Creator/IMetadataCreator.cs +++ b/src/Ocelot/Configuration/Creator/IMetadataCreator.cs @@ -4,5 +4,5 @@ namespace Ocelot.Configuration.Creator; public interface IMetadataCreator { - Dictionary Create(Dictionary metadata, FileGlobalConfiguration fileGlobalConfiguration); + IDictionary Create(Dictionary metadata, FileGlobalConfiguration fileGlobalConfiguration); } diff --git a/src/Ocelot/Configuration/Creator/MetadataCreator.cs b/src/Ocelot/Configuration/Creator/MetadataCreator.cs index 6d9ffe520..934cb9862 100644 --- a/src/Ocelot/Configuration/Creator/MetadataCreator.cs +++ b/src/Ocelot/Configuration/Creator/MetadataCreator.cs @@ -4,26 +4,20 @@ namespace Ocelot.Configuration.Creator; public class MetadataCreator : IMetadataCreator { - public Dictionary Create(Dictionary routeMetadata, FileGlobalConfiguration fileGlobalConfiguration) + public IDictionary Create(Dictionary routeMetadata, FileGlobalConfiguration fileGlobalConfiguration) { var metadata = fileGlobalConfiguration?.Metadata != null ? new Dictionary(fileGlobalConfiguration.Metadata) - : new(); + : new Dictionary(); - if (routeMetadata != null) + if (routeMetadata == null) { - foreach (var (key, value) in routeMetadata) - { - if (metadata.ContainsKey(key)) - { - // Replace the global value by the one in file route - metadata[key] = value; - } - else - { - metadata.Add(key, value); - } - } + return metadata; + } + + foreach (var (key, value) in routeMetadata) + { + metadata[key] = value; } return metadata; diff --git a/src/Ocelot/Configuration/DownstreamRoute.cs b/src/Ocelot/Configuration/DownstreamRoute.cs index a000bd45a..71799dd1a 100644 --- a/src/Ocelot/Configuration/DownstreamRoute.cs +++ b/src/Ocelot/Configuration/DownstreamRoute.cs @@ -42,7 +42,7 @@ public DownstreamRoute( Version downstreamHttpVersion, HttpVersionPolicy downstreamHttpVersionPolicy, Dictionary upstreamHeaders, - Dictionary metadata) + IDictionary metadata) { DangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator; AddHeadersToDownstream = addHeadersToDownstream; @@ -77,7 +77,7 @@ public DownstreamRoute( AddHeadersToUpstream = addHeadersToUpstream; SecurityOptions = securityOptions; DownstreamHttpMethod = downstreamHttpMethod; - DownstreamHttpVersion = downstreamHttpVersion; + DownstreamHttpVersion = downstreamHttpVersion; DownstreamHttpVersionPolicy = downstreamHttpVersionPolicy; UpstreamHeaders = upstreamHeaders ?? new(); Metadata = metadata; @@ -116,7 +116,7 @@ public DownstreamRoute( public bool DangerousAcceptAnyServerCertificateValidator { get; } public SecurityOptions SecurityOptions { get; } public string DownstreamHttpMethod { get; } - public Version DownstreamHttpVersion { get; } + public Version DownstreamHttpVersion { get; } /// The enum specifies behaviors for selecting and negotiating the HTTP version for a request. /// An enum value being mapped from a constant. @@ -130,6 +130,6 @@ public DownstreamRoute( /// public HttpVersionPolicy DownstreamHttpVersionPolicy { get; } public Dictionary UpstreamHeaders { get; } - public Dictionary Metadata { get; } + public IDictionary Metadata { get; } } } diff --git a/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs index 2aa054c56..a486a0d9f 100644 --- a/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs @@ -7,7 +7,7 @@ public class MetadataCreatorTests { private FileGlobalConfiguration _globalConfiguration; private Dictionary _metadataInRoute; - private Dictionary _result; + private IDictionary _result; private readonly MetadataCreator _sut = new(); [Fact] From b5087d85f6917e9fa50b688c43a9e9e34f777ec8 Mon Sep 17 00:00:00 2001 From: Van Tran Date: Mon, 11 Dec 2023 23:15:50 +0700 Subject: [PATCH 04/32] feat(configuration): replace Dictionary<> by IDictionary<> --- src/Ocelot/Configuration/Creator/IMetadataCreator.cs | 2 +- src/Ocelot/Configuration/Creator/MetadataCreator.cs | 2 +- src/Ocelot/Configuration/File/FileRoute.cs | 6 +++--- .../Configuration/RoutesCreatorTests.cs | 10 +++++----- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Ocelot/Configuration/Creator/IMetadataCreator.cs b/src/Ocelot/Configuration/Creator/IMetadataCreator.cs index d188c55c9..c60f2de6b 100644 --- a/src/Ocelot/Configuration/Creator/IMetadataCreator.cs +++ b/src/Ocelot/Configuration/Creator/IMetadataCreator.cs @@ -4,5 +4,5 @@ namespace Ocelot.Configuration.Creator; public interface IMetadataCreator { - IDictionary Create(Dictionary metadata, FileGlobalConfiguration fileGlobalConfiguration); + IDictionary Create(IDictionary metadata, FileGlobalConfiguration fileGlobalConfiguration); } diff --git a/src/Ocelot/Configuration/Creator/MetadataCreator.cs b/src/Ocelot/Configuration/Creator/MetadataCreator.cs index 934cb9862..5e18b545d 100644 --- a/src/Ocelot/Configuration/Creator/MetadataCreator.cs +++ b/src/Ocelot/Configuration/Creator/MetadataCreator.cs @@ -4,7 +4,7 @@ namespace Ocelot.Configuration.Creator; public class MetadataCreator : IMetadataCreator { - public IDictionary Create(Dictionary routeMetadata, FileGlobalConfiguration fileGlobalConfiguration) + public IDictionary Create(IDictionary routeMetadata, FileGlobalConfiguration fileGlobalConfiguration) { var metadata = fileGlobalConfiguration?.Metadata != null ? new Dictionary(fileGlobalConfiguration.Metadata) diff --git a/src/Ocelot/Configuration/File/FileRoute.cs b/src/Ocelot/Configuration/File/FileRoute.cs index e27b3ca52..e8a05d0c9 100644 --- a/src/Ocelot/Configuration/File/FileRoute.cs +++ b/src/Ocelot/Configuration/File/FileRoute.cs @@ -24,7 +24,7 @@ public FileRoute() SecurityOptions = new FileSecurityOptions(); UpstreamHeaderTemplates = new Dictionary(); UpstreamHeaderTransform = new Dictionary(); - UpstreamHttpMethod = new List(); + UpstreamHttpMethod = new List(); Metadata = new Dictionary(); } @@ -77,7 +77,7 @@ public FileRoute(FileRoute from) public List UpstreamHttpMethod { get; set; } public string UpstreamPathTemplate { get; set; } public IDictionary UpstreamHeaderTemplates { get; set; } - public Dictionary Metadata { get; set; } + public IDictionary Metadata { get; set; } /// /// Clones this object by making a deep copy. @@ -125,7 +125,7 @@ public static void DeepCopy(FileRoute from, FileRoute to) to.UpstreamHost = from.UpstreamHost; to.UpstreamHttpMethod = new(from.UpstreamHttpMethod); to.UpstreamPathTemplate = from.UpstreamPathTemplate; - to.Metadata = new(from.Metadata); + to.Metadata = new Dictionary(from.Metadata); } } } diff --git a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs index b7342aa63..7a5fcc05e 100644 --- a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs @@ -43,7 +43,7 @@ public class RoutesCreatorTests : UnitTest private List _dhp; private LoadBalancerOptions _lbo; private List _result; - private Version _expectedVersion; + private Version _expectedVersion; private HttpVersionPolicy _expectedVersionPolicy; private Dictionary _uht; private Dictionary _expectedMetadata; @@ -124,8 +124,8 @@ public void should_return_re_routes() { { "e","f" }, }, - UpstreamHttpMethod = new List { "GET", "POST" }, - Metadata = new() + UpstreamHttpMethod = new List { "GET", "POST" }, + Metadata = new Dictionary() { ["foo"] = "bar", }, @@ -147,7 +147,7 @@ public void should_return_re_routes() { "k","l" }, }, UpstreamHttpMethod = new List { "PUT", "DELETE" }, - Metadata = new() + Metadata = new Dictionary() { ["foo"] = "baz", }, @@ -300,7 +300,7 @@ private void ThenTheDepsAreCalledFor(FileRoute fileRoute, FileGlobalConfiguratio _hfarCreator.Verify(x => x.Create(fileRoute), Times.Once); _daCreator.Verify(x => x.Create(fileRoute), Times.Once); _lboCreator.Verify(x => x.Create(fileRoute.LoadBalancerOptions), Times.Once); - _soCreator.Verify(x => x.Create(fileRoute.SecurityOptions), Times.Once); + _soCreator.Verify(x => x.Create(fileRoute.SecurityOptions), Times.Once); _metadataCreator.Verify(x => x.Create(fileRoute.Metadata, globalConfig), Times.Once); } } From f132867deab81e0a33a5a616e95235658f34f7c0 Mon Sep 17 00:00:00 2001 From: Van Tran Date: Mon, 11 Dec 2023 23:39:49 +0700 Subject: [PATCH 05/32] feat(configuration): replace Dictionary<> by IDictionary<> --- src/Ocelot/Configuration/File/FileGlobalConfiguration.cs | 2 +- test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs index ec28d19fa..81875ad64 100644 --- a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs +++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs @@ -44,6 +44,6 @@ public FileGlobalConfiguration() /// public string DownstreamHttpVersionPolicy { get; set; } - public Dictionary Metadata { get; set; } + public IDictionary Metadata { get; set; } } } diff --git a/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs index a486a0d9f..5ca6a1874 100644 --- a/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs @@ -60,7 +60,7 @@ private void GivenSomeMetadataInGlobalConfiguration() { _globalConfiguration = new FileGlobalConfiguration() { - Metadata = new() + Metadata = new Dictionary { ["foo"] = "bar", }, @@ -69,12 +69,12 @@ private void GivenSomeMetadataInGlobalConfiguration() private void GivenEmptyMetadataInRoute() { - _metadataInRoute = new(); + _metadataInRoute = new Dictionary(); } private void GivenSomeMetadataInRoute() { - _metadataInRoute = new() + _metadataInRoute = new Dictionary { ["foo"] = "baz", }; From f84327af1152b172aa3f53107ac336f677d951c5 Mon Sep 17 00:00:00 2001 From: Van Tran Date: Mon, 11 Dec 2023 23:44:23 +0700 Subject: [PATCH 06/32] feat(configuration): update the data type of FileDynamicRoute Metadata --- .../Configuration/File/FileDynamicRoute.cs | 2 +- .../Configuration/DynamicsCreatorTests.cs | 20 +++++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/Ocelot/Configuration/File/FileDynamicRoute.cs b/src/Ocelot/Configuration/File/FileDynamicRoute.cs index bddea1b98..a42581531 100644 --- a/src/Ocelot/Configuration/File/FileDynamicRoute.cs +++ b/src/Ocelot/Configuration/File/FileDynamicRoute.cs @@ -19,6 +19,6 @@ public class FileDynamicRoute /// /// public string DownstreamHttpVersionPolicy { get; set; } - public Dictionary Metadata { get; set; } + public IDictionary Metadata { get; set; } } } diff --git a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs index b0965584f..1999e141a 100644 --- a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs @@ -24,7 +24,7 @@ public DynamicsCreatorTests() { _versionCreator = new Mock(); _versionPolicyCreator = new Mock(); - _metadataCreator = new Mock(); + _metadataCreator = new Mock(); _rloCreator = new Mock(); _creator = new DynamicsCreator(_rloCreator.Object, _versionCreator.Object, _versionPolicyCreator.Object); } @@ -56,9 +56,8 @@ public void should_return_re_routes() { EnableRateLimiting = false, }, - DownstreamHttpVersion = "1.1", - DownstreamHttpVersionPolicy = VersionPolicies.RequestVersionOrLower, - Metadata = new() + DownstreamHttpVersion = "1.1", + Metadata = new Dictionary { ["foo"] = "bar", }, @@ -71,8 +70,7 @@ public void should_return_re_routes() EnableRateLimiting = true, }, DownstreamHttpVersion = "2.0", - DownstreamHttpVersionPolicy = VersionPolicies.RequestVersionOrHigher, - Metadata = new() + Metadata = new Dictionary { ["foo"] = "baz", }, @@ -109,8 +107,8 @@ private void ThenTheVersionCreatorIsCalledCorrectly() _versionPolicyCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].DownstreamHttpVersionPolicy), Times.Once); _versionPolicyCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[1].DownstreamHttpVersionPolicy), Times.Once); - } - + } + private void ThenTheMetadataCreatorIsCalledCorrectly() { _metadataCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].Metadata, It.IsAny()), Times.Once); @@ -137,8 +135,8 @@ private void GivenTheVersionCreatorReturns() { _version = new Version("1.1"); _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_version); - } - + } + private void GivenTheVersionPolicyCreatorReturns() { _versionPolicy = HttpVersionPolicy.RequestVersionOrLower; @@ -170,7 +168,7 @@ private void ThenTheRloCreatorIsNotCalled() { _rloCreator.Verify(x => x.Create(It.IsAny(), It.IsAny()), Times.Never); } - + private void ThenTheMetadataCreatorIsNotCalled() { _metadataCreator.Verify(x => x.Create(It.IsAny>(), It.IsAny()), Times.Never); From 3a944440fbc9af57c0edfbb837f5251fa99a60a1 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Tue, 12 Dec 2023 15:43:47 +0300 Subject: [PATCH 07/32] formatting --- src/Ocelot/Configuration/File/FileRoute.cs | 177 +++++++++++---------- 1 file changed, 89 insertions(+), 88 deletions(-) diff --git a/src/Ocelot/Configuration/File/FileRoute.cs b/src/Ocelot/Configuration/File/FileRoute.cs index e8a05d0c9..eccd2c1df 100644 --- a/src/Ocelot/Configuration/File/FileRoute.cs +++ b/src/Ocelot/Configuration/File/FileRoute.cs @@ -1,7 +1,7 @@ using Ocelot.Configuration.Creator; namespace Ocelot.Configuration.File -{ +{ public class FileRoute : IRoute, ICloneable { public FileRoute() @@ -17,6 +17,7 @@ public FileRoute() FileCacheOptions = new FileCacheOptions(); HttpHandlerOptions = new FileHttpHandlerOptions(); LoadBalancerOptions = new FileLoadBalancerOptions(); + Metadata = new Dictionary(); Priority = 1; QoSOptions = new FileQoSOptions(); RateLimitOptions = new FileRateLimitRule(); @@ -24,26 +25,25 @@ public FileRoute() SecurityOptions = new FileSecurityOptions(); UpstreamHeaderTemplates = new Dictionary(); UpstreamHeaderTransform = new Dictionary(); - UpstreamHttpMethod = new List(); - Metadata = new Dictionary(); - } - - 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; } + 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; } /// The enum specifies behaviors for selecting and negotiating the HTTP version for a request. /// A value of defined constants. @@ -56,76 +56,77 @@ public FileRoute(FileRoute from) /// /// public string DownstreamHttpVersionPolicy { 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; } + 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 IDictionary Metadata { 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; } public IDictionary UpstreamHeaderTemplates { get; set; } public IDictionary Metadata { 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; + + /// + /// 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.DownstreamHttpVersionPolicy = from.DownstreamHttpVersionPolicy; - 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.UpstreamHeaderTemplates = new Dictionary(from.UpstreamHeaderTemplates); - to.UpstreamHeaderTransform = new(from.UpstreamHeaderTransform); - to.UpstreamHost = from.UpstreamHost; - to.UpstreamHttpMethod = new(from.UpstreamHttpMethod); - to.UpstreamPathTemplate = from.UpstreamPathTemplate; + 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.Metadata = new Dictionary(from.Metadata); - } + 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.UpstreamHeaderTemplates = new Dictionary(from.UpstreamHeaderTemplates); + to.UpstreamHeaderTransform = new(from.UpstreamHeaderTransform); + to.UpstreamHost = from.UpstreamHost; + to.UpstreamHttpMethod = new(from.UpstreamHttpMethod); + to.UpstreamPathTemplate = from.UpstreamPathTemplate; + } } } From 2b07078d699eff86e5dd0be04da2ee3caf3cf419 Mon Sep 17 00:00:00 2001 From: Van Tran Date: Tue, 19 Mar 2024 01:22:45 +0700 Subject: [PATCH 08/32] feat(configuration): fix integration tests --- test/Ocelot.IntegrationTests/AdministrationTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Ocelot.IntegrationTests/AdministrationTests.cs b/test/Ocelot.IntegrationTests/AdministrationTests.cs index 22d2d965f..b92b5328b 100644 --- a/test/Ocelot.IntegrationTests/AdministrationTests.cs +++ b/test/Ocelot.IntegrationTests/AdministrationTests.cs @@ -878,11 +878,11 @@ private void ThenTheStatusCodeShouldBe(HttpStatusCode expectedHttpStatusCode) { _response.StatusCode.ShouldBe(expectedHttpStatusCode); } - + private void ThenTheResultHaveMultiLineIndentedJson() { const string indent = " "; - const int total = 46, skip = 1; + const int total = 47, skip = 1; var lines = _response.Content.ReadAsStringAsync().Result.Split(Environment.NewLine); lines.Length.ShouldBeGreaterThanOrEqualTo(total); lines.First().ShouldNotStartWith(indent); From 35ec151ce6e12615c765be2bab4d6a0ee062bb3a Mon Sep 17 00:00:00 2001 From: Van Tran Date: Tue, 19 Mar 2024 01:06:21 +0700 Subject: [PATCH 09/32] feat !1843 add extension methods for DownstreamRoute to get metadata --- .../DownstreamRouteExtensions.cs | 87 ++++++ .../DownstreamRouteExtensionsTests.cs | 249 ++++++++++++++++++ 2 files changed, 336 insertions(+) create mode 100644 src/Ocelot/Configuration/DownstreamRouteExtensions.cs create mode 100644 test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs diff --git a/src/Ocelot/Configuration/DownstreamRouteExtensions.cs b/src/Ocelot/Configuration/DownstreamRouteExtensions.cs new file mode 100644 index 000000000..5a911d538 --- /dev/null +++ b/src/Ocelot/Configuration/DownstreamRouteExtensions.cs @@ -0,0 +1,87 @@ +using System.Globalization; +using System.Numerics; +using System.Text.Json; + +namespace Ocelot.Configuration +{ + public static class DownstreamRouteExtensions + { + public static string GetMetadataValue(this DownstreamRoute downstreamRoute, + string key, + string defaultValue = null) + { + var metadata = downstreamRoute?.Metadata; + + if (metadata == null) + { + return defaultValue; + } + + if (!metadata.TryGetValue(key, out string value)) + { + return defaultValue; + } + + return value; + } + +#if NET7_0_OR_GREATER + public static T GetMetadataNumber(this DownstreamRoute downstreamRoute, + string key, + T defaultValue = default, + NumberStyles numberStyles = NumberStyles.Any, + CultureInfo cultureInfo = null) + where T : INumberBase + { + var metadataValue = downstreamRoute.GetMetadataValue(key); + if (metadataValue == null) + { + return defaultValue; + } + + IFormatProvider formatProvider = cultureInfo ?? CultureInfo.CurrentCulture; + return T.Parse(metadataValue, numberStyles, formatProvider); + } +#endif + + public static string[] GetMetadataValues(this DownstreamRoute downstreamRoute, + string key, + string separator = ",", + StringSplitOptions stringSplitOptions = StringSplitOptions.RemoveEmptyEntries, + string trimChars = " ") + { + var metadataValue = downstreamRoute.GetMetadataValue(key); + if (metadataValue == null) + { + return Array.Empty(); + } + + var strings = metadataValue.Split(separator, stringSplitOptions); + char[] trimCharsArray = trimChars.ToCharArray(); + + if (trimCharsArray.Length > 0) + { + for (var i = 0; i < strings.Length; i++) + { + strings[i] = strings[i].Trim(trimCharsArray); + } + } + + return strings.Where(x => x.Length > 0).ToArray(); + } + + public static T GetMetadataFromJson(this DownstreamRoute downstreamRoute, + string key, + T defaultValue = default, + JsonSerializerOptions jsonSerializerOptions = null) + { + var metadataValue = downstreamRoute.GetMetadataValue(key); + if (metadataValue == null) + { + return defaultValue; + } + + return JsonSerializer.Deserialize(metadataValue, jsonSerializerOptions); + } + } +} diff --git a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs new file mode 100644 index 000000000..6f560ce0f --- /dev/null +++ b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs @@ -0,0 +1,249 @@ +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; +using Ocelot.Values; +using System.Numerics; +using System.Text.Json; + +namespace Ocelot.UnitTests.Configuration; + +public class DownstreamRouteExtensionsTests +{ + private readonly Dictionary _metadata; + private readonly DownstreamRoute _downstreamRoute; + + public DownstreamRouteExtensionsTests() + { + _metadata = new Dictionary(); + _downstreamRoute = new DownstreamRoute( + null, + new UpstreamPathTemplate(null, 0, false, null), + new List(), + new List(), + new List(), + null, + null, + new HttpHandlerOptions(false, false, false, false, 0, TimeSpan.Zero), + default, + default, + new QoSOptions(0, 0, 0, null), + null, + null, + default, + new CacheOptions(0, null, null), + new LoadBalancerOptions(null, null, 0), + new RateLimitOptions(false, null, null, false, null, null, null, 0), + new Dictionary(), + new List(), + new List(), + new List(), + new List(), + default, + default, + new AuthenticationOptions(null, null, null), + new DownstreamPathTemplate(null), + null, + new List(), + new List(), + new List(), + default, + new SecurityOptions(), + null, + new Version(), + _metadata); + } + + [Theory] + [InlineData("key1", null)] + [InlineData("hello", "world")] + public void should_return_default_value_when_key_not_found(string key, string defaultValue) + { + // Arrange + _metadata.Add(key, defaultValue); + + // Act + var metadataValue = _downstreamRoute.GetMetadataValue(key, defaultValue); + + // Assert + metadataValue.ShouldBe(defaultValue); + } + + [Theory] + [InlineData("hello", "world")] + [InlineData("object.key", "value1,value2,value3")] + public void should_return_found_metadata_value(string key, string value) + { + // Arrange + _metadata.Add(key, value); + + // Act + var metadataValue = _downstreamRoute.GetMetadataValue(key); + + //Assert + metadataValue.ShouldBe(value); + } + + [Theory] + [InlineData("mykey", "")] + [InlineData("mykey", "value1", "value1")] + [InlineData("mykey", "value1,value2", "value1", "value2")] + [InlineData("mykey", "value1, value2", "value1", "value2")] + [InlineData("mykey", "value1,,,value2", "value1", "value2")] + [InlineData("mykey", "value1, ,value2", "value1", "value2")] + [InlineData("mykey", "value1, value2, value3", "value1", "value2", "value3")] + [InlineData("mykey", ", ,value1, ,, ,,,,,value2,,, ", "value1", "value2")] + public void should_split_strings(string key, string value, params string[] expected) + { + // Arrange + _metadata.Add(key, value); + + // Act + var metadataValue = _downstreamRoute.GetMetadataValues(key); + + //Assert + metadataValue.ShouldBe(expected); + } + + [Fact] + public void should_parse_from_json_null() => should_parse_object_from_json("mykey", "null", null); + + [Fact] + public void should_parse_from_json_string() => should_parse_object_from_json("mykey", "\"string\"", "string"); + + [Fact] + public void should_parse_from_json_numbers() => should_parse_object_from_json("mykey", "123", 123); + + [Fact] + public void should_parse_from_object() + => should_parse_object_from_json( + "mykey", + "{\"Id\": 88, \"Value\": \"Hello World!\", \"MyTime\": \"2024-01-01T10:10:10.000Z\"}", + new FakeObject { Id = 88, Value = "Hello World!", MyTime = new DateTime(2024, 1, 1, 10, 10, 10, DateTimeKind.Unspecified) }); + + private void should_parse_object_from_json(string key, string value, object expected) + { + // Arrange + _metadata.Add(key, value); + + // Act + var metadataValue = _downstreamRoute.GetMetadataFromJson(key); + + //Assert + metadataValue.ShouldBeEquivalentTo(expected); + } + + [Fact] + public void should_parse_from_json_array() + { + // Arrange + var key = "mykey"; + _metadata.Add(key, "[\"value1\", \"value2\", \"value3\"]"); + + // Act + var metadataValue = _downstreamRoute.GetMetadataFromJson>(key); + + //Assert + metadataValue.ShouldNotBeNull(); + metadataValue.ElementAt(0).ShouldBe("value1"); + metadataValue.ElementAt(1).ShouldBe("value2"); + metadataValue.ElementAt(2).ShouldBe("value3"); + } + + [Fact] + public void should_throw_error_when_invalid_json() + { + // Arrange + var key = "mykey"; + _metadata.Add(key, "[[["); + + // Act + + //Assert + Assert.Throws(() => + { + _ = _downstreamRoute.GetMetadataFromJson>(key); + }); + } + + [Fact] + public void should_parse_json_with_custom_json_settings_options() + { + // Arrange + var key = "mykey"; + var value = "{\"id\": 88, \"value\": \"Hello World!\", \"myTime\": \"2024-01-01T10:10:10.000Z\"}"; + var expected = new FakeObject + { + Id = 88, + Value = "Hello World!", + MyTime = new DateTime(2024, 1, 1, 10, 10, 10, DateTimeKind.Unspecified), + }; + var serializerOptions = new JsonSerializerOptions() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }; + _metadata.Add(key, value); + + // Act + var metadataValue = _downstreamRoute.GetMetadataFromJson(key, jsonSerializerOptions: serializerOptions); + + //Assert + metadataValue.ShouldBeEquivalentTo(expected); + } + + record FakeObject + { + public int Id { get; set; } + public string Value { get; set; } + public DateTime MyTime { get; set; } + } + +#if NET7_0_OR_GREATER + + [Theory] + [InlineData("0", 0)] + [InlineData("99", 99)] + [InlineData("500", 500)] + [InlineData("999999999", 999999999)] + public void should_parse_integers(string value, int expected) => should_parse_number(value, expected); + + [Theory] + [InlineData("0", 0)] + [InlineData("0.5", 0.5)] + [InlineData("99", 99)] + [InlineData("99.5", 99.5)] + [InlineData("999999999", 999999999)] + [InlineData("999999999.5", 999999999.5)] + public void should_parse_double(string value, double expected) => should_parse_number(value, expected); + + private void should_parse_number(string value, T expected) + where T : INumberBase + { + // Arrange + var key = "mykey"; + _metadata.Add(key, value); + + // Act + var metadataValue = _downstreamRoute.GetMetadataNumber(key); + + //Assert + metadataValue.ShouldBe(expected); + } + + [Fact] + public void should_throw_error_when_invalid_number() + { + + // Arrange + var key = "mykey"; + _metadata.Add(key, "xyz"); + + // Act + + // Assert + Assert.Throws(() => + { + _ = _downstreamRoute.GetMetadataNumber(key); + }); + } + +#endif +} From 97df8240c270c3650cad19b070917c02ba5b6439 Mon Sep 17 00:00:00 2001 From: Van Tran Date: Tue, 19 Mar 2024 02:25:44 +0700 Subject: [PATCH 10/32] feat !1843 add extension methods for DownstreamRoute --- .../DownstreamRouteExtensions.cs | 17 +++++++++ .../DownstreamRouteExtensionsTests.cs | 35 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/src/Ocelot/Configuration/DownstreamRouteExtensions.cs b/src/Ocelot/Configuration/DownstreamRouteExtensions.cs index 5a911d538..705129a39 100644 --- a/src/Ocelot/Configuration/DownstreamRouteExtensions.cs +++ b/src/Ocelot/Configuration/DownstreamRouteExtensions.cs @@ -83,5 +83,22 @@ public static T GetMetadataFromJson(this DownstreamRoute downstreamRoute, return JsonSerializer.Deserialize(metadataValue, jsonSerializerOptions); } + + public static bool IsMetadataValueTruthy(this DownstreamRoute downstreamRoute, string key) + { + var metadataValue = downstreamRoute.GetMetadataValue(key); + if (metadataValue == null) + { + return false; + } + + var trimmedValue = metadataValue.Trim().ToLower(); + return trimmedValue == "true" || + trimmedValue == "yes" || + trimmedValue == "on" || + trimmedValue == "ok" || + trimmedValue == "enable" || + trimmedValue == "enabled"; + } } } diff --git a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs index 6f560ce0f..d5f055172 100644 --- a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs @@ -246,4 +246,39 @@ public void should_throw_error_when_invalid_number() } #endif + + [Theory] + [InlineData("true", true)] + [InlineData("yes", true)] + [InlineData("on", true)] + [InlineData("enabled", true)] + [InlineData("enable", true)] + [InlineData("ok", true)] + [InlineData(" true ", true)] + [InlineData(" yes ", true)] + [InlineData(" on ", true)] + [InlineData(" enabled ", true)] + [InlineData(" enable ", true)] + [InlineData(" ok ", true)] + [InlineData("", false)] + [InlineData(" ", false)] + [InlineData(null, false)] + [InlineData("false", false)] + [InlineData("off", false)] + [InlineData("disabled", false)] + [InlineData("disable", false)] + [InlineData("no", false)] + [InlineData("abcxyz", false)] + public void should_parse_truthy_values(string value, bool expected) + { + // Arrange + var key = "mykey"; + _metadata.Add(key, value); + + // Act + var isTrusthy = _downstreamRoute.IsMetadataValueTruthy(key); + + //Assert + isTrusthy.ShouldBe(expected); + } } From 775e1adf98e3165dc3afbec1037c9c35f3774577 Mon Sep 17 00:00:00 2001 From: Van Tran Date: Tue, 19 Mar 2024 02:25:53 +0700 Subject: [PATCH 11/32] feat !1843 update docs --- docs/features/metadata.rst | 96 ++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 2 files changed, 97 insertions(+) create mode 100644 docs/features/metadata.rst diff --git a/docs/features/metadata.rst b/docs/features/metadata.rst new file mode 100644 index 000000000..ea71d93a2 --- /dev/null +++ b/docs/features/metadata.rst @@ -0,0 +1,96 @@ +Metadata +======== + +Configuration +------------- + +Ocelot provides various features such as routing, authentication, caching, load +balancing, and more. +However, some users may encounter situations where Ocelot does not meet their +specific needs or they want to customize its behavior. +In such cases, Ocelot allows users to add metadata to the route configuration. +This property can store any arbitrary data that users can access in middlewares +or delegating handlers. + +By using the metadata, users can implement their own logic and extend the +functionality of Ocelot e.g. + +.. code-block:: json + + { + "Routes": [ + { + "UpstreamHttpMethod": [ "GET" ], + "UpstreamPathTemplate": "/posts/{postId}", + "DownstreamPathTemplate": "/api/posts/{postId}", + "DownstreamHostAndPorts": [ + { "Host": "localhost", "Port": 80 } + ], + "Metadata": { + "id": "FindPost", + "tags": "tag1, tag2, area1, area2, func1", + "plugin1.enabled": "true", + "plugin1.values": "[1, 2, 3, 4, 5]", + "plugin1.param": "value2", + "plugin1.param2": "123", + "plugin2/param1": "overwritten-value", + "plugin2/param2": "{\"name\":\"John Doe\",\"age\":30,\"city\":\"New York\",\"is_student\":false,\"hobbies\":[\"reading\",\"hiking\",\"cooking\"]}" + } + } + ], + "GlobalConfiguration": { + "Metadata": { + "instance_name": "machine-1", + "plugin2/param1": "default-value" + } + } + } + +Now, the route metadata can be accessed through the ``DownstreamRoute`` object: + +.. code-block:: csharp + + public class MyMiddleware + { + public Task Invoke(HttpContext context, Func next) + { + var route = context.Items.DownstreamRoute(); + var enabled = metadata.GetMetadataFromJson("plugin1.enabled"); + var values = metadata.GetMetadataValues("plugin1.values"); + var param1 = metadata.GetMetadataValue("plugin1.param", "system-default-value"); + var param2 = metadata.GetMetadataNumber("plugin1.param2"); + + // working on the plugin1's function + + return next?.Invoke(); + } + } + +Extension Methods +----------------- + +Ocelot provides some extension methods help you to retrieve your metadata values effortlessly. + +.. list-table:: + :widths: 20 40 40 + + * - Method + - Description + - Notes + * - ``GetMetadataValue`` + - The metadata value is a string. + - + * - ``GetMetadataValues`` + - The metadata value is spitted by a given separator (default ``,``) and + returned as a string array. + - + * - ``GetMetadataNumber`` + - The metadata value is parsed to a number. + - | Only available in .NET 7 or above. + | For .NET 6, use ``GetMetadataFromJson<>``. + * - ``GetMetadataFromJson`` + - The metadata value is serialized to the given generic type. + - + * - ``IsMetadataValueTruthy`` + - Check if the metadata value is a truthy value. + - The truthy values are: ``true``, ``yes``, ``ok``, ``on``, ``enable``, ``enabled`` diff --git a/docs/index.rst b/docs/index.rst index be0b6deb4..c313c5104 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -113,6 +113,7 @@ Updated Documentation features/kubernetes features/loadbalancer features/logging + features/metadata features/methodtransformation features/middlewareinjection features/qualityofservice From 1c954214cfe8fe1b939d88cf3bce388ec728d975 Mon Sep 17 00:00:00 2001 From: Van Tran Date: Tue, 19 Mar 2024 02:28:22 +0700 Subject: [PATCH 12/32] feat !1843 update docs --- docs/features/metadata.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/metadata.rst b/docs/features/metadata.rst index ea71d93a2..080e59259 100644 --- a/docs/features/metadata.rst +++ b/docs/features/metadata.rst @@ -55,7 +55,7 @@ Now, the route metadata can be accessed through the ``DownstreamRoute`` object: public Task Invoke(HttpContext context, Func next) { var route = context.Items.DownstreamRoute(); - var enabled = metadata.GetMetadataFromJson("plugin1.enabled"); + var enabled = metadata.IsMetadataValueTruthy("plugin1.enabled"); var values = metadata.GetMetadataValues("plugin1.values"); var param1 = metadata.GetMetadataValue("plugin1.param", "system-default-value"); var param2 = metadata.GetMetadataNumber("plugin1.param2"); From d03bdbc6e45774dfa2a08778519cfb9429e594d2 Mon Sep 17 00:00:00 2001 From: Van Tran Date: Tue, 19 Mar 2024 02:29:43 +0700 Subject: [PATCH 13/32] feat !1843 cleanup split string logic --- src/Ocelot/Configuration/DownstreamRouteExtensions.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Ocelot/Configuration/DownstreamRouteExtensions.cs b/src/Ocelot/Configuration/DownstreamRouteExtensions.cs index 705129a39..529fabc95 100644 --- a/src/Ocelot/Configuration/DownstreamRouteExtensions.cs +++ b/src/Ocelot/Configuration/DownstreamRouteExtensions.cs @@ -59,12 +59,9 @@ public static string[] GetMetadataValues(this DownstreamRoute downstreamRoute, var strings = metadataValue.Split(separator, stringSplitOptions); char[] trimCharsArray = trimChars.ToCharArray(); - if (trimCharsArray.Length > 0) + for (var i = 0; i < strings.Length; i++) { - for (var i = 0; i < strings.Length; i++) - { - strings[i] = strings[i].Trim(trimCharsArray); - } + strings[i] = strings[i].Trim(trimCharsArray); } return strings.Where(x => x.Length > 0).ToArray(); From 474eef0403f752c9b33018d0b558dc72d26b1339 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Fri, 12 Apr 2024 19:56:53 +0300 Subject: [PATCH 14/32] SA1505: An opening brace should not be followed by a blank line --- .../Configuration/DownstreamRouteExtensionsTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs index d5f055172..379b8865f 100644 --- a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs @@ -231,7 +231,6 @@ private void should_parse_number(string value, T expected) [Fact] public void should_throw_error_when_invalid_number() { - // Arrange var key = "mykey"; _metadata.Add(key, "xyz"); From 48a17a30e3075afa277d307f2b0616160922819e Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Fri, 12 Apr 2024 20:07:29 +0300 Subject: [PATCH 15/32] IDE1006: Naming rule violation: These words must begin with upper case characters: should_xxx --- .../DownstreamRouteExtensionsTests.cs | 34 +++++++++---------- .../Configuration/DynamicsCreatorTests.cs | 4 +-- .../Configuration/MetadataCreatorTests.cs | 10 +++--- .../Configuration/RoutesCreatorTests.cs | 4 +-- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs index 379b8865f..502d685e3 100644 --- a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs @@ -55,7 +55,7 @@ public DownstreamRouteExtensionsTests() [Theory] [InlineData("key1", null)] [InlineData("hello", "world")] - public void should_return_default_value_when_key_not_found(string key, string defaultValue) + public void Should_return_default_value_when_key_not_found(string key, string defaultValue) { // Arrange _metadata.Add(key, defaultValue); @@ -70,7 +70,7 @@ public void should_return_default_value_when_key_not_found(string key, string de [Theory] [InlineData("hello", "world")] [InlineData("object.key", "value1,value2,value3")] - public void should_return_found_metadata_value(string key, string value) + public void Should_return_found_metadata_value(string key, string value) { // Arrange _metadata.Add(key, value); @@ -91,7 +91,7 @@ public void should_return_found_metadata_value(string key, string value) [InlineData("mykey", "value1, ,value2", "value1", "value2")] [InlineData("mykey", "value1, value2, value3", "value1", "value2", "value3")] [InlineData("mykey", ", ,value1, ,, ,,,,,value2,,, ", "value1", "value2")] - public void should_split_strings(string key, string value, params string[] expected) + public void Should_split_strings(string key, string value, params string[] expected) { // Arrange _metadata.Add(key, value); @@ -104,22 +104,22 @@ public void should_split_strings(string key, string value, params string[] expec } [Fact] - public void should_parse_from_json_null() => should_parse_object_from_json("mykey", "null", null); + public void Should_parse_from_json_null() => Should_parse_object_from_json("mykey", "null", null); [Fact] - public void should_parse_from_json_string() => should_parse_object_from_json("mykey", "\"string\"", "string"); + public void Should_parse_from_json_string() => Should_parse_object_from_json("mykey", "\"string\"", "string"); [Fact] - public void should_parse_from_json_numbers() => should_parse_object_from_json("mykey", "123", 123); + public void Should_parse_from_json_numbers() => Should_parse_object_from_json("mykey", "123", 123); [Fact] - public void should_parse_from_object() - => should_parse_object_from_json( + public void Should_parse_from_object() + => Should_parse_object_from_json( "mykey", "{\"Id\": 88, \"Value\": \"Hello World!\", \"MyTime\": \"2024-01-01T10:10:10.000Z\"}", new FakeObject { Id = 88, Value = "Hello World!", MyTime = new DateTime(2024, 1, 1, 10, 10, 10, DateTimeKind.Unspecified) }); - private void should_parse_object_from_json(string key, string value, object expected) + private void Should_parse_object_from_json(string key, string value, object expected) { // Arrange _metadata.Add(key, value); @@ -132,7 +132,7 @@ private void should_parse_object_from_json(string key, string value, object e } [Fact] - public void should_parse_from_json_array() + public void Should_parse_from_json_array() { // Arrange var key = "mykey"; @@ -149,7 +149,7 @@ public void should_parse_from_json_array() } [Fact] - public void should_throw_error_when_invalid_json() + public void Should_throw_error_when_invalid_json() { // Arrange var key = "mykey"; @@ -165,7 +165,7 @@ public void should_throw_error_when_invalid_json() } [Fact] - public void should_parse_json_with_custom_json_settings_options() + public void Should_parse_json_with_custom_json_settings_options() { // Arrange var key = "mykey"; @@ -203,7 +203,7 @@ record FakeObject [InlineData("99", 99)] [InlineData("500", 500)] [InlineData("999999999", 999999999)] - public void should_parse_integers(string value, int expected) => should_parse_number(value, expected); + public void Should_parse_integers(string value, int expected) => Should_parse_number(value, expected); [Theory] [InlineData("0", 0)] @@ -212,9 +212,9 @@ record FakeObject [InlineData("99.5", 99.5)] [InlineData("999999999", 999999999)] [InlineData("999999999.5", 999999999.5)] - public void should_parse_double(string value, double expected) => should_parse_number(value, expected); + public void Should_parse_double(string value, double expected) => Should_parse_number(value, expected); - private void should_parse_number(string value, T expected) + private void Should_parse_number(string value, T expected) where T : INumberBase { // Arrange @@ -229,7 +229,7 @@ private void should_parse_number(string value, T expected) } [Fact] - public void should_throw_error_when_invalid_number() + public void Should_throw_error_when_invalid_number() { // Arrange var key = "mykey"; @@ -268,7 +268,7 @@ public void should_throw_error_when_invalid_number() [InlineData("disable", false)] [InlineData("no", false)] [InlineData("abcxyz", false)] - public void should_parse_truthy_values(string value, bool expected) + public void Should_parse_truthy_values(string value, bool expected) { // Arrange var key = "mykey"; diff --git a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs index 1999e141a..c349f5d7e 100644 --- a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs @@ -30,7 +30,7 @@ public DynamicsCreatorTests() } [Fact] - public void should_return_nothing() + public void Should_return_nothing() { var fileConfig = new FileConfiguration(); @@ -43,7 +43,7 @@ public void should_return_nothing() } [Fact] - public void should_return_re_routes() + public void Should_return_routes() { var fileConfig = new FileConfiguration { diff --git a/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs index 5ca6a1874..1f18eae3e 100644 --- a/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs @@ -3,7 +3,7 @@ namespace Ocelot.UnitTests.Configuration; -public class MetadataCreatorTests +public class MetadataCreatorTests : UnitTest { private FileGlobalConfiguration _globalConfiguration; private Dictionary _metadataInRoute; @@ -11,7 +11,7 @@ public class MetadataCreatorTests private readonly MetadataCreator _sut = new(); [Fact] - public void should_return_empty_metadata() + public void Should_return_empty_metadata() { this.Given(_ => GivenEmptyMetadataInGlobalConfiguration()) .Given(_ => GivenEmptyMetadataInRoute()) @@ -20,7 +20,7 @@ public void should_return_empty_metadata() } [Fact] - public void should_return_global_metadata() + public void Should_return_global_metadata() { this.Given(_ => GivenSomeMetadataInGlobalConfiguration()) .Given(_ => GivenEmptyMetadataInRoute()) @@ -29,7 +29,7 @@ public void should_return_global_metadata() } [Fact] - public void should_return_route_metadata() + public void Should_return_route_metadata() { this.Given(_ => GivenEmptyMetadataInGlobalConfiguration()) .Given(_ => GivenSomeMetadataInRoute()) @@ -38,7 +38,7 @@ public void should_return_route_metadata() } [Fact] - public void should_overwrite_global_metadata() + public void Should_overwrite_global_metadata() { this.Given(_ => GivenSomeMetadataInGlobalConfiguration()) .Given(_ => GivenSomeMetadataInRoute()) diff --git a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs index 7a5fcc05e..2bbb91378 100644 --- a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs @@ -91,7 +91,7 @@ public RoutesCreatorTests() } [Fact] - public void should_return_nothing() + public void Should_return_nothing() { var fileConfig = new FileConfiguration(); @@ -102,7 +102,7 @@ public void should_return_nothing() } [Fact] - public void should_return_re_routes() + public void Should_return_routes() { var fileConfig = new FileConfiguration { From 7055457dcf518b56d3ca0c7dbd223d905118db7a Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Fri, 19 Apr 2024 21:53:02 +0300 Subject: [PATCH 16/32] Fix compile errors after rebasing --- src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs | 2 +- src/Ocelot/Configuration/DownstreamRoute.cs | 2 +- src/Ocelot/Configuration/File/FileRoute.cs | 1 - .../Configuration/DownstreamRouteExtensionsTests.cs | 2 ++ test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs index eca7ed353..ffcd11c7f 100644 --- a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs @@ -51,7 +51,7 @@ public DownstreamRouteBuilder() _delegatingHandlers = new(); _addHeadersToDownstream = new(); _addHeadersToUpstream = new(); - _metadata = new(); + _metadata = new Dictionary(); } public DownstreamRouteBuilder WithDownstreamAddresses(List downstreamAddresses) diff --git a/src/Ocelot/Configuration/DownstreamRoute.cs b/src/Ocelot/Configuration/DownstreamRoute.cs index 71799dd1a..060456bea 100644 --- a/src/Ocelot/Configuration/DownstreamRoute.cs +++ b/src/Ocelot/Configuration/DownstreamRoute.cs @@ -80,7 +80,7 @@ public DownstreamRoute( DownstreamHttpVersion = downstreamHttpVersion; DownstreamHttpVersionPolicy = downstreamHttpVersionPolicy; UpstreamHeaders = upstreamHeaders ?? new(); - Metadata = metadata; + Metadata = metadata ?? new Dictionary(); } public string Key { get; } diff --git a/src/Ocelot/Configuration/File/FileRoute.cs b/src/Ocelot/Configuration/File/FileRoute.cs index eccd2c1df..15746c6a9 100644 --- a/src/Ocelot/Configuration/File/FileRoute.cs +++ b/src/Ocelot/Configuration/File/FileRoute.cs @@ -78,7 +78,6 @@ public FileRoute(FileRoute from) public List UpstreamHttpMethod { get; set; } public string UpstreamPathTemplate { get; set; } public IDictionary UpstreamHeaderTemplates { get; set; } - public IDictionary Metadata { get; set; } /// /// Clones this object by making a deep copy. diff --git a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs index 502d685e3..020056571 100644 --- a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs @@ -49,6 +49,8 @@ public DownstreamRouteExtensionsTests() new SecurityOptions(), null, new Version(), + HttpVersionPolicy.RequestVersionExact, + new(), _metadata); } diff --git a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs index c349f5d7e..cf76c6345 100644 --- a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs @@ -26,7 +26,7 @@ public DynamicsCreatorTests() _versionPolicyCreator = new Mock(); _metadataCreator = new Mock(); _rloCreator = new Mock(); - _creator = new DynamicsCreator(_rloCreator.Object, _versionCreator.Object, _versionPolicyCreator.Object); + _creator = new DynamicsCreator(_rloCreator.Object, _versionCreator.Object, _versionPolicyCreator.Object, _metadataCreator.Object); } [Fact] From dfcc35174e061e5cb8c36c015c377ba9d3ef87cd Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Fri, 19 Apr 2024 22:19:04 +0300 Subject: [PATCH 17/32] Fix unit tests + AAA pattern --- .../Configuration/DynamicsCreatorTests.cs | 91 +++++++++---------- 1 file changed, 44 insertions(+), 47 deletions(-) diff --git a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs index cf76c6345..ccffa145d 100644 --- a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs @@ -32,65 +32,62 @@ public DynamicsCreatorTests() [Fact] public void Should_return_nothing() { + // Arrange var fileConfig = new FileConfiguration(); + GivenThe(fileConfig); - this.Given(_ => GivenThe(fileConfig)) - .When(_ => WhenICreate()) - .Then(_ => ThenNothingIsReturned()) - .And(_ => ThenTheRloCreatorIsNotCalled()) - .And(_ => ThenTheMetadataCreatorIsNotCalled()) - .BDDfy(); + // Act + WhenICreate(); + + // Assert + ThenNothingIsReturned(); + ThenTheRloCreatorIsNotCalled(); + ThenTheMetadataCreatorIsNotCalled(); } [Fact] public void Should_return_routes() { + // Arrange var fileConfig = new FileConfiguration { - DynamicRoutes = new List + DynamicRoutes = new() { - new() - { - ServiceName = "1", - RateLimitRule = new FileRateLimitRule - { - EnableRateLimiting = false, - }, - DownstreamHttpVersion = "1.1", - Metadata = new Dictionary - { - ["foo"] = "bar", - }, - }, - new() - { - ServiceName = "2", - RateLimitRule = new FileRateLimitRule - { - EnableRateLimiting = true, - }, - DownstreamHttpVersion = "2.0", - Metadata = new Dictionary - { - ["foo"] = "baz", - }, - }, + GivenDynamicRoute("1", false, "1.1", "foo", "bar"), + GivenDynamicRoute("2", true, "2.0", "foo", "baz"), }, }; - - this.Given(_ => GivenThe(fileConfig)) - .And(_ => GivenTheRloCreatorReturns()) - .And(_ => GivenTheVersionCreatorReturns()) - .And(_ => GivenTheVersionPolicyCreatorReturns()) - .And(_ => GivenTheMetadataCreatorReturns()) - .When(_ => WhenICreate()) - .Then(_ => ThenTheRoutesAreReturned()) - .And(_ => ThenTheRloCreatorIsCalledCorrectly()) - .And(_ => ThenTheVersionCreatorIsCalledCorrectly()) - .And(_ => ThenTheMetadataCreatorIsCalledCorrectly()) - .BDDfy(); + GivenThe(fileConfig); + GivenTheRloCreatorReturns(); + GivenTheVersionCreatorReturns(); + GivenTheVersionPolicyCreatorReturns(); + GivenTheMetadataCreatorReturns(); + + // Act + WhenICreate(); + + // Assert + ThenTheRoutesAreReturned(); + ThenTheRloCreatorIsCalledCorrectly(); + ThenTheVersionCreatorIsCalledCorrectly(); + ThenTheMetadataCreatorIsCalledCorrectly(); } + private FileDynamicRoute GivenDynamicRoute(string serviceName, bool enableRateLimiting, + string downstreamHttpVersion, string key, string value) => new() + { + ServiceName = serviceName, + RateLimitRule = new FileRateLimitRule + { + EnableRateLimiting = enableRateLimiting, + }, + DownstreamHttpVersion = downstreamHttpVersion, + Metadata = new Dictionary + { + [key] = value, + }, + }; + private void ThenTheRloCreatorIsCalledCorrectly() { _rloCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].RateLimitRule, @@ -105,8 +102,8 @@ private void ThenTheVersionCreatorIsCalledCorrectly() _versionCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].DownstreamHttpVersion), Times.Once); _versionCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[1].DownstreamHttpVersion), Times.Once); - _versionPolicyCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].DownstreamHttpVersionPolicy), Times.Once); - _versionPolicyCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[1].DownstreamHttpVersionPolicy), Times.Once); + _versionPolicyCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[0].DownstreamHttpVersionPolicy), Times.Exactly(2)); + _versionPolicyCreator.Verify(x => x.Create(_fileConfig.DynamicRoutes[1].DownstreamHttpVersionPolicy), Times.Exactly(2)); } private void ThenTheMetadataCreatorIsCalledCorrectly() From 8f217529f3af6edde58287b7832f82d1209b1d9e Mon Sep 17 00:00:00 2001 From: Guillaume Gnaegi <58469901+ggnaegi@users.noreply.github.com> Date: Thu, 2 May 2024 08:44:51 +0200 Subject: [PATCH 18/32] First Version, providing a generic extension method GetMetadata with global configuration --- .../Builder/DownstreamRouteBuilder.cs | 10 +- .../Builder/MetadataOptionsBuilder.cs | 54 ++++++ .../Configuration/Creator/DynamicsCreator.cs | 3 +- .../Configuration/Creator/IMetadataCreator.cs | 2 +- .../Configuration/Creator/MetadataCreator.cs | 30 ++-- src/Ocelot/Configuration/DownstreamRoute.cs | 6 +- .../DownstreamRouteExtensions.cs | 154 ++++++++++-------- .../File/FileGlobalConfiguration.cs | 4 +- .../Configuration/File/FileMetadataOptions.cs | 33 ++++ src/Ocelot/Configuration/MetadataOptions.cs | 45 +++++ .../CustomOcelotMiddleware.cs | 2 +- .../DownstreamRouteExtensionsTests.cs | 56 +++---- .../Configuration/DynamicsCreatorTests.cs | 6 +- .../Configuration/MetadataCreatorTests.cs | 20 ++- .../Configuration/RoutesCreatorTests.cs | 19 ++- 15 files changed, 300 insertions(+), 144 deletions(-) create mode 100644 src/Ocelot/Configuration/Builder/MetadataOptionsBuilder.cs create mode 100644 src/Ocelot/Configuration/File/FileMetadataOptions.cs create mode 100644 src/Ocelot/Configuration/MetadataOptions.cs diff --git a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs index ffcd11c7f..f5fee83a3 100644 --- a/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs +++ b/src/Ocelot/Configuration/Builder/DownstreamRouteBuilder.cs @@ -1,6 +1,5 @@ using Ocelot.Configuration.Creator; using Ocelot.Values; -using System.Collections.Generic; namespace Ocelot.Configuration.Builder; @@ -43,7 +42,7 @@ public class DownstreamRouteBuilder private Version _downstreamHttpVersion; private HttpVersionPolicy _downstreamHttpVersionPolicy; private Dictionary _upstreamHeaders; - private IDictionary _metadata; + private MetadataOptions _metadataOptions; public DownstreamRouteBuilder() { @@ -51,7 +50,6 @@ public DownstreamRouteBuilder() _delegatingHandlers = new(); _addHeadersToDownstream = new(); _addHeadersToUpstream = new(); - _metadata = new Dictionary(); } public DownstreamRouteBuilder WithDownstreamAddresses(List downstreamAddresses) @@ -278,9 +276,9 @@ public DownstreamRouteBuilder WithDownstreamHttpVersionPolicy(HttpVersionPolicy return this; } - public DownstreamRouteBuilder WithMetadata(IDictionary metadata) + public DownstreamRouteBuilder WithMetadata(MetadataOptions metadataOptions) { - _metadata = new Dictionary(metadata); + _metadataOptions = metadataOptions; return this; } @@ -323,6 +321,6 @@ public DownstreamRoute Build() _downstreamHttpVersion, _downstreamHttpVersionPolicy, _upstreamHeaders, - _metadata); + _metadataOptions); } } diff --git a/src/Ocelot/Configuration/Builder/MetadataOptionsBuilder.cs b/src/Ocelot/Configuration/Builder/MetadataOptionsBuilder.cs new file mode 100644 index 000000000..79b765672 --- /dev/null +++ b/src/Ocelot/Configuration/Builder/MetadataOptionsBuilder.cs @@ -0,0 +1,54 @@ +using System.Globalization; + +namespace Ocelot.Configuration.Builder; + +public class MetadataOptionsBuilder +{ + private string[] _separators; + private char[] _trimChars; + private StringSplitOptions _stringSplitOption; + private NumberStyles _numberStyle; + private CultureInfo _currentCulture; + private IDictionary _metadata; + + public MetadataOptionsBuilder WithSeparators(string[] separators) + { + _separators = separators; + return this; + } + + public MetadataOptionsBuilder WithTrimChars(char[] trimChars) + { + _trimChars = trimChars; + return this; + } + + public MetadataOptionsBuilder WithStringSplitOption(string stringSplitOption) + { + _stringSplitOption = Enum.Parse(stringSplitOption); + return this; + } + + public MetadataOptionsBuilder WithNumberStyle(string numberStyle) + { + _numberStyle = Enum.Parse(numberStyle); + return this; + } + + public MetadataOptionsBuilder WithCurrentCulture(string currentCulture) + { + _currentCulture = CultureInfo.GetCultureInfo(currentCulture); + return this; + } + + public MetadataOptionsBuilder WithMetadata(IDictionary metadata) + { + _metadata = metadata; + return this; + } + + public MetadataOptions Build() + { + return new MetadataOptions(_separators, _trimChars, _stringSplitOption, _numberStyle, _currentCulture, _metadata); + } +} diff --git a/src/Ocelot/Configuration/Creator/DynamicsCreator.cs b/src/Ocelot/Configuration/Creator/DynamicsCreator.cs index 803b06b14..cb5cdf419 100644 --- a/src/Ocelot/Configuration/Creator/DynamicsCreator.cs +++ b/src/Ocelot/Configuration/Creator/DynamicsCreator.cs @@ -35,8 +35,7 @@ private Route SetUpDynamicRoute(FileDynamicRoute fileDynamicRoute, FileGlobalCon .Create(fileDynamicRoute.RateLimitRule, globalConfiguration); var version = _versionCreator.Create(fileDynamicRoute.DownstreamHttpVersion); - var versionPolicy = _versionPolicyCreator.Create(fileDynamicRoute.DownstreamHttpVersionPolicy); - + var versionPolicy = _versionPolicyCreator.Create(fileDynamicRoute.DownstreamHttpVersionPolicy); var metadata = _metadataCreator.Create(fileDynamicRoute.Metadata, globalConfiguration); var downstreamRoute = new DownstreamRouteBuilder() diff --git a/src/Ocelot/Configuration/Creator/IMetadataCreator.cs b/src/Ocelot/Configuration/Creator/IMetadataCreator.cs index c60f2de6b..7b9291a40 100644 --- a/src/Ocelot/Configuration/Creator/IMetadataCreator.cs +++ b/src/Ocelot/Configuration/Creator/IMetadataCreator.cs @@ -4,5 +4,5 @@ namespace Ocelot.Configuration.Creator; public interface IMetadataCreator { - IDictionary Create(IDictionary metadata, FileGlobalConfiguration fileGlobalConfiguration); + MetadataOptions Create(IDictionary metadata, FileGlobalConfiguration fileGlobalConfiguration); } diff --git a/src/Ocelot/Configuration/Creator/MetadataCreator.cs b/src/Ocelot/Configuration/Creator/MetadataCreator.cs index 5e18b545d..637e1ca31 100644 --- a/src/Ocelot/Configuration/Creator/MetadataCreator.cs +++ b/src/Ocelot/Configuration/Creator/MetadataCreator.cs @@ -1,25 +1,33 @@ -using Ocelot.Configuration.File; +using Ocelot.Configuration.Builder; +using Ocelot.Configuration.File; namespace Ocelot.Configuration.Creator; public class MetadataCreator : IMetadataCreator { - public IDictionary Create(IDictionary routeMetadata, FileGlobalConfiguration fileGlobalConfiguration) + public MetadataOptions Create(IDictionary metadata, FileGlobalConfiguration fileGlobalConfiguration) { - var metadata = fileGlobalConfiguration?.Metadata != null - ? new Dictionary(fileGlobalConfiguration.Metadata) - : new Dictionary(); + return Create(new FileMetadataOptions { Metadata = metadata ?? new Dictionary() }, fileGlobalConfiguration); + } - if (routeMetadata == null) - { - return metadata; - } + public MetadataOptions Create(FileMetadataOptions routeMetadataOptions, FileGlobalConfiguration fileGlobalConfiguration) + { + var metadata = fileGlobalConfiguration.MetadataOptions.Metadata.Any() + ? new Dictionary(fileGlobalConfiguration.MetadataOptions.Metadata) + : new Dictionary(); - foreach (var (key, value) in routeMetadata) + foreach (var (key, value) in routeMetadataOptions.Metadata) { metadata[key] = value; } - return metadata; + return new MetadataOptionsBuilder() + .WithMetadata(metadata) + .WithSeparators(fileGlobalConfiguration.MetadataOptions.Separators) + .WithTrimChars(fileGlobalConfiguration.MetadataOptions.TrimChars) + .WithStringSplitOption(fileGlobalConfiguration.MetadataOptions.StringSplitOption) + .WithNumberStyle(fileGlobalConfiguration.MetadataOptions.NumberStyle) + .WithCurrentCulture(fileGlobalConfiguration.MetadataOptions.CurrentCulture) + .Build(); } } diff --git a/src/Ocelot/Configuration/DownstreamRoute.cs b/src/Ocelot/Configuration/DownstreamRoute.cs index 060456bea..0241f9b61 100644 --- a/src/Ocelot/Configuration/DownstreamRoute.cs +++ b/src/Ocelot/Configuration/DownstreamRoute.cs @@ -42,7 +42,7 @@ public DownstreamRoute( Version downstreamHttpVersion, HttpVersionPolicy downstreamHttpVersionPolicy, Dictionary upstreamHeaders, - IDictionary metadata) + MetadataOptions metadataOptions) { DangerousAcceptAnyServerCertificateValidator = dangerousAcceptAnyServerCertificateValidator; AddHeadersToDownstream = addHeadersToDownstream; @@ -80,7 +80,7 @@ public DownstreamRoute( DownstreamHttpVersion = downstreamHttpVersion; DownstreamHttpVersionPolicy = downstreamHttpVersionPolicy; UpstreamHeaders = upstreamHeaders ?? new(); - Metadata = metadata ?? new Dictionary(); + MetadataOptions = metadataOptions; } public string Key { get; } @@ -130,6 +130,6 @@ public DownstreamRoute( /// public HttpVersionPolicy DownstreamHttpVersionPolicy { get; } public Dictionary UpstreamHeaders { get; } - public IDictionary Metadata { get; } + public MetadataOptions MetadataOptions { get; } } } diff --git a/src/Ocelot/Configuration/DownstreamRouteExtensions.cs b/src/Ocelot/Configuration/DownstreamRouteExtensions.cs index 529fabc95..d5aa24f17 100644 --- a/src/Ocelot/Configuration/DownstreamRouteExtensions.cs +++ b/src/Ocelot/Configuration/DownstreamRouteExtensions.cs @@ -1,101 +1,113 @@ -using System.Globalization; -using System.Numerics; -using System.Text.Json; +using System.Text.Json; -namespace Ocelot.Configuration +namespace Ocelot.Configuration; + +public static class DownstreamRouteExtensions { - public static class DownstreamRouteExtensions + private static readonly HashSet TruthyValues = + new(StringComparer.OrdinalIgnoreCase) + { + "true", + "yes", + "on", + "ok", + "enable", + "enabled", + "1", + }; + + private static readonly HashSet FalsyValues = + new(StringComparer.OrdinalIgnoreCase) + { + "false", + "no", + "off", + "disable", + "disabled", + "0", + }; + + /// + /// The known numeric types + /// + private static readonly HashSet NumericTypes = new() + { + typeof(byte), + typeof(sbyte), + typeof(short), + typeof(ushort), + typeof(int), + typeof(uint), + typeof(long), + typeof(ulong), + typeof(float), + typeof(double), + typeof(decimal), + }; + + public static T GetMetadata(this DownstreamRoute downstreamRoute, string key, T defaultValue = default, + JsonSerializerOptions jsonSerializerOptions = null) { - public static string GetMetadataValue(this DownstreamRoute downstreamRoute, - string key, - string defaultValue = null) + var metadata = downstreamRoute?.MetadataOptions.Metadata; + + if (metadata == null || !metadata.TryGetValue(key, out var metadataValue)) { - var metadata = downstreamRoute?.Metadata; + return defaultValue; + } - if (metadata == null) - { - return defaultValue; - } + // if the value is null, return the default value of the target type + if (metadataValue == null) + { + return default; + } - if (!metadata.TryGetValue(key, out string value)) - { - return defaultValue; - } + return (T)ConvertTo(typeof(T), metadataValue, downstreamRoute.MetadataOptions, + jsonSerializerOptions ?? new JsonSerializerOptions(JsonSerializerDefaults.Web)); + } + private static object ConvertTo(Type targetType, string value, MetadataOptions metadataOptions, + JsonSerializerOptions jsonSerializerOptions) + { + if (targetType == typeof(string)) + { return value; } -#if NET7_0_OR_GREATER - public static T GetMetadataNumber(this DownstreamRoute downstreamRoute, - string key, - T defaultValue = default, - NumberStyles numberStyles = NumberStyles.Any, - CultureInfo cultureInfo = null) - where T : INumberBase + if (targetType == typeof(bool)) { - var metadataValue = downstreamRoute.GetMetadataValue(key); - if (metadataValue == null) - { - return defaultValue; - } - - IFormatProvider formatProvider = cultureInfo ?? CultureInfo.CurrentCulture; - return T.Parse(metadataValue, numberStyles, formatProvider); + return TruthyValues.Contains(value.Trim()); } -#endif - public static string[] GetMetadataValues(this DownstreamRoute downstreamRoute, - string key, - string separator = ",", - StringSplitOptions stringSplitOptions = StringSplitOptions.RemoveEmptyEntries, - string trimChars = " ") + if (targetType == typeof(bool?)) { - var metadataValue = downstreamRoute.GetMetadataValue(key); - if (metadataValue == null) + if (TruthyValues.Contains(value.Trim())) { - return Array.Empty(); + return true; } - var strings = metadataValue.Split(separator, stringSplitOptions); - char[] trimCharsArray = trimChars.ToCharArray(); - - for (var i = 0; i < strings.Length; i++) + if (FalsyValues.Contains(value.Trim())) { - strings[i] = strings[i].Trim(trimCharsArray); + return false; } - return strings.Where(x => x.Length > 0).ToArray(); + return null; } - public static T GetMetadataFromJson(this DownstreamRoute downstreamRoute, - string key, - T defaultValue = default, - JsonSerializerOptions jsonSerializerOptions = null) + if (targetType == typeof(string[])) { - var metadataValue = downstreamRoute.GetMetadataValue(key); - if (metadataValue == null) + if (value == null) { - return defaultValue; + return Array.Empty(); } - return JsonSerializer.Deserialize(metadataValue, jsonSerializerOptions); + return value.Split(metadataOptions.Separators, metadataOptions.StringSplitOption) + .Select(s => s.Trim(metadataOptions.TrimChars)) + .Where(s => !string.IsNullOrEmpty(s)) + .ToArray(); } - public static bool IsMetadataValueTruthy(this DownstreamRoute downstreamRoute, string key) - { - var metadataValue = downstreamRoute.GetMetadataValue(key); - if (metadataValue == null) - { - return false; - } - - var trimmedValue = metadataValue.Trim().ToLower(); - return trimmedValue == "true" || - trimmedValue == "yes" || - trimmedValue == "on" || - trimmedValue == "ok" || - trimmedValue == "enable" || - trimmedValue == "enabled"; - } + return NumericTypes.Contains(targetType) + ? Convert.ChangeType(value, targetType, metadataOptions.CurrentCulture) + : JsonSerializer.Deserialize(value, targetType, jsonSerializerOptions); } } diff --git a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs index 81875ad64..9472e055a 100644 --- a/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs +++ b/src/Ocelot/Configuration/File/FileGlobalConfiguration.cs @@ -11,7 +11,7 @@ public FileGlobalConfiguration() LoadBalancerOptions = new FileLoadBalancerOptions(); QoSOptions = new FileQoSOptions(); HttpHandlerOptions = new FileHttpHandlerOptions(); - Metadata = new Dictionary(); + MetadataOptions = new FileMetadataOptions(); } public string RequestIdKey { get; set; } @@ -44,6 +44,6 @@ public FileGlobalConfiguration() /// public string DownstreamHttpVersionPolicy { get; set; } - public IDictionary Metadata { get; set; } + public FileMetadataOptions MetadataOptions { get; set; } } } diff --git a/src/Ocelot/Configuration/File/FileMetadataOptions.cs b/src/Ocelot/Configuration/File/FileMetadataOptions.cs new file mode 100644 index 000000000..8d34bbe44 --- /dev/null +++ b/src/Ocelot/Configuration/File/FileMetadataOptions.cs @@ -0,0 +1,33 @@ +using System.Globalization; + +namespace Ocelot.Configuration.File; + +public class FileMetadataOptions +{ + public FileMetadataOptions() + { + Separators = new[] { "," }; + TrimChars = new[] { ' ' }; + StringSplitOption = Enum.GetName(typeof(StringSplitOptions), StringSplitOptions.None); + NumberStyle = Enum.GetName(typeof(NumberStyles), NumberStyles.Any); + CurrentCulture = CultureInfo.CurrentCulture.Name; + Metadata = new Dictionary(); + } + + public FileMetadataOptions(FileMetadataOptions from) + { + Separators = from.Separators; + TrimChars = from.TrimChars; + StringSplitOption = from.StringSplitOption; + NumberStyle = from.NumberStyle; + CurrentCulture = from.CurrentCulture; + Metadata = from.Metadata; + } + + public IDictionary Metadata { get; set; } + public string[] Separators { get; set; } + public char[] TrimChars { get; set; } + public string StringSplitOption { get; set; } + public string NumberStyle { get; set; } + public string CurrentCulture { get; set; } +} diff --git a/src/Ocelot/Configuration/MetadataOptions.cs b/src/Ocelot/Configuration/MetadataOptions.cs new file mode 100644 index 000000000..b94a1fb7c --- /dev/null +++ b/src/Ocelot/Configuration/MetadataOptions.cs @@ -0,0 +1,45 @@ +using Ocelot.Configuration.File; +using System.Globalization; + +namespace Ocelot.Configuration; + +public class MetadataOptions +{ + public MetadataOptions(MetadataOptions from) + { + Separators = from.Separators; + TrimChars = from.TrimChars; + StringSplitOption = from.StringSplitOption; + NumberStyle = from.NumberStyle; + CurrentCulture = from.CurrentCulture; + Metadata = from.Metadata; + } + + public MetadataOptions(FileMetadataOptions from) + { + StringSplitOption = Enum.Parse(from.StringSplitOption); + NumberStyle = Enum.Parse(from.NumberStyle); + CurrentCulture = CultureInfo.GetCultureInfo(from.CurrentCulture); + Separators = from.Separators; + TrimChars = from.TrimChars; + Metadata = from.Metadata; + } + + public MetadataOptions(string[] separators, char[] trimChars, StringSplitOptions stringSplitOption, + NumberStyles numberStyle, CultureInfo currentCulture, IDictionary metadata) + { + Separators = separators; + TrimChars = trimChars; + StringSplitOption = stringSplitOption; + NumberStyle = numberStyle; + CurrentCulture = currentCulture; + Metadata = metadata; + } + + public string[] Separators { get; } + public char[] TrimChars { get; } + public StringSplitOptions StringSplitOption { get; } + public NumberStyles NumberStyle { get; } + public CultureInfo CurrentCulture { get; } + public IDictionary Metadata { get; set; } +} diff --git a/test/Ocelot.ManualTest/CustomOcelotMiddleware.cs b/test/Ocelot.ManualTest/CustomOcelotMiddleware.cs index 6370956d7..f3d251887 100644 --- a/test/Ocelot.ManualTest/CustomOcelotMiddleware.cs +++ b/test/Ocelot.ManualTest/CustomOcelotMiddleware.cs @@ -12,7 +12,7 @@ public static Task Invoke(HttpContext context, Func next) var logger = GetLogger(context); var downstreamRoute = context.Items.DownstreamRoute(); - if (downstreamRoute?.Metadata is { } metadata) + if (downstreamRoute?.MetadataOptions?.Metadata is { } metadata) { logger.LogInformation(() => { diff --git a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs index 020056571..78b7624b2 100644 --- a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs @@ -1,5 +1,6 @@ using Ocelot.Configuration; using Ocelot.Configuration.Creator; +using Ocelot.Configuration.File; using Ocelot.Values; using System.Numerics; using System.Text.Json; @@ -8,12 +9,10 @@ namespace Ocelot.UnitTests.Configuration; public class DownstreamRouteExtensionsTests { - private readonly Dictionary _metadata; private readonly DownstreamRoute _downstreamRoute; public DownstreamRouteExtensionsTests() { - _metadata = new Dictionary(); _downstreamRoute = new DownstreamRoute( null, new UpstreamPathTemplate(null, 0, false, null), @@ -51,7 +50,7 @@ public DownstreamRouteExtensionsTests() new Version(), HttpVersionPolicy.RequestVersionExact, new(), - _metadata); + new MetadataOptions(new FileMetadataOptions())); } [Theory] @@ -60,10 +59,10 @@ public DownstreamRouteExtensionsTests() public void Should_return_default_value_when_key_not_found(string key, string defaultValue) { // Arrange - _metadata.Add(key, defaultValue); + _downstreamRoute.MetadataOptions.Metadata.Add(key, defaultValue); // Act - var metadataValue = _downstreamRoute.GetMetadataValue(key, defaultValue); + var metadataValue = _downstreamRoute.GetMetadata(key, defaultValue); // Assert metadataValue.ShouldBe(defaultValue); @@ -75,10 +74,10 @@ public void Should_return_default_value_when_key_not_found(string key, string de public void Should_return_found_metadata_value(string key, string value) { // Arrange - _metadata.Add(key, value); + _downstreamRoute.MetadataOptions.Metadata.Add(key, value); // Act - var metadataValue = _downstreamRoute.GetMetadataValue(key); + var metadataValue = _downstreamRoute.GetMetadata(key); //Assert metadataValue.ShouldBe(value); @@ -96,10 +95,10 @@ public void Should_return_found_metadata_value(string key, string value) public void Should_split_strings(string key, string value, params string[] expected) { // Arrange - _metadata.Add(key, value); + _downstreamRoute.MetadataOptions.Metadata.Add(key, value); // Act - var metadataValue = _downstreamRoute.GetMetadataValues(key); + var metadataValue = _downstreamRoute.GetMetadata(key); //Assert metadataValue.ShouldBe(expected); @@ -109,7 +108,7 @@ public void Should_split_strings(string key, string value, params string[] expec public void Should_parse_from_json_null() => Should_parse_object_from_json("mykey", "null", null); [Fact] - public void Should_parse_from_json_string() => Should_parse_object_from_json("mykey", "\"string\"", "string"); + public void Should_parse_from_json_string() => Should_parse_object_from_json("mykey", "string", "string"); [Fact] public void Should_parse_from_json_numbers() => Should_parse_object_from_json("mykey", "123", 123); @@ -124,10 +123,10 @@ public void Should_parse_from_object() private void Should_parse_object_from_json(string key, string value, object expected) { // Arrange - _metadata.Add(key, value); + _downstreamRoute.MetadataOptions.Metadata.Add(key, value); // Act - var metadataValue = _downstreamRoute.GetMetadataFromJson(key); + var metadataValue = _downstreamRoute.GetMetadata(key); //Assert metadataValue.ShouldBeEquivalentTo(expected); @@ -138,16 +137,17 @@ public void Should_parse_from_json_array() { // Arrange var key = "mykey"; - _metadata.Add(key, "[\"value1\", \"value2\", \"value3\"]"); + _downstreamRoute.MetadataOptions.Metadata.Add(key, "[\"value1\", \"value2\", \"value3\"]"); // Act - var metadataValue = _downstreamRoute.GetMetadataFromJson>(key); + var metadataValue = _downstreamRoute.GetMetadata>(key); //Assert - metadataValue.ShouldNotBeNull(); - metadataValue.ElementAt(0).ShouldBe("value1"); - metadataValue.ElementAt(1).ShouldBe("value2"); - metadataValue.ElementAt(2).ShouldBe("value3"); + IEnumerable enumerable = metadataValue as string[] ?? metadataValue.ToArray(); + enumerable.ShouldNotBeNull(); + enumerable.ElementAt(0).ShouldBe("value1"); + enumerable.ElementAt(1).ShouldBe("value2"); + enumerable.ElementAt(2).ShouldBe("value3"); } [Fact] @@ -155,14 +155,14 @@ public void Should_throw_error_when_invalid_json() { // Arrange var key = "mykey"; - _metadata.Add(key, "[[["); + _downstreamRoute.MetadataOptions.Metadata.Add(key, "[[["); // Act //Assert Assert.Throws(() => { - _ = _downstreamRoute.GetMetadataFromJson>(key); + _ = _downstreamRoute.GetMetadata>(key); }); } @@ -182,10 +182,10 @@ public void Should_parse_json_with_custom_json_settings_options() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; - _metadata.Add(key, value); + _downstreamRoute.MetadataOptions.Metadata.Add(key, value); // Act - var metadataValue = _downstreamRoute.GetMetadataFromJson(key, jsonSerializerOptions: serializerOptions); + var metadataValue = _downstreamRoute.GetMetadata(key, jsonSerializerOptions: serializerOptions); //Assert metadataValue.ShouldBeEquivalentTo(expected); @@ -221,10 +221,10 @@ private void Should_parse_number(string value, T expected) { // Arrange var key = "mykey"; - _metadata.Add(key, value); + _downstreamRoute.MetadataOptions.Metadata.Add(key, value); // Act - var metadataValue = _downstreamRoute.GetMetadataNumber(key); + var metadataValue = _downstreamRoute.GetMetadata(key); //Assert metadataValue.ShouldBe(expected); @@ -235,14 +235,14 @@ public void Should_throw_error_when_invalid_number() { // Arrange var key = "mykey"; - _metadata.Add(key, "xyz"); + _downstreamRoute.MetadataOptions.Metadata.Add(key, "xyz"); // Act // Assert Assert.Throws(() => { - _ = _downstreamRoute.GetMetadataNumber(key); + _ = _downstreamRoute.GetMetadata(key); }); } @@ -274,10 +274,10 @@ public void Should_parse_truthy_values(string value, bool expected) { // Arrange var key = "mykey"; - _metadata.Add(key, value); + _downstreamRoute.MetadataOptions.Metadata.Add(key, value); // Act - var isTrusthy = _downstreamRoute.IsMetadataValueTruthy(key); + var isTrusthy = _downstreamRoute.GetMetadata(key); //Assert isTrusthy.ShouldBe(expected); diff --git a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs index ccffa145d..3d06802fc 100644 --- a/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DynamicsCreatorTests.cs @@ -146,8 +146,8 @@ private void GivenTheMetadataCreatorReturns() { ["foo"] = "bar", }; - _metadataCreator.Setup(x => x.Create(It.IsAny>(), It.IsAny())) - .Returns(_expectedMetadata); + _metadataCreator.Setup(x => x.Create(It.IsAny>(), It.IsAny())) + .Returns(new MetadataOptions(new FileMetadataOptions{Metadata = _expectedMetadata})); } private void GivenTheRloCreatorReturns() @@ -168,7 +168,7 @@ private void ThenTheRloCreatorIsNotCalled() private void ThenTheMetadataCreatorIsNotCalled() { - _metadataCreator.Verify(x => x.Create(It.IsAny>(), It.IsAny()), Times.Never); + _metadataCreator.Verify(x => x.Create(It.IsAny>(), It.IsAny()), Times.Never); } private void ThenNothingIsReturned() diff --git a/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs index 1f18eae3e..b72129ee6 100644 --- a/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs @@ -1,4 +1,5 @@ -using Ocelot.Configuration.Creator; +using Ocelot.Configuration; +using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; namespace Ocelot.UnitTests.Configuration; @@ -7,7 +8,7 @@ public class MetadataCreatorTests : UnitTest { private FileGlobalConfiguration _globalConfiguration; private Dictionary _metadataInRoute; - private IDictionary _result; + private MetadataOptions _result; private readonly MetadataCreator _sut = new(); [Fact] @@ -48,7 +49,7 @@ public void Should_overwrite_global_metadata() private void WhenICreate() { - _result = _sut.Create(_metadataInRoute, _globalConfiguration); + _result = _sut.Create(new FileMetadataOptions{Metadata = _metadataInRoute}, _globalConfiguration); } private void GivenEmptyMetadataInGlobalConfiguration() @@ -60,9 +61,12 @@ private void GivenSomeMetadataInGlobalConfiguration() { _globalConfiguration = new FileGlobalConfiguration() { - Metadata = new Dictionary + MetadataOptions = new FileMetadataOptions { - ["foo"] = "bar", + Metadata = new Dictionary + { + ["foo"] = "bar", + } }, }; } @@ -82,12 +86,12 @@ private void GivenSomeMetadataInRoute() private void ThenDownstreamRouteMetadataMustBeEmpty() { - _result.Keys.ShouldBeEmpty(); + _result.Metadata.Keys.ShouldBeEmpty(); } private void ThenDownstreamMetadataMustContain(string key, string value) { - _result.Keys.ShouldContain(key); - _result[key].ShouldBeEquivalentTo(value); + _result.Metadata.Keys.ShouldContain(key); + _result.Metadata[key].ShouldBeEquivalentTo(value); } } diff --git a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs index 2bbb91378..e11c5576f 100644 --- a/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/RoutesCreatorTests.cs @@ -125,9 +125,9 @@ public void Should_return_routes() { "e","f" }, }, UpstreamHttpMethod = new List { "GET", "POST" }, - Metadata = new Dictionary() - { - ["foo"] = "bar", + Metadata = new Dictionary + { + ["foo"] = "bar", }, }, new() @@ -147,9 +147,9 @@ public void Should_return_routes() { "k","l" }, }, UpstreamHttpMethod = new List { "PUT", "DELETE" }, - Metadata = new Dictionary() - { - ["foo"] = "baz", + Metadata = new Dictionary + { + ["foo"] = "baz", }, }, }, @@ -208,7 +208,10 @@ private void GivenTheDependenciesAreSetUpCorrectly() _versionCreator.Setup(x => x.Create(It.IsAny())).Returns(_expectedVersion); _versionPolicyCreator.Setup(x => x.Create(It.IsAny())).Returns(_expectedVersionPolicy); _uhtpCreator.Setup(x => x.Create(It.IsAny())).Returns(_uht); - _metadataCreator.Setup(x => x.Create(It.IsAny>(), It.IsAny())).Returns(_expectedMetadata); + _metadataCreator.Setup(x => x.Create(It.IsAny>(), It.IsAny())).Returns(new MetadataOptions(new FileMetadataOptions + { + Metadata = _expectedMetadata, + })); } private void ThenTheRoutesAreCreated() @@ -268,7 +271,7 @@ private void ThenTheRouteIsSet(FileRoute expected, int routeIndex) _result[routeIndex].DownstreamRoute[0].RouteClaimsRequirement.ShouldBe(expected.RouteClaimsRequirement); _result[routeIndex].DownstreamRoute[0].DownstreamPathTemplate.Value.ShouldBe(expected.DownstreamPathTemplate); _result[routeIndex].DownstreamRoute[0].Key.ShouldBe(expected.Key); - _result[routeIndex].DownstreamRoute[0].Metadata.ShouldBe(_expectedMetadata); + _result[routeIndex].DownstreamRoute[0].MetadataOptions.Metadata.ShouldBe(_expectedMetadata); _result[routeIndex].UpstreamHttpMethod .Select(x => x.Method) .ToList() From 768a30562729b7e0cc06a86facc399daa25cf86a Mon Sep 17 00:00:00 2001 From: Guillaume Gnaegi <58469901+ggnaegi@users.noreply.github.com> Date: Thu, 2 May 2024 09:58:08 +0200 Subject: [PATCH 19/32] Adding ConvertToNumericType method to be able to use the NumberStyles enum --- .../DownstreamRouteExtensions.cs | 68 ++++++++++++++++--- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/src/Ocelot/Configuration/DownstreamRouteExtensions.cs b/src/Ocelot/Configuration/DownstreamRouteExtensions.cs index d5aa24f17..fb5335310 100644 --- a/src/Ocelot/Configuration/DownstreamRouteExtensions.cs +++ b/src/Ocelot/Configuration/DownstreamRouteExtensions.cs @@ -1,9 +1,14 @@ -using System.Text.Json; +using System.Globalization; +using System.Reflection; +using System.Text.Json; namespace Ocelot.Configuration; public static class DownstreamRouteExtensions { + /// + /// The known truthy values + /// private static readonly HashSet TruthyValues = new(StringComparer.OrdinalIgnoreCase) { @@ -16,6 +21,9 @@ public static class DownstreamRouteExtensions "1", }; + /// + /// The known falsy values + /// private static readonly HashSet FalsyValues = new(StringComparer.OrdinalIgnoreCase) { @@ -45,26 +53,40 @@ public static class DownstreamRouteExtensions typeof(decimal), }; + /// + /// Extension method to get metadata from a downstream route. + /// + /// The metadata target type. + /// The current downstream route. + /// The metadata key in downstream route Metadata dictionary. + /// The fallback value if no value found. + /// Custom json serializer options if needed. + /// The parsed metadata value. public static T GetMetadata(this DownstreamRoute downstreamRoute, string key, T defaultValue = default, JsonSerializerOptions jsonSerializerOptions = null) { var metadata = downstreamRoute?.MetadataOptions.Metadata; - if (metadata == null || !metadata.TryGetValue(key, out var metadataValue)) + if (metadata == null || !metadata.TryGetValue(key, out var metadataValue) || metadataValue == null) { return defaultValue; } - // if the value is null, return the default value of the target type - if (metadataValue == null) - { - return default; - } - return (T)ConvertTo(typeof(T), metadataValue, downstreamRoute.MetadataOptions, jsonSerializerOptions ?? new JsonSerializerOptions(JsonSerializerDefaults.Web)); } + /// + /// Converting a string value to the target type + /// Some custom conversion has been for the following types: + /// bool, bool?, string[], numeric types + /// otherwise trying to deserialize the value using the JsonSerializer + /// + /// The target type. + /// The string value. + /// The metadata options, it includes the global configuration. + /// If needed, some custom json serializer options. + /// The converted string. private static object ConvertTo(Type targetType, string value, MetadataOptions metadataOptions, JsonSerializerOptions jsonSerializerOptions) { @@ -107,7 +129,35 @@ private static object ConvertTo(Type targetType, string value, MetadataOptions m } return NumericTypes.Contains(targetType) - ? Convert.ChangeType(value, targetType, metadataOptions.CurrentCulture) + ? ConvertToNumericType(value, targetType, metadataOptions.CurrentCulture, metadataOptions.NumberStyle) : JsonSerializer.Deserialize(value, targetType, jsonSerializerOptions); } + + /// + /// Using reflection to invoke the Parse method of the numeric type + /// + /// The number as string. + /// The target numeric type. + /// The current format provider. + /// The current number style configuration. + /// The parsed string as object of type targetType. + /// Exception thrown if the type doesn't contain a "Parse" method. This shouldn't happen. + private static object ConvertToNumericType(string value, Type targetType, IFormatProvider provider, + NumberStyles numberStyle) + { + MethodInfo parseMethod = + targetType.GetMethod("Parse", new[] { typeof(string), typeof(NumberStyles), typeof(IFormatProvider) }) ?? + throw new InvalidOperationException("No suitable parse method found."); + + try + { + // Invoke the parse method dynamically with the number style and format provider + return parseMethod.Invoke(null, new object[] { value, numberStyle, provider }); + } + catch (TargetInvocationException e) + { + // if the parse method throws an exception, rethrow the inner exception + throw e.InnerException ?? e; + } + } } From 7544f6097d9c207e620dc52f23e2358ed3a8fd4b Mon Sep 17 00:00:00 2001 From: Guillaume Gnaegi <58469901+ggnaegi@users.noreply.github.com> Date: Thu, 2 May 2024 23:26:18 +0200 Subject: [PATCH 20/32] adding first acceptance tests --- .../Configuration/Creator/MetadataCreator.cs | 13 +- .../DownstreamMetadataTests.cs | 318 ++++++++++++++++++ test/Ocelot.AcceptanceTests/Steps.cs | 28 ++ .../Configuration/MetadataCreatorTests.cs | 6 +- 4 files changed, 353 insertions(+), 12 deletions(-) create mode 100644 test/Ocelot.AcceptanceTests/DownstreamMetadataTests.cs diff --git a/src/Ocelot/Configuration/Creator/MetadataCreator.cs b/src/Ocelot/Configuration/Creator/MetadataCreator.cs index 637e1ca31..02f4985c3 100644 --- a/src/Ocelot/Configuration/Creator/MetadataCreator.cs +++ b/src/Ocelot/Configuration/Creator/MetadataCreator.cs @@ -7,22 +7,17 @@ public class MetadataCreator : IMetadataCreator { public MetadataOptions Create(IDictionary metadata, FileGlobalConfiguration fileGlobalConfiguration) { - return Create(new FileMetadataOptions { Metadata = metadata ?? new Dictionary() }, fileGlobalConfiguration); - } - - public MetadataOptions Create(FileMetadataOptions routeMetadataOptions, FileGlobalConfiguration fileGlobalConfiguration) - { - var metadata = fileGlobalConfiguration.MetadataOptions.Metadata.Any() + var mergedMetadata = fileGlobalConfiguration.MetadataOptions.Metadata.Any() ? new Dictionary(fileGlobalConfiguration.MetadataOptions.Metadata) : new Dictionary(); - foreach (var (key, value) in routeMetadataOptions.Metadata) + foreach (var (key, value) in metadata) { - metadata[key] = value; + mergedMetadata[key] = value; } return new MetadataOptionsBuilder() - .WithMetadata(metadata) + .WithMetadata(mergedMetadata) .WithSeparators(fileGlobalConfiguration.MetadataOptions.Separators) .WithTrimChars(fileGlobalConfiguration.MetadataOptions.TrimChars) .WithStringSplitOption(fileGlobalConfiguration.MetadataOptions.StringSplitOption) diff --git a/test/Ocelot.AcceptanceTests/DownstreamMetadataTests.cs b/test/Ocelot.AcceptanceTests/DownstreamMetadataTests.cs new file mode 100644 index 000000000..ad59c2293 --- /dev/null +++ b/test/Ocelot.AcceptanceTests/DownstreamMetadataTests.cs @@ -0,0 +1,318 @@ +using Microsoft.AspNetCore.Http; +using Ocelot.Configuration; +using Ocelot.Configuration.File; +using Ocelot.Middleware; + +namespace Ocelot.AcceptanceTests; + +public class DownstreamMetadataTests : IDisposable +{ + private readonly Steps _steps; + private readonly ServiceHandler _serviceHandler; + + public DownstreamMetadataTests() + { + _steps = new Steps(); + _serviceHandler = new ServiceHandler(); + } + + public void Dispose() + { + _steps?.Dispose(); + _serviceHandler?.Dispose(); + } + + [Theory] + [InlineData(typeof(StringDownStreamMetadataHandler))] + [InlineData(typeof(StringArrayDownStreamMetadataHandler))] + [InlineData(typeof(BoolDownStreamMetadataHandler))] + [InlineData(typeof(DoubleDownStreamMetadataHandler))] + [InlineData(typeof(SuperDataContainerDownStreamMetadataHandler))] + public void ShouldMatchTargetObjects(Type currentType) + { + (Dictionary sourceDictionary, Dictionary _) = + GetSourceAndTargetDictionary(currentType); + + 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" }, + Metadata = sourceDictionary, + DelegatingHandlers = new List { currentType.Name, }, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => x.GivenOcelotIsRunningWithSpecificHandlerForType(currentType)) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string url) + { + _serviceHandler.GivenThereIsAServiceRunningOn(url, context => + { + context.Response.StatusCode = 200; + return Task.CompletedTask; + }); + } + + /// + /// Starting ocelot with the delegating handler of type currentType + /// + /// The current delegating handler type. + /// Throws if delegating handler type doesn't match. + private void GivenOcelotIsRunningWithSpecificHandlerForType(Type currentType) + { + if (currentType == typeof(StringDownStreamMetadataHandler)) + { + _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); + } + else if (currentType == typeof(StringArrayDownStreamMetadataHandler)) + { + _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); + } + else if (currentType == typeof(BoolDownStreamMetadataHandler)) + { + _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); + } + else if (currentType == typeof(DoubleDownStreamMetadataHandler)) + { + _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); + } + else if (currentType == typeof(SuperDataContainerDownStreamMetadataHandler)) + { + _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); + } + else + { + throw new NotImplementedException(); + } + } + + // It would have been better to use a generic method, but it is not possible to use a generic type as a parameter + // for the delegating handler name + private class StringDownStreamMetadataHandler : DownstreamMetadataHandler + { + public StringDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) + { + } + } + + private class StringArrayDownStreamMetadataHandler : DownstreamMetadataHandler + { + public StringArrayDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base( + httpContextAccessor) + { + } + } + + private class BoolDownStreamMetadataHandler : DownstreamMetadataHandler + { + public BoolDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) + { + } + } + + private class DoubleDownStreamMetadataHandler : DownstreamMetadataHandler + { + public DoubleDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) + { + } + } + + private class SuperDataContainerDownStreamMetadataHandler : DownstreamMetadataHandler + { + public SuperDataContainerDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base( + httpContextAccessor) + { + } + } + + /// + /// Simple delegating handler that checks if the metadata is correctly passed to the downstream route + /// and checking if the extension method GetMetadata returns the correct value + /// + /// The current type. + private class DownstreamMetadataHandler : DelegatingHandler + { + private readonly IHttpContextAccessor _httpContextAccessor; + + public DownstreamMetadataHandler(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + protected override Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) + { + var downstreamRoute = _httpContextAccessor.HttpContext?.Items.DownstreamRoute(); + + (Dictionary _, Dictionary targetDictionary) = + GetSourceAndTargetDictionary(typeof(T)); + + foreach (var key in targetDictionary.Keys) + { + Assert.Equal(targetDictionary[key], downstreamRoute.GetMetadata(key)); + } + + return base.SendAsync(request, cancellationToken); + } + } + + /// + /// Method retrieving the source and target dictionary for the current type. + /// The source value is of type string and the target is of type object. + /// + /// The current type. + /// A source and a target directory to compare the results. + /// Throws if type not found. + public static (Dictionary SourceDictionary, Dictionary TargetDictionary) + GetSourceAndTargetDictionary(Type currentType) + { + Dictionary sourceDictionary; + Dictionary targetDictionary; + if (currentType == typeof(StringDownStreamMetadataHandler) || currentType == typeof(string)) + { + sourceDictionary = new Dictionary { { "Key1", "Value1" }, { "Key2", "Value2" }, }; + + targetDictionary = new Dictionary { { "Key1", "Value1" }, { "Key2", "Value2" }, }; + + return (sourceDictionary, targetDictionary); + } + + if (currentType == typeof(StringArrayDownStreamMetadataHandler) || currentType == typeof(string[])) + { + sourceDictionary = new Dictionary + { + { "Key1", "Value1, Value2, Value3" }, + { "Key2", "Value2, Value3, Value4" }, + { "Key3", "Value3, ,Value4, Value5" }, + }; + + targetDictionary = new Dictionary + { + { "Key1", new[] { "Value1", "Value2", "Value3" } }, + { "Key2", new[] { "Value2", "Value3", "Value4" } }, + { "Key3", new[] { "Value3", "Value4", "Value5" } }, + }; + + return (sourceDictionary, targetDictionary); + } + + if (currentType == typeof(BoolDownStreamMetadataHandler) || currentType == typeof(bool?)) + { + sourceDictionary = new Dictionary + { + { "Key1", "true" }, + { "Key2", "false" }, + { "Key3", "null" }, + { "Key4", "disabled" }, + { "Key5", "0" }, + { "Key6", "1" }, + { "Key7", "yes" }, + { "Key8", "enabled" }, + { "Key9", "on" }, + { "Key10", "off" }, + { "Key11", "test" }, + }; + + targetDictionary = new Dictionary + { + { "Key1", true }, + { "Key2", false }, + { "Key3", null }, + { "Key4", false }, + { "Key5", false }, + { "Key6", true }, + { "Key7", true }, + { "Key8", true }, + { "Key9", true }, + { "Key10", false }, + { "Key11", null }, + }; + + return (sourceDictionary, targetDictionary); + } + + if (currentType == typeof(DoubleDownStreamMetadataHandler) || currentType == typeof(double)) + { + sourceDictionary = new Dictionary { { "Key1", "0.00001" }, { "Key2", "0.00000001" }, }; + + targetDictionary = new Dictionary { { "Key1", 0.00001 }, { "Key2", 0.00000001 }, }; + + return (sourceDictionary, targetDictionary); + } + + if (currentType == typeof(SuperDataContainerDownStreamMetadataHandler) || currentType == typeof(SuperDataContainer)) + { + sourceDictionary = new Dictionary + { + { "Key1", "{\"key1\":\"Bonjour\",\"key2\":\"Hello\",\"key3\":0.00001,\"key4\":true}" }, + }; + + targetDictionary = new Dictionary + { + { + "Key1", + new SuperDataContainer + { + Key1 = "Bonjour", + Key2 = "Hello", + Key3 = 0.00001, + Key4 = true, + } + }, + }; + + return (sourceDictionary, targetDictionary); + } + + throw new NotImplementedException(); + } + + public class SuperDataContainer + { + public string Key1 { get; set; } + public string Key2 { get; set; } + public double Key3 { get; set; } + public bool? Key4 { get; set; } + + public override bool Equals(object obj) + { + // Check for null and compare run-time types. + if (obj == null || this.GetType() != obj.GetType()) + { + return false; + } + + SuperDataContainer other = (SuperDataContainer)obj; + return Key1 == other.Key1 && Key2 == other.Key2 && Key3.Equals(other.Key3) && Key4 == other.Key4; + } + + // https://stackoverflow.com/questions/263400/what-is-the-best-algorithm-for-overriding-gethashcode + public override int GetHashCode() + { + unchecked + { + int hash = 17; + hash = (hash * 23) + (Key1?.GetHashCode() ?? 0); + hash = (hash * 23) + (Key2?.GetHashCode() ?? 0); + hash = (hash * 23) + Key3.GetHashCode(); + hash = (hash * 23) + (Key4?.GetHashCode() ?? 0); + return hash; + } + } + } +} diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 7f95a8a9d..7f6243502 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -29,6 +29,7 @@ using Serilog.Core; using System.IO.Compression; using System.Net.Http.Headers; +using System.Security.Policy; using System.Text; using static Ocelot.AcceptanceTests.HttpDelegatingHandlersTests; using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; @@ -476,6 +477,33 @@ public void GivenOcelotIsRunningWithMiddlewareBeforePipeline(Func() + where T: DelegatingHandler + { + _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.AddSingleton(_webHostBuilder); + s.AddOcelot() + .AddDelegatingHandler(); + }) + .Configure(a => { a.UseOcelot().Wait(); }); + + _ocelotServer = new TestServer(_webHostBuilder); + _ocelotClient = _ocelotServer.CreateClient(); + } + public void GivenOcelotIsRunningWithSpecificHandlersRegisteredInDi() where TOne : DelegatingHandler where TWo : DelegatingHandler diff --git a/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs index b72129ee6..6d520e3f6 100644 --- a/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs @@ -49,7 +49,7 @@ public void Should_overwrite_global_metadata() private void WhenICreate() { - _result = _sut.Create(new FileMetadataOptions{Metadata = _metadataInRoute}, _globalConfiguration); + _result = _sut.Create( _metadataInRoute, _globalConfiguration); } private void GivenEmptyMetadataInGlobalConfiguration() @@ -59,14 +59,14 @@ private void GivenEmptyMetadataInGlobalConfiguration() private void GivenSomeMetadataInGlobalConfiguration() { - _globalConfiguration = new FileGlobalConfiguration() + _globalConfiguration = new FileGlobalConfiguration { MetadataOptions = new FileMetadataOptions { Metadata = new Dictionary { ["foo"] = "bar", - } + }, }, }; } From 7f2f575c14d48439eb35b83cd31200618562bc91 Mon Sep 17 00:00:00 2001 From: Guillaume Gnaegi <58469901+ggnaegi@users.noreply.github.com> Date: Thu, 2 May 2024 23:53:49 +0200 Subject: [PATCH 21/32] The tests are now passing again... --- src/Ocelot/Configuration/Creator/MetadataCreator.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Ocelot/Configuration/Creator/MetadataCreator.cs b/src/Ocelot/Configuration/Creator/MetadataCreator.cs index 02f4985c3..e26ae71ec 100644 --- a/src/Ocelot/Configuration/Creator/MetadataCreator.cs +++ b/src/Ocelot/Configuration/Creator/MetadataCreator.cs @@ -7,9 +7,11 @@ public class MetadataCreator : IMetadataCreator { public MetadataOptions Create(IDictionary metadata, FileGlobalConfiguration fileGlobalConfiguration) { - var mergedMetadata = fileGlobalConfiguration.MetadataOptions.Metadata.Any() - ? new Dictionary(fileGlobalConfiguration.MetadataOptions.Metadata) - : new Dictionary(); + // metadata from the route could be null when no metadata is defined + metadata ??= new Dictionary(); + + // metadata from the global configuration is never null + var mergedMetadata = new Dictionary(fileGlobalConfiguration.MetadataOptions.Metadata); foreach (var (key, value) in metadata) { From a3a2725beb5f10edeb7c053343633777cc03b944 Mon Sep 17 00:00:00 2001 From: Guillaume Gnaegi <58469901+ggnaegi@users.noreply.github.com> Date: Fri, 3 May 2024 13:11:11 +0200 Subject: [PATCH 22/32] adding latest test cases. That should be enough (includes global configuration changes too) --- .../DownstreamMetadataTests.cs | 369 ++++++++++++++++-- 1 file changed, 330 insertions(+), 39 deletions(-) diff --git a/test/Ocelot.AcceptanceTests/DownstreamMetadataTests.cs b/test/Ocelot.AcceptanceTests/DownstreamMetadataTests.cs index ad59c2293..291932151 100644 --- a/test/Ocelot.AcceptanceTests/DownstreamMetadataTests.cs +++ b/test/Ocelot.AcceptanceTests/DownstreamMetadataTests.cs @@ -2,6 +2,7 @@ using Ocelot.Configuration; using Ocelot.Configuration.File; using Ocelot.Middleware; +using System.Globalization; namespace Ocelot.AcceptanceTests; @@ -10,6 +11,22 @@ public class DownstreamMetadataTests : IDisposable private readonly Steps _steps; private readonly ServiceHandler _serviceHandler; + public enum StringArrayConfig + { + Default = 1, + AlternateSeparators, + AlternateTrimChars, + AlternateStringSplitOptions, + Mix, + } + + public enum NumberConfig + { + Default = 1, + AlternateNumberStyle, + AlternateCulture, + } + public DownstreamMetadataTests() { _steps = new Steps(); @@ -59,13 +76,134 @@ public void ShouldMatchTargetObjects(Type currentType) .BDDfy(); } - private void GivenThereIsAServiceRunningOn(string url) + /// + /// Testing the string array type with different configurations. + /// + /// The possible separators. + /// The trimmed characters. + /// If the empty entries should be removed. + /// The current test configuration. + [Theory] + [InlineData(new[] { "," }, new[] { ' ' }, nameof(StringSplitOptions.None), StringArrayConfig.Default)] + [InlineData( + new[] { ";", ".", "," }, + new[] { ' ' }, + nameof(StringSplitOptions.None), + StringArrayConfig.AlternateSeparators)] + [InlineData( + new[] { "," }, + new[] { ' ', ';', ':' }, + nameof(StringSplitOptions.None), + StringArrayConfig.AlternateTrimChars)] + [InlineData( + new[] { "," }, + new[] { ' ' }, + nameof(StringSplitOptions.RemoveEmptyEntries), + StringArrayConfig.AlternateStringSplitOptions)] + [InlineData( + new[] { ";", ".", "," }, + new[] { ' ', '_', ':' }, + nameof(StringSplitOptions.RemoveEmptyEntries), + StringArrayConfig.Mix)] + public void ShouldMatchTargetStringArrayAccordingToConfiguration( + string[] separators, + char[] trimChars, + string stringSplitOption, + StringArrayConfig currentConfig) { - _serviceHandler.GivenThereIsAServiceRunningOn(url, context => + (Dictionary sourceDictionary, Dictionary _) = + GetSourceAndTargetDictionariesForStringArrayType(currentConfig); + + sourceDictionary.Add(nameof(StringArrayConfig), currentConfig.ToString()); + + var port = PortFinder.GetRandomPort(); + var configuration = new FileConfiguration { - context.Response.StatusCode = 200; - return Task.CompletedTask; - }); + Routes = new List + { + new() + { + DownstreamPathTemplate = "/", + DownstreamHostAndPorts = new List { new() { Host = "localhost", Port = port, }, }, + DownstreamScheme = "http", + UpstreamPathTemplate = "/", + UpstreamHttpMethod = new List { "Get" }, + Metadata = sourceDictionary, + DelegatingHandlers = new List { nameof(StringArrayDownStreamMetadataHandler) }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + MetadataOptions = new FileMetadataOptions + { + Separators = separators, TrimChars = trimChars, StringSplitOption = stringSplitOption, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => x.GivenOcelotIsRunningWithSpecificHandlerForType(typeof(StringArrayDownStreamMetadataHandler))) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + [Theory] + [InlineData(NumberStyles.Any, "de-CH", NumberConfig.Default)] + [InlineData(NumberStyles.AllowParentheses | NumberStyles.AllowLeadingWhite | NumberStyles.AllowTrailingWhite | NumberStyles.AllowLeadingSign, "de-CH", NumberConfig.AlternateNumberStyle)] + public void ShouldMatchTargetNumberAccordingToConfiguration( + NumberStyles numberStyles, + string cultureName, + NumberConfig currentConfig) + { + (Dictionary sourceDictionary, Dictionary _) = + GetSourceAndTargetDictionariesForNumberType(); + + sourceDictionary.Add(nameof(NumberConfig), currentConfig.ToString()); + + 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" }, + Metadata = sourceDictionary, + DelegatingHandlers = new List { nameof(IntDownStreamMetadataHandler) }, + }, + }, + GlobalConfiguration = new FileGlobalConfiguration + { + MetadataOptions = new FileMetadataOptions + { + NumberStyle = numberStyles.ToString(), CurrentCulture = cultureName, + }, + }, + }; + + this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}")) + .And(x => _steps.GivenThereIsAConfiguration(configuration)) + .And(x => x.GivenOcelotIsRunningWithSpecificHandlerForType(typeof(IntDownStreamMetadataHandler))) + .When(x => _steps.WhenIGetUrlOnTheApiGateway($"/")) + .Then(x => _steps.ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .BDDfy(); + } + + private void GivenThereIsAServiceRunningOn(string url) + { + _serviceHandler.GivenThereIsAServiceRunningOn( + url, + context => + { + context.Response.StatusCode = 200; + return Task.CompletedTask; + }); } /// @@ -75,29 +213,30 @@ private void GivenThereIsAServiceRunningOn(string url) /// Throws if delegating handler type doesn't match. private void GivenOcelotIsRunningWithSpecificHandlerForType(Type currentType) { - if (currentType == typeof(StringDownStreamMetadataHandler)) - { - _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); - } - else if (currentType == typeof(StringArrayDownStreamMetadataHandler)) - { - _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); - } - else if (currentType == typeof(BoolDownStreamMetadataHandler)) - { - _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); - } - else if (currentType == typeof(DoubleDownStreamMetadataHandler)) + switch (currentType) { - _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); - } - else if (currentType == typeof(SuperDataContainerDownStreamMetadataHandler)) - { - _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); - } - else - { - throw new NotImplementedException(); + case { } t when t == typeof(StringDownStreamMetadataHandler): + _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); + break; + case { } t when t == typeof(StringArrayDownStreamMetadataHandler): + _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); + break; + case { } t when t == typeof(BoolDownStreamMetadataHandler): + _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); + break; + case { } t when t == typeof(DoubleDownStreamMetadataHandler): + _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); + break; + case { } t when t == typeof(SuperDataContainerDownStreamMetadataHandler): + _steps + .GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi< + SuperDataContainerDownStreamMetadataHandler>(); + break; + case { } t when t == typeof(IntDownStreamMetadataHandler): + _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); + break; + default: + throw new NotImplementedException(); } } @@ -132,6 +271,13 @@ public DoubleDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) } } + private class IntDownStreamMetadataHandler : DownstreamMetadataHandler + { + public IntDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) + { + } + } + private class SuperDataContainerDownStreamMetadataHandler : DownstreamMetadataHandler { public SuperDataContainerDownStreamMetadataHandler(IHttpContextAccessor httpContextAccessor) : base( @@ -154,23 +300,168 @@ public DownstreamMetadataHandler(IHttpContextAccessor httpContextAccessor) _httpContextAccessor = httpContextAccessor; } - protected override Task SendAsync(HttpRequestMessage request, + protected override Task SendAsync( + HttpRequestMessage request, CancellationToken cancellationToken) { var downstreamRoute = _httpContextAccessor.HttpContext?.Items.DownstreamRoute(); - (Dictionary _, Dictionary targetDictionary) = - GetSourceAndTargetDictionary(typeof(T)); + if (downstreamRoute.MetadataOptions.Metadata.ContainsKey(nameof(StringArrayConfig))) + { + var currentConfig = + Enum.Parse(downstreamRoute.MetadataOptions.Metadata[nameof(StringArrayConfig)]); + downstreamRoute.MetadataOptions.Metadata.Remove(nameof(StringArrayConfig)); + + (Dictionary _, Dictionary targetDictionary) = + GetSourceAndTargetDictionariesForStringArrayType(currentConfig); - foreach (var key in targetDictionary.Keys) + foreach (var key in targetDictionary.Keys) + { + Assert.Equal(targetDictionary[key], downstreamRoute.GetMetadata(key)); + } + } + else if (downstreamRoute.MetadataOptions.Metadata.ContainsKey(nameof(NumberConfig))) { - Assert.Equal(targetDictionary[key], downstreamRoute.GetMetadata(key)); + downstreamRoute.MetadataOptions.Metadata.Remove(nameof(NumberConfig)); + + (Dictionary _, Dictionary targetDictionary) = + GetSourceAndTargetDictionariesForNumberType(); + + foreach (var key in targetDictionary.Keys) + { + Assert.Equal(targetDictionary[key], downstreamRoute.GetMetadata(key)); + } + } + else + { + (Dictionary _, Dictionary targetDictionary) = + GetSourceAndTargetDictionary(typeof(T)); + + foreach (var key in targetDictionary.Keys) + { + Assert.Equal(targetDictionary[key], downstreamRoute.GetMetadata(key)); + } } return base.SendAsync(request, cancellationToken); } } + public static (Dictionary SourceDictionary, Dictionary TargetDictionary) + GetSourceAndTargetDictionariesForStringArrayType(StringArrayConfig currentConfig) + { + Dictionary sourceDictionary; + Dictionary targetDictionary; + + if (currentConfig == StringArrayConfig.Default) + { + sourceDictionary = new Dictionary + { + { "Key1", "Value1, Value2, Value3" }, + { "Key2", "Value2, Value3, Value4" }, + { "Key3", "Value3, ,Value4, Value5" }, + }; + + targetDictionary = new Dictionary + { + { "Key1", new[] { "Value1", "Value2", "Value3" } }, + { "Key2", new[] { "Value2", "Value3", "Value4" } }, + { "Key3", new[] { "Value3", "Value4", "Value5" } }, + }; + + return (sourceDictionary, targetDictionary); + } + + if (currentConfig == StringArrayConfig.AlternateSeparators) + { + sourceDictionary = new Dictionary + { + { "Key1", "Value1; Value2. Value3" }, + { "Key2", "Value2. Value3, Value4" }, + { "Key3", "Value3, ,Value4; Value5" }, + }; + + targetDictionary = new Dictionary + { + { "Key1", new[] { "Value1", "Value2", "Value3" } }, + { "Key2", new[] { "Value2", "Value3", "Value4" } }, + { "Key3", new[] { "Value3", "Value4", "Value5" } }, + }; + + return (sourceDictionary, targetDictionary); + } + + if (currentConfig == StringArrayConfig.AlternateTrimChars) + { + sourceDictionary = new Dictionary + { + { "Key1", "Value1; :, Value2 :, Value3 " }, + { "Key2", " Value2, Value3; , Value4" }, + { "Key3", "Value3 , ,Value4, Value5 " }, + }; + + targetDictionary = new Dictionary + { + { "Key1", new[] { "Value1", "Value2", "Value3" } }, + { "Key2", new[] { "Value2", "Value3", "Value4" } }, + { "Key3", new[] { "Value3", "Value4", "Value5" } }, + }; + + return (sourceDictionary, targetDictionary); + } + + if (currentConfig == StringArrayConfig.AlternateStringSplitOptions) + { + sourceDictionary = new Dictionary + { + { "Key1", "Value1, ,Value2, Value3, " }, + { "Key2", "Value2, , ,Value3, Value4, , ," }, + { "Key3", "Value3, ,Value4, , ,Value5" }, + }; + + targetDictionary = new Dictionary + { + { "Key1", new[] { "Value1", "Value2", "Value3" } }, + { "Key2", new[] { "Value2", "Value3", "Value4" } }, + { "Key3", new[] { "Value3", "Value4", "Value5" } }, + }; + + return (sourceDictionary, targetDictionary); + } + + if (currentConfig == StringArrayConfig.Mix) + { + sourceDictionary = new Dictionary + { + { "Key1", "Value1; :, Value2. :, Value3 " }, + { "Key2", " Value2_, , , Value3; , Value4" }, + { "Key3", "Value3:; , ,Value4, Value5 " }, + }; + + targetDictionary = new Dictionary + { + { "Key1", new[] { "Value1", "Value2", "Value3" } }, + { "Key2", new[] { "Value2", "Value3", "Value4" } }, + { "Key3", new[] { "Value3", "Value4", "Value5" } }, + }; + + return (sourceDictionary, targetDictionary); + } + + throw new NotImplementedException(); + } + + public static (Dictionary SourceDictionary, Dictionary TargetDictionary) + GetSourceAndTargetDictionariesForNumberType() + { + return ( + new Dictionary + { + { "Key1", "-2" }, { "Key2", " (1000000) " }, { "Key3", "-1000000000 " }, + }, + new Dictionary { { "Key1", -2 }, { "Key2", -1000000 }, { "Key3", -1000000000 } }); + } + /// /// Method retrieving the source and target dictionary for the current type. /// The source value is of type string and the target is of type object. @@ -255,7 +546,8 @@ public static (Dictionary SourceDictionary, Dictionary { @@ -265,13 +557,9 @@ public static (Dictionary SourceDictionary, Dictionary { { - "Key1", - new SuperDataContainer + "Key1", new SuperDataContainer { - Key1 = "Bonjour", - Key2 = "Hello", - Key3 = 0.00001, - Key4 = true, + Key1 = "Bonjour", Key2 = "Hello", Key3 = 0.00001, Key4 = true, } }, }; @@ -285,8 +573,11 @@ public static (Dictionary SourceDictionary, Dictionary Date: Fri, 3 May 2024 00:33:38 +0200 Subject: [PATCH 23/32] Update metadata.rst --- docs/features/metadata.rst | 42 +++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/docs/features/metadata.rst b/docs/features/metadata.rst index 080e59259..f1930906e 100644 --- a/docs/features/metadata.rst +++ b/docs/features/metadata.rst @@ -55,10 +55,10 @@ Now, the route metadata can be accessed through the ``DownstreamRoute`` object: public Task Invoke(HttpContext context, Func next) { var route = context.Items.DownstreamRoute(); - var enabled = metadata.IsMetadataValueTruthy("plugin1.enabled"); - var values = metadata.GetMetadataValues("plugin1.values"); - var param1 = metadata.GetMetadataValue("plugin1.param", "system-default-value"); - var param2 = metadata.GetMetadataNumber("plugin1.param2"); + var enabled = route.GetMetadata("plugin1.enabled"); + var values = route.GetMetadata("plugin1.values"); + var param1 = route.GetMetadata("plugin1.param", "system-default-value"); + var param2 = route.GetMetadata("plugin1.param2"); // working on the plugin1's function @@ -69,7 +69,9 @@ Now, the route metadata can be accessed through the ``DownstreamRoute`` object: Extension Methods ----------------- -Ocelot provides some extension methods help you to retrieve your metadata values effortlessly. +Ocelot provides one DowstreamRoute extension method to help you retrieve your metadata values effortlessly. +With the exception of the types string, bool, bool?, string[] and numeric, all strings passed as parameters are treated as json strings and an attempt is made to convert them into objects of generic type T. +If the value is null, then, if not explicitely specified, the default for the chosen target type is returned. .. list-table:: :widths: 20 40 40 @@ -77,20 +79,22 @@ Ocelot provides some extension methods help you to retrieve your metadata values * - Method - Description - Notes - * - ``GetMetadataValue`` - - The metadata value is a string. - - - * - ``GetMetadataValues`` - - The metadata value is spitted by a given separator (default ``,``) and + * - ``GetMetadata`` + - The metadata value is returned as string without further parsing + * - ``GetMetadata`` + - The metadata value is splitted by a given separator (default ``,``) and returned as a string array. - - - * - ``GetMetadataNumber`` + - Several parameters can be set in the global configuration, such as Separators (default = ``[","]``), StringSplitOptions (default ``None``) and TrimChars, the characters that should be trimmed (default = ``[' ']``). + * - ``GetMetadata`` - The metadata value is parsed to a number. - - | Only available in .NET 7 or above. - | For .NET 6, use ``GetMetadataFromJson<>``. - * - ``GetMetadataFromJson`` - - The metadata value is serialized to the given generic type. - - - * - ``IsMetadataValueTruthy`` - - Check if the metadata value is a truthy value. + - Some parameters can be set in the global configuration, such as NumberStyle (default ``Any``) and CurrentCulture (default ``CultureInfo.CurrentCulture``) + * - ``GetMetadata`` + - The metadata value is converted to the given generic type. The value is treated as a json string and the json serializer tries to deserialize the string to the target type. + - A JsonSerializerOptions object can be passed as method parameter, Web is used as default. + * - ``GetMetadata`` + - Check if the metadata value is a truthy value, otherwise return false. - The truthy values are: ``true``, ``yes``, ``ok``, ``on``, ``enable``, ``enabled`` + * - ``GetMetadata`` + - Check if the metadata value is a truthy value (return true), or falsy value (return false), otherwise return null. + - The known truthy values are: ``true``, ``yes``, ``ok``, ``on``, ``enable``, ``enabled``, ``1``, the known falsy values are: ``false``, ``no``, ``off``, ``disable``, ``disabled``, ``0`` + From c9724e2f35436908eef4a77e0fbff84e6341389c Mon Sep 17 00:00:00 2001 From: Guillaume Gnaegi <58469901+ggnaegi@users.noreply.github.com> Date: Fri, 3 May 2024 14:03:17 +0200 Subject: [PATCH 24/32] adding the xml docs for IMetadataCreator and MetadataCreator --- src/Ocelot/Configuration/Creator/IMetadataCreator.cs | 3 +++ src/Ocelot/Configuration/Creator/MetadataCreator.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/Ocelot/Configuration/Creator/IMetadataCreator.cs b/src/Ocelot/Configuration/Creator/IMetadataCreator.cs index 7b9291a40..d15c4e8b3 100644 --- a/src/Ocelot/Configuration/Creator/IMetadataCreator.cs +++ b/src/Ocelot/Configuration/Creator/IMetadataCreator.cs @@ -2,6 +2,9 @@ namespace Ocelot.Configuration.Creator; +/// +/// This interface describes the creation of metadata options. +/// public interface IMetadataCreator { MetadataOptions Create(IDictionary metadata, FileGlobalConfiguration fileGlobalConfiguration); diff --git a/src/Ocelot/Configuration/Creator/MetadataCreator.cs b/src/Ocelot/Configuration/Creator/MetadataCreator.cs index e26ae71ec..1055f54df 100644 --- a/src/Ocelot/Configuration/Creator/MetadataCreator.cs +++ b/src/Ocelot/Configuration/Creator/MetadataCreator.cs @@ -3,6 +3,9 @@ namespace Ocelot.Configuration.Creator; +/// +/// This class implements the interface. +/// public class MetadataCreator : IMetadataCreator { public MetadataOptions Create(IDictionary metadata, FileGlobalConfiguration fileGlobalConfiguration) From 0d379628c29d45706bb295a39b7aa71079d6745b Mon Sep 17 00:00:00 2001 From: Guillaume Gnaegi <58469901+ggnaegi@users.noreply.github.com> Date: Fri, 3 May 2024 14:05:25 +0200 Subject: [PATCH 25/32] renaming MetadataCreator to DefaultMetadataCreator --- .../Creator/{MetadataCreator.cs => DefaultMetadataCreator.cs} | 2 +- src/Ocelot/DependencyInjection/OcelotBuilder.cs | 2 +- ...MetadataCreatorTests.cs => DefaultMetadataCreatorTests.cs} | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/Ocelot/Configuration/Creator/{MetadataCreator.cs => DefaultMetadataCreator.cs} (95%) rename test/Ocelot.UnitTests/Configuration/{MetadataCreatorTests.cs => DefaultMetadataCreatorTests.cs} (96%) diff --git a/src/Ocelot/Configuration/Creator/MetadataCreator.cs b/src/Ocelot/Configuration/Creator/DefaultMetadataCreator.cs similarity index 95% rename from src/Ocelot/Configuration/Creator/MetadataCreator.cs rename to src/Ocelot/Configuration/Creator/DefaultMetadataCreator.cs index 1055f54df..b5f159f85 100644 --- a/src/Ocelot/Configuration/Creator/MetadataCreator.cs +++ b/src/Ocelot/Configuration/Creator/DefaultMetadataCreator.cs @@ -6,7 +6,7 @@ namespace Ocelot.Configuration.Creator; /// /// This class implements the interface. /// -public class MetadataCreator : IMetadataCreator +public class DefaultMetadataCreator : IMetadataCreator { public MetadataOptions Create(IDictionary metadata, FileGlobalConfiguration fileGlobalConfiguration) { diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 312302bea..195c7ff19 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -117,7 +117,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton, OcelotConfigurationMonitor>(); - Services.TryAddSingleton(); + Services.TryAddSingleton(); Services.AddOcelotMessageInvokerPool(); // See this for why we register this as singleton: diff --git a/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs similarity index 96% rename from test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs rename to test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs index 6d520e3f6..89f98693e 100644 --- a/test/Ocelot.UnitTests/Configuration/MetadataCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs @@ -4,12 +4,12 @@ namespace Ocelot.UnitTests.Configuration; -public class MetadataCreatorTests : UnitTest +public class DefaultMetadataCreatorTests : UnitTest { private FileGlobalConfiguration _globalConfiguration; private Dictionary _metadataInRoute; private MetadataOptions _result; - private readonly MetadataCreator _sut = new(); + private readonly DefaultMetadataCreator _sut = new(); [Fact] public void Should_return_empty_metadata() From 3c624fa3ddc803178a31b6f6ca89a8885a0ffef7 Mon Sep 17 00:00:00 2001 From: Guillaume Gnaegi <58469901+ggnaegi@users.noreply.github.com> Date: Fri, 3 May 2024 14:13:39 +0200 Subject: [PATCH 26/32] number tests for .net 6 too --- .../Configuration/DownstreamRouteExtensionsTests.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs index 78b7624b2..0c840b256 100644 --- a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs @@ -198,8 +198,6 @@ record FakeObject public DateTime MyTime { get; set; } } -#if NET7_0_OR_GREATER - [Theory] [InlineData("0", 0)] [InlineData("99", 99)] @@ -217,7 +215,6 @@ record FakeObject public void Should_parse_double(string value, double expected) => Should_parse_number(value, expected); private void Should_parse_number(string value, T expected) - where T : INumberBase { // Arrange var key = "mykey"; @@ -246,8 +243,6 @@ public void Should_throw_error_when_invalid_number() }); } -#endif - [Theory] [InlineData("true", true)] [InlineData("yes", true)] From 0279fe3d7600f2b98d1fbad91754c0780dc0a1cd Mon Sep 17 00:00:00 2001 From: Guillaume Gnaegi <58469901+ggnaegi@users.noreply.github.com> Date: Fri, 3 May 2024 14:36:19 +0200 Subject: [PATCH 27/32] Moving Metadata specific downstream route extensions to the Metadata folder --- .../{Configuration => Metadata}/DownstreamRouteExtensions.cs | 5 +++-- test/Ocelot.AcceptanceTests/DownstreamMetadataTests.cs | 2 +- .../Configuration/DownstreamRouteExtensionsTests.cs | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) rename src/Ocelot/{Configuration => Metadata}/DownstreamRouteExtensions.cs (98%) diff --git a/src/Ocelot/Configuration/DownstreamRouteExtensions.cs b/src/Ocelot/Metadata/DownstreamRouteExtensions.cs similarity index 98% rename from src/Ocelot/Configuration/DownstreamRouteExtensions.cs rename to src/Ocelot/Metadata/DownstreamRouteExtensions.cs index fb5335310..8243fd27e 100644 --- a/src/Ocelot/Configuration/DownstreamRouteExtensions.cs +++ b/src/Ocelot/Metadata/DownstreamRouteExtensions.cs @@ -1,8 +1,9 @@ -using System.Globalization; +using Ocelot.Configuration; +using System.Globalization; using System.Reflection; using System.Text.Json; -namespace Ocelot.Configuration; +namespace Ocelot.Metadata; public static class DownstreamRouteExtensions { diff --git a/test/Ocelot.AcceptanceTests/DownstreamMetadataTests.cs b/test/Ocelot.AcceptanceTests/DownstreamMetadataTests.cs index 291932151..72bf5e685 100644 --- a/test/Ocelot.AcceptanceTests/DownstreamMetadataTests.cs +++ b/test/Ocelot.AcceptanceTests/DownstreamMetadataTests.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Http; -using Ocelot.Configuration; using Ocelot.Configuration.File; +using Ocelot.Metadata; using Ocelot.Middleware; using System.Globalization; diff --git a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs index 0c840b256..c6a90b948 100644 --- a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs @@ -1,6 +1,7 @@ using Ocelot.Configuration; using Ocelot.Configuration.Creator; using Ocelot.Configuration.File; +using Ocelot.Metadata; using Ocelot.Values; using System.Numerics; using System.Text.Json; From a4db7043087c7b7e4616c5abdc859f28380929cd Mon Sep 17 00:00:00 2001 From: Guillaume Gnaegi <58469901+ggnaegi@users.noreply.github.com> Date: Fri, 3 May 2024 14:37:56 +0200 Subject: [PATCH 28/32] cleanup --- .../Configuration/DownstreamRouteExtensionsTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs index c6a90b948..bab8742ad 100644 --- a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs @@ -3,7 +3,6 @@ using Ocelot.Configuration.File; using Ocelot.Metadata; using Ocelot.Values; -using System.Numerics; using System.Text.Json; namespace Ocelot.UnitTests.Configuration; From 8bd1dbc1f371de553602936ab923adce5aa0ffdd Mon Sep 17 00:00:00 2001 From: Guillaume Gnaegi <58469901+ggnaegi@users.noreply.github.com> Date: Fri, 17 May 2024 23:16:58 +0200 Subject: [PATCH 29/32] applying some of the requested changes --- src/Ocelot/DependencyInjection/Features.cs | 8 +++++ .../DependencyInjection/OcelotBuilder.cs | 3 +- .../Metadata/DownstreamRouteExtensions.cs | 31 ++++++++++--------- .../DownstreamMetadataTests.cs | 9 ++++-- 4 files changed, 32 insertions(+), 19 deletions(-) rename test/Ocelot.AcceptanceTests/{ => DownstreamMetadata}/DownstreamMetadataTests.cs (98%) diff --git a/src/Ocelot/DependencyInjection/Features.cs b/src/Ocelot/DependencyInjection/Features.cs index b1abdd9b5..a14460306 100644 --- a/src/Ocelot/DependencyInjection/Features.cs +++ b/src/Ocelot/DependencyInjection/Features.cs @@ -15,4 +15,12 @@ public static IServiceCollection AddHeaderRouting(this IServiceCollection servic .AddSingleton() .AddSingleton() .AddSingleton(); + + /// + /// Ocelot feature: Inject custom metadata and use it in delegating handlers. + /// + /// The services collection to add the feature to. + /// The same object. + public static IServiceCollection AddOcelotMetadata(this IServiceCollection services) => + services.AddSingleton(); } diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs index 195c7ff19..f0a2249b8 100644 --- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs +++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs @@ -117,7 +117,8 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo Services.TryAddSingleton(); Services.TryAddSingleton(); Services.TryAddSingleton, OcelotConfigurationMonitor>(); - Services.TryAddSingleton(); + + Services.AddOcelotMetadata(); Services.AddOcelotMessageInvokerPool(); // See this for why we register this as singleton: diff --git a/src/Ocelot/Metadata/DownstreamRouteExtensions.cs b/src/Ocelot/Metadata/DownstreamRouteExtensions.cs index 8243fd27e..b523b18ad 100644 --- a/src/Ocelot/Metadata/DownstreamRouteExtensions.cs +++ b/src/Ocelot/Metadata/DownstreamRouteExtensions.cs @@ -135,30 +135,31 @@ private static object ConvertTo(Type targetType, string value, MetadataOptions m } /// - /// Using reflection to invoke the Parse method of the numeric type + /// Converting string to the known numeric types /// /// The number as string. /// The target numeric type. /// The current format provider. /// The current number style configuration. /// The parsed string as object of type targetType. - /// Exception thrown if the type doesn't contain a "Parse" method. This shouldn't happen. + /// Exception thrown if the conversion for the type target type can't be found. private static object ConvertToNumericType(string value, Type targetType, IFormatProvider provider, NumberStyles numberStyle) { - MethodInfo parseMethod = - targetType.GetMethod("Parse", new[] { typeof(string), typeof(NumberStyles), typeof(IFormatProvider) }) ?? - throw new InvalidOperationException("No suitable parse method found."); - - try - { - // Invoke the parse method dynamically with the number style and format provider - return parseMethod.Invoke(null, new object[] { value, numberStyle, provider }); - } - catch (TargetInvocationException e) + return targetType switch { - // if the parse method throws an exception, rethrow the inner exception - throw e.InnerException ?? e; - } + { } t when t == typeof(byte) => byte.Parse(value, numberStyle, provider), + { } t when t == typeof(sbyte) => sbyte.Parse(value, numberStyle, provider), + { } t when t == typeof(short) => short.Parse(value, numberStyle, provider), + { } t when t == typeof(ushort) => ushort.Parse(value, numberStyle, provider), + { } t when t == typeof(int) => int.Parse(value, numberStyle, provider), + { } t when t == typeof(uint) => uint.Parse(value, numberStyle, provider), + { } t when t == typeof(long) => long.Parse(value, numberStyle, provider), + { } t when t == typeof(ulong) => ulong.Parse(value, numberStyle, provider), + { } t when t == typeof(float) => float.Parse(value, numberStyle, provider), + { } t when t == typeof(double) => double.Parse(value, numberStyle, provider), + { } t when t == typeof(decimal) => decimal.Parse(value, numberStyle, provider), + _ => throw new NotImplementedException($"No conversion available for the type: {targetType.Name}"), + }; } } diff --git a/test/Ocelot.AcceptanceTests/DownstreamMetadataTests.cs b/test/Ocelot.AcceptanceTests/DownstreamMetadata/DownstreamMetadataTests.cs similarity index 98% rename from test/Ocelot.AcceptanceTests/DownstreamMetadataTests.cs rename to test/Ocelot.AcceptanceTests/DownstreamMetadata/DownstreamMetadataTests.cs index 72bf5e685..b944c2d3d 100644 --- a/test/Ocelot.AcceptanceTests/DownstreamMetadataTests.cs +++ b/test/Ocelot.AcceptanceTests/DownstreamMetadata/DownstreamMetadataTests.cs @@ -4,7 +4,7 @@ using Ocelot.Middleware; using System.Globalization; -namespace Ocelot.AcceptanceTests; +namespace Ocelot.AcceptanceTests.DownstreamMetadata; public class DownstreamMetadataTests : IDisposable { @@ -136,7 +136,9 @@ public void ShouldMatchTargetStringArrayAccordingToConfiguration( { MetadataOptions = new FileMetadataOptions { - Separators = separators, TrimChars = trimChars, StringSplitOption = stringSplitOption, + Separators = separators, + TrimChars = trimChars, + StringSplitOption = stringSplitOption, }, }, }; @@ -182,7 +184,8 @@ public void ShouldMatchTargetNumberAccordingToConfiguration( { MetadataOptions = new FileMetadataOptions { - NumberStyle = numberStyles.ToString(), CurrentCulture = cultureName, + NumberStyle = numberStyles.ToString(), + CurrentCulture = cultureName, }, }, }; From 987c28847c5f5c0020e065230af325d7970400fb Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Tue, 21 May 2024 22:26:48 +0300 Subject: [PATCH 30/32] Final code review by @raman-m --- .../Creator/DefaultMetadataCreator.cs | 15 +++--- .../Configuration/Creator/IMetadataCreator.cs | 2 +- .../Metadata/DownstreamRouteExtensions.cs | 14 ++--- .../EurekaServiceDiscoveryTests.cs | 6 +-- .../HttpDelegatingHandlersTests.cs | 2 +- .../DownstreamMetadataTests.cs | 20 ++++--- test/Ocelot.AcceptanceTests/Steps.cs | 31 +---------- .../DefaultMetadataCreatorTests.cs | 52 +++++++++++++------ 8 files changed, 67 insertions(+), 75 deletions(-) rename test/Ocelot.AcceptanceTests/{DownstreamMetadata => Metadata}/DownstreamMetadataTests.cs (96%) diff --git a/src/Ocelot/Configuration/Creator/DefaultMetadataCreator.cs b/src/Ocelot/Configuration/Creator/DefaultMetadataCreator.cs index b5f159f85..4ffcafe67 100644 --- a/src/Ocelot/Configuration/Creator/DefaultMetadataCreator.cs +++ b/src/Ocelot/Configuration/Creator/DefaultMetadataCreator.cs @@ -8,13 +8,14 @@ namespace Ocelot.Configuration.Creator; /// public class DefaultMetadataCreator : IMetadataCreator { - public MetadataOptions Create(IDictionary metadata, FileGlobalConfiguration fileGlobalConfiguration) + public MetadataOptions Create(IDictionary metadata, FileGlobalConfiguration globalConfiguration) { // metadata from the route could be null when no metadata is defined metadata ??= new Dictionary(); // metadata from the global configuration is never null - var mergedMetadata = new Dictionary(fileGlobalConfiguration.MetadataOptions.Metadata); + var options = globalConfiguration.MetadataOptions; + var mergedMetadata = new Dictionary(options.Metadata); foreach (var (key, value) in metadata) { @@ -23,11 +24,11 @@ public MetadataOptions Create(IDictionary metadata, FileGlobalCo return new MetadataOptionsBuilder() .WithMetadata(mergedMetadata) - .WithSeparators(fileGlobalConfiguration.MetadataOptions.Separators) - .WithTrimChars(fileGlobalConfiguration.MetadataOptions.TrimChars) - .WithStringSplitOption(fileGlobalConfiguration.MetadataOptions.StringSplitOption) - .WithNumberStyle(fileGlobalConfiguration.MetadataOptions.NumberStyle) - .WithCurrentCulture(fileGlobalConfiguration.MetadataOptions.CurrentCulture) + .WithSeparators(options.Separators) + .WithTrimChars(options.TrimChars) + .WithStringSplitOption(options.StringSplitOption) + .WithNumberStyle(options.NumberStyle) + .WithCurrentCulture(options.CurrentCulture) .Build(); } } diff --git a/src/Ocelot/Configuration/Creator/IMetadataCreator.cs b/src/Ocelot/Configuration/Creator/IMetadataCreator.cs index d15c4e8b3..5252d620b 100644 --- a/src/Ocelot/Configuration/Creator/IMetadataCreator.cs +++ b/src/Ocelot/Configuration/Creator/IMetadataCreator.cs @@ -7,5 +7,5 @@ namespace Ocelot.Configuration.Creator; /// public interface IMetadataCreator { - MetadataOptions Create(IDictionary metadata, FileGlobalConfiguration fileGlobalConfiguration); + MetadataOptions Create(IDictionary metadata, FileGlobalConfiguration globalConfiguration); } diff --git a/src/Ocelot/Metadata/DownstreamRouteExtensions.cs b/src/Ocelot/Metadata/DownstreamRouteExtensions.cs index b523b18ad..ca3ba25e6 100644 --- a/src/Ocelot/Metadata/DownstreamRouteExtensions.cs +++ b/src/Ocelot/Metadata/DownstreamRouteExtensions.cs @@ -8,7 +8,7 @@ namespace Ocelot.Metadata; public static class DownstreamRouteExtensions { /// - /// The known truthy values + /// The known truthy values. /// private static readonly HashSet TruthyValues = new(StringComparer.OrdinalIgnoreCase) @@ -23,7 +23,7 @@ public static class DownstreamRouteExtensions }; /// - /// The known falsy values + /// The known falsy values. /// private static readonly HashSet FalsyValues = new(StringComparer.OrdinalIgnoreCase) @@ -37,7 +37,7 @@ public static class DownstreamRouteExtensions }; /// - /// The known numeric types + /// The known numeric types. /// private static readonly HashSet NumericTypes = new() { @@ -78,10 +78,10 @@ public static T GetMetadata(this DownstreamRoute downstreamRoute, string key, } /// - /// Converting a string value to the target type + /// Converting a string value to the target type. /// Some custom conversion has been for the following types: - /// bool, bool?, string[], numeric types - /// otherwise trying to deserialize the value using the JsonSerializer + /// , , , numeric types; + /// otherwise trying to deserialize the value using the JsonSerializer. /// /// The target type. /// The string value. @@ -135,7 +135,7 @@ private static object ConvertTo(Type targetType, string value, MetadataOptions m } /// - /// Converting string to the known numeric types + /// Converting string to the known numeric types. /// /// The number as string. /// The target numeric type. diff --git a/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs b/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs index 1b7c1e99f..b36bf50a5 100644 --- a/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs +++ b/test/Ocelot.AcceptanceTests/EurekaServiceDiscoveryTests.cs @@ -111,7 +111,7 @@ private void GivenThereIsAFakeEurekaServiceDiscoveryProvider(string url, string evictionTimestamp = 0, serviceUpTimestamp = 1457714988223, }, - metadata = new Metadata + metadata = new() { value = "java.util.Collections$EmptyMap", }, @@ -231,7 +231,7 @@ public class LeaseInfo public long serviceUpTimestamp { get; set; } } - public class Metadata + public class ValueMetadata { [JsonProperty("@class")] public string value { get; set; } @@ -250,7 +250,7 @@ public class Instance public int countryId { get; set; } public DataCenterInfo dataCenterInfo { get; set; } public LeaseInfo leaseInfo { get; set; } - public Metadata metadata { get; set; } + public ValueMetadata metadata { get; set; } public string homePageUrl { get; set; } public string statusPageUrl { get; set; } public string healthCheckUrl { get; set; } diff --git a/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs b/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs index 83f79e720..330861317 100644 --- a/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs +++ b/test/Ocelot.AcceptanceTests/HttpDelegatingHandlersTests.cs @@ -123,7 +123,7 @@ public void should_call_global_di_handlers_multiple_times() this.Given(x => x.GivenThereIsAServiceRunningOn($"http://localhost:{port}", "/", 200, "Hello from Laura")) .And(x => _steps.GivenThereIsAConfiguration(configuration)) - .And(x => _steps.GivenOcelotIsRunningWithGlobalHandlerRegisteredInDi()) + .And(x => _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(true)) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) .When(x => _steps.WhenIGetUrlOnTheApiGateway("/")) diff --git a/test/Ocelot.AcceptanceTests/DownstreamMetadata/DownstreamMetadataTests.cs b/test/Ocelot.AcceptanceTests/Metadata/DownstreamMetadataTests.cs similarity index 96% rename from test/Ocelot.AcceptanceTests/DownstreamMetadata/DownstreamMetadataTests.cs rename to test/Ocelot.AcceptanceTests/Metadata/DownstreamMetadataTests.cs index b944c2d3d..64c17d72b 100644 --- a/test/Ocelot.AcceptanceTests/DownstreamMetadata/DownstreamMetadataTests.cs +++ b/test/Ocelot.AcceptanceTests/Metadata/DownstreamMetadataTests.cs @@ -4,7 +4,7 @@ using Ocelot.Middleware; using System.Globalization; -namespace Ocelot.AcceptanceTests.DownstreamMetadata; +namespace Ocelot.AcceptanceTests.Metadata; public class DownstreamMetadataTests : IDisposable { @@ -210,7 +210,7 @@ private void GivenThereIsAServiceRunningOn(string url) } /// - /// Starting ocelot with the delegating handler of type currentType + /// Starting ocelot with the delegating handler of type currentType. /// /// The current delegating handler type. /// Throws if delegating handler type doesn't match. @@ -219,24 +219,22 @@ private void GivenOcelotIsRunningWithSpecificHandlerForType(Type currentType) switch (currentType) { case { } t when t == typeof(StringDownStreamMetadataHandler): - _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); + _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(); break; case { } t when t == typeof(StringArrayDownStreamMetadataHandler): - _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); + _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(); break; case { } t when t == typeof(BoolDownStreamMetadataHandler): - _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); + _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(); break; case { } t when t == typeof(DoubleDownStreamMetadataHandler): - _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); + _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(); break; case { } t when t == typeof(SuperDataContainerDownStreamMetadataHandler): - _steps - .GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi< - SuperDataContainerDownStreamMetadataHandler>(); + _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(); break; case { } t when t == typeof(IntDownStreamMetadataHandler): - _steps.GivenOcelotIsRunningWithSpecificHandlerRegisteredInDi(); + _steps.GivenOcelotIsRunningWithHandlerRegisteredInDi(); break; default: throw new NotImplementedException(); @@ -291,7 +289,7 @@ public SuperDataContainerDownStreamMetadataHandler(IHttpContextAccessor httpCont /// /// Simple delegating handler that checks if the metadata is correctly passed to the downstream route - /// and checking if the extension method GetMetadata returns the correct value + /// and checking if the extension method GetMetadata returns the correct value. /// /// The current type. private class DownstreamMetadataHandler : DelegatingHandler diff --git a/test/Ocelot.AcceptanceTests/Steps.cs b/test/Ocelot.AcceptanceTests/Steps.cs index 7f6243502..e1d24d343 100644 --- a/test/Ocelot.AcceptanceTests/Steps.cs +++ b/test/Ocelot.AcceptanceTests/Steps.cs @@ -477,33 +477,6 @@ public void GivenOcelotIsRunningWithMiddlewareBeforePipeline(Func() - where T: DelegatingHandler - { - _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.AddSingleton(_webHostBuilder); - s.AddOcelot() - .AddDelegatingHandler(); - }) - .Configure(a => { a.UseOcelot().Wait(); }); - - _ocelotServer = new TestServer(_webHostBuilder); - _ocelotClient = _ocelotServer.CreateClient(); - } - public void GivenOcelotIsRunningWithSpecificHandlersRegisteredInDi() where TOne : DelegatingHandler where TWo : DelegatingHandler @@ -564,7 +537,7 @@ public void GivenOcelotIsRunningWithGlobalHandlersRegisteredInDi() _ocelotClient = _ocelotServer.CreateClient(); } - public void GivenOcelotIsRunningWithGlobalHandlerRegisteredInDi() + public void GivenOcelotIsRunningWithHandlerRegisteredInDi(bool global = false) where TOne : DelegatingHandler { _webHostBuilder = new WebHostBuilder(); @@ -583,7 +556,7 @@ public void GivenOcelotIsRunningWithGlobalHandlerRegisteredInDi() { s.AddSingleton(_webHostBuilder); s.AddOcelot() - .AddDelegatingHandler(true); + .AddDelegatingHandler(global); }) .Configure(a => { a.UseOcelot().Wait(); }); diff --git a/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs index 89f98693e..1dc86f0c5 100644 --- a/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs @@ -14,37 +14,57 @@ public class DefaultMetadataCreatorTests : UnitTest [Fact] public void Should_return_empty_metadata() { - this.Given(_ => GivenEmptyMetadataInGlobalConfiguration()) - .Given(_ => GivenEmptyMetadataInRoute()) - .When(_ => WhenICreate()) - .Then(_ => ThenDownstreamRouteMetadataMustBeEmpty()); + // Arrange + GivenEmptyMetadataInGlobalConfiguration(); + GivenEmptyMetadataInRoute(); + + // Act + WhenICreate(); + + // Assert + ThenDownstreamRouteMetadataMustBeEmpty(); } [Fact] public void Should_return_global_metadata() { - this.Given(_ => GivenSomeMetadataInGlobalConfiguration()) - .Given(_ => GivenEmptyMetadataInRoute()) - .When(_ => WhenICreate()) - .Then(_ => ThenDownstreamMetadataMustContain("foo", "bar")); + // Arrange + GivenSomeMetadataInGlobalConfiguration(); + GivenEmptyMetadataInRoute(); + + // Act + WhenICreate(); + + // Assert + ThenDownstreamMetadataMustContain("foo", "bar"); } [Fact] public void Should_return_route_metadata() { - this.Given(_ => GivenEmptyMetadataInGlobalConfiguration()) - .Given(_ => GivenSomeMetadataInRoute()) - .When(_ => WhenICreate()) - .Then(_ => ThenDownstreamMetadataMustContain("foo", "baz")); + // Arrange + GivenEmptyMetadataInGlobalConfiguration(); + GivenSomeMetadataInRoute(); + + // Act + WhenICreate(); + + // Assert + ThenDownstreamMetadataMustContain("foo", "baz"); } [Fact] public void Should_overwrite_global_metadata() { - this.Given(_ => GivenSomeMetadataInGlobalConfiguration()) - .Given(_ => GivenSomeMetadataInRoute()) - .When(_ => WhenICreate()) - .Then(_ => ThenDownstreamMetadataMustContain("foo", "baz")); + // Arrange + GivenSomeMetadataInGlobalConfiguration(); + GivenSomeMetadataInRoute(); + + // Act + WhenICreate(); + + // Assert + ThenDownstreamMetadataMustContain("foo", "baz"); } private void WhenICreate() From 40b404f9fa1ad977950ccc76d3210236f6d82785 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Tue, 21 May 2024 22:36:46 +0300 Subject: [PATCH 31/32] Add traits --- test/Ocelot.AcceptanceTests/Metadata/DownstreamMetadataTests.cs | 1 + .../Configuration/DefaultMetadataCreatorTests.cs | 1 + .../Configuration/DownstreamRouteExtensionsTests.cs | 1 + 3 files changed, 3 insertions(+) diff --git a/test/Ocelot.AcceptanceTests/Metadata/DownstreamMetadataTests.cs b/test/Ocelot.AcceptanceTests/Metadata/DownstreamMetadataTests.cs index 64c17d72b..ea09880df 100644 --- a/test/Ocelot.AcceptanceTests/Metadata/DownstreamMetadataTests.cs +++ b/test/Ocelot.AcceptanceTests/Metadata/DownstreamMetadataTests.cs @@ -6,6 +6,7 @@ namespace Ocelot.AcceptanceTests.Metadata; +[Trait("Feat", "738")] public class DownstreamMetadataTests : IDisposable { private readonly Steps _steps; diff --git a/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs b/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs index 1dc86f0c5..b150c6f3c 100644 --- a/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DefaultMetadataCreatorTests.cs @@ -4,6 +4,7 @@ namespace Ocelot.UnitTests.Configuration; +[Trait("Feat", "738")] public class DefaultMetadataCreatorTests : UnitTest { private FileGlobalConfiguration _globalConfiguration; diff --git a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs index bab8742ad..7d8b61c98 100644 --- a/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs +++ b/test/Ocelot.UnitTests/Configuration/DownstreamRouteExtensionsTests.cs @@ -7,6 +7,7 @@ namespace Ocelot.UnitTests.Configuration; +[Trait("Feat", "738")] public class DownstreamRouteExtensionsTests { private readonly DownstreamRoute _downstreamRoute; From 5f2beb64b389cab9dc294d4a2e0f7addd4d34450 Mon Sep 17 00:00:00 2001 From: Raman Maksimchuk Date: Tue, 21 May 2024 22:45:51 +0300 Subject: [PATCH 32/32] Fix docs build error --- docs/features/metadata.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/features/metadata.rst b/docs/features/metadata.rst index f1930906e..902ff972e 100644 --- a/docs/features/metadata.rst +++ b/docs/features/metadata.rst @@ -81,9 +81,9 @@ If the value is null, then, if not explicitely specified, the default for the ch - Notes * - ``GetMetadata`` - The metadata value is returned as string without further parsing + - * - ``GetMetadata`` - - The metadata value is splitted by a given separator (default ``,``) and - returned as a string array. + - The metadata value is splitted by a given separator (default ``,``) and returned as a string array. - Several parameters can be set in the global configuration, such as Separators (default = ``[","]``), StringSplitOptions (default ``None``) and TrimChars, the characters that should be trimmed (default = ``[' ']``). * - ``GetMetadata`` - The metadata value is parsed to a number. @@ -97,4 +97,3 @@ If the value is null, then, if not explicitely specified, the default for the ch * - ``GetMetadata`` - Check if the metadata value is a truthy value (return true), or falsy value (return false), otherwise return null. - The known truthy values are: ``true``, ``yes``, ``ok``, ``on``, ``enable``, ``enabled``, ``1``, the known falsy values are: ``false``, ``no``, ``off``, ``disable``, ``disabled``, ``0`` -