diff --git a/src/Ocelot/Configuration/Creator/AggregatesCreator.cs b/src/Ocelot/Configuration/Creator/AggregatesCreator.cs index 8b77c5339..72e18b415 100644 --- a/src/Ocelot/Configuration/Creator/AggregatesCreator.cs +++ b/src/Ocelot/Configuration/Creator/AggregatesCreator.cs @@ -26,15 +26,15 @@ private Route SetUpAggregateRoute(IEnumerable routes, FileAggregateRoute { var applicableRoutes = new List(); var allRoutes = routes.SelectMany(x => x.DownstreamRoute); - var downstreamRoutes = aggregateRoute.RouteKeys.Select(routeKey => allRoutes.FirstOrDefault(q => q.Key == routeKey)); - foreach (var downstreamRoute in downstreamRoutes) - { - if (downstreamRoute == null) - { - return null; - } - - applicableRoutes.Add(downstreamRoute); + foreach (var key in aggregateRoute.RouteKeys) + { + var match = allRoutes.FirstOrDefault(r => r.Key == key); + if (match is null) + { + return null; + } + + applicableRoutes.Add(match); } var upstreamTemplatePattern = _creator.Create(aggregateRoute); diff --git a/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs b/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs index c148eb9b9..ee0e06ebd 100644 --- a/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs +++ b/src/Ocelot/Multiplexer/MultiplexingMiddleware.cs @@ -169,8 +169,10 @@ private IEnumerable> ProcessRouteWithComplexAggregation(Aggreg var values = jObject.SelectTokens(matchAdvancedAgg.JsonPath).Select(s => s.ToString()).Distinct(); foreach (var value in values) { - var tPnv = httpContext.Items.TemplatePlaceholderNameAndValues(); - tPnv.Add(new PlaceholderNameAndValue('{' + matchAdvancedAgg.Parameter + '}', value)); + var tPnv = new List(httpContext.Items.TemplatePlaceholderNameAndValues()) + { + new('{' + matchAdvancedAgg.Parameter + '}', value), + }; processing.Add(ProcessRouteAsync(httpContext, downstreamRoute, tPnv)); } @@ -255,6 +257,16 @@ protected virtual Task MapAsync(HttpContext httpContext, Route route, List 0) + { + for (int i = 0; i < contexts.Count && i < route.DownstreamRouteConfig.Count; i++) + { + var key = route.DownstreamRouteConfig[i].RouteKey; + contexts[i].Items["CurrentAggregateRouteKey"] = key; + } + } + var aggregator = _factory.Get(route); return aggregator.Aggregate(route, httpContext, contexts); } diff --git a/test/Ocelot.AcceptanceTests/AggregateTests.cs b/test/Ocelot.AcceptanceTests/AggregateTests.cs index 9dff5f8f8..c4871e85e 100644 --- a/test/Ocelot.AcceptanceTests/AggregateTests.cs +++ b/test/Ocelot.AcceptanceTests/AggregateTests.cs @@ -685,6 +685,134 @@ public void Should_return_response_200_with_copied_form_sent_on_multiple_service .BDDfy(); } + [Fact] + [Trait("Bug", "2248")] + [Trait("PR", "2328")] // https://github.com/ThreeMammals/Ocelot/pull/2328 + public void Should_match_downstream_routes_using_route_keys_array() + { + var portUser = PortFinder.GetRandomPort(); + var userRoute = GivenRoute(portUser, "/user", "/user"); + userRoute.Key = "User"; + + var portProduct = PortFinder.GetRandomPort(); + var productRoute = GivenRoute(portProduct, "/product", "/product"); + productRoute.Key = "Product"; + + var aggregate = new FileAggregateRoute + { + RouteKeys = new() { "User", "Product" }, + UpstreamPathTemplate = "/composite", + UpstreamHttpMethod = ["Get"], + }; + + var configuration = GivenConfiguration(userRoute, productRoute); + configuration.Aggregates = new() { aggregate }; + this.Given(_ => GivenThereIsAConfiguration(configuration)) + .And(_ => GivenOcelotIsRunning()) + .And(_ => GivenThereIsAServiceRunningOn(portUser, "/user", MapGetUser)) + .And(_ => GivenThereIsAServiceRunningOn(portProduct, "/product", MapGetProduct)) + .When(_ => WhenIGetUrlOnTheApiGateway("/composite")) + .Then(_ => ThenTheStatusCodeShouldBeOK()) + .BDDfy(); + } + + [Fact] + [Trait("Bug", "2248")] + [Trait("PR", "2328")] // https://github.com/ThreeMammals/Ocelot/pull/2328 + public void Should_expand_jsonpath_array_into_multiple_parameterized_calls() + { + var commentsPort = PortFinder.GetRandomPort(); + var usersPort = PortFinder.GetRandomPort(); + + var comments = new FileRoute + { + Key = "comments", + DownstreamScheme = "http", + DownstreamHostAndPorts = new() { new("localhost", commentsPort) }, + DownstreamPathTemplate = "/comments", + UpstreamPathTemplate = "/comments", + UpstreamHttpMethod = [HttpMethods.Get], + }; + + var user = new FileRoute + { + Key = "user", + DownstreamScheme = "http", + DownstreamHostAndPorts = new() { new("localhost", usersPort) }, + DownstreamPathTemplate = "/users/{userId}", + UpstreamPathTemplate = "/users/{userId}", + UpstreamHttpMethod = [HttpMethods.Get], + }; + + var aggregate = new FileAggregateRoute + { + UpstreamPathTemplate = "/aggregatecommentuser", + UpstreamHttpMethod = [HttpMethods.Get], + RouteKeys = ["comments", "user"], + RouteKeysConfig = new() + { + new AggregateRouteConfig + { + RouteKey = "user", + JsonPath = "$[*].userId", + Parameter = "userId", + }, + }, + }; + + var config = new FileConfiguration + { + Routes = new() { comments, user }, + Aggregates = new() { aggregate }, + }; + + handler.GivenThereIsAServiceRunningOn(commentsPort, async ctx => + { + if (ctx.Request.Path.Value == "/comments") + { + ctx.Response.StatusCode = 200; + ctx.Response.ContentType = "application/json"; + await ctx.Response.WriteAsync("[{\"id\":1,\"userId\":1},{\"id\":2,\"userId\":2}]"); + } + else + { + ctx.Response.StatusCode = 404; + } + }); + + handler.GivenThereIsAServiceRunningOn(usersPort, async ctx => + { + var parts = ctx.Request.Path.Value?.Trim('/').Split('/'); + var ok = parts?.Length == 2 && parts[0] == "users" && int.TryParse(parts[1], out var id); + ctx.Response.StatusCode = ok ? 200 : 400; + ctx.Response.ContentType = "application/json"; + await ctx.Response.WriteAsync(ok + ? $"{{\"id\":{parts![1]},\"name\":\"User-{parts[1]}\"}}" + : "{\"error\":\"bad id\"}"); + }); + + var expected = + "{\"comments\":[{\"id\":1,\"userId\":1},{\"id\":2,\"userId\":2}],\"user\":[{\"id\":1,\"name\":\"User-1\"},{\"id\":2,\"name\":\"User-2\"}]}"; + + this.Given(_ => GivenThereIsAConfiguration(config)) + .And(_ => GivenOcelotIsRunning()) + .When(_ => WhenIGetUrlOnTheApiGateway("/aggregatecommentuser")) + .Then(_ => ThenTheStatusCodeShouldBe(HttpStatusCode.OK)) + .And(_ => ThenTheResponseBodyShouldBe(expected)) + .BDDfy(); + } + + Task MapGetUser(HttpContext ctx) + { + ctx.Response.StatusCode = 200; + return ctx.Response.WriteAsync("OK-user"); + } + Task MapGetProduct(HttpContext ctx) + { + ctx.Response.StatusCode = 200; + return ctx.Response.WriteAsync("OK-product"); + } + private static string FormatFormCollection(IFormCollection reqForm) { var sb = new StringBuilder() diff --git a/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs b/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs index 7817f646b..27d24ebb4 100644 --- a/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs +++ b/test/Ocelot.UnitTests/Multiplexing/MultiplexingMiddlewareTests.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Moq.Protected; using Ocelot.Configuration; using Ocelot.Configuration.Builder; @@ -264,7 +264,72 @@ public async Task If_Using_3_Routes_WithAggregator_ProcessSingleRoute_Is_Never_C ItExpr.IsAny>()); _count.ShouldBe(3); - } + } + + [Fact] + [Trait("Bug", "2248")] + [Trait("PR", "2328")] + public async Task Should_expand_jsonpath_array_into_multiple_parameterized_calls() + { + RequestDelegate responder = context => + { + var json = @"[{""userId"":1},{""userId"":2}]"; + context.Items.Add("DownstreamResponse", + new DownstreamResponse(new StringContent(json, Encoding.UTF8, "application/json"), + HttpStatusCode.OK, new List
(), "test")); + if (!context.Items.ContainsKey("TemplatePlaceholderNameAndValues")) + context.Items.Add("TemplatePlaceholderNameAndValues", new List()); + _count++; + return Task.CompletedTask; + }; + + var mock = MockMiddlewareFactory(null, responder); + + var route = new Route + { + DownstreamRoute = + [ + new DownstreamRouteBuilder().WithKey("comments").Build(), + new DownstreamRouteBuilder().WithKey("user").Build() + ], + DownstreamRouteConfig = + [ + new AggregateRouteConfig { RouteKey = "user", JsonPath = "$[*].userId", Parameter = "userId" } + ], + Aggregator = "TestAggregator", + }; + + GivenTheFollowing(route); + + await _middleware.Invoke(_httpContext); + + _count.ShouldBe(3); + mock.Protected().Verify("MapAsync", Times.Once(), + ItExpr.IsAny(), + ItExpr.IsAny(), + ItExpr.Is>(list => list.Count == 3)); + } + + [Fact] + [Trait("Bug", "2248")] + [Trait("PR", "2328")] + public async Task Should_verify_each_context_has_aggregate_key() + { + + var route = GivenRoutesWithAggregator(); + GivenTheFollowing(route); + + _middleware = new MultiplexingMiddleware(AggregateRequestDelegateFactory(), loggerFactory.Object, factory.Object); + + await _middleware.Invoke(_httpContext); + + _count.ShouldBe(3); + + aggregator.Verify(a => a.Aggregate( + It.IsAny(), + It.IsAny(), + It.IsAny>()), Times.Once()); + } private RequestDelegate AggregateRequestDelegateFactory() {