Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions src/Ocelot/Configuration/Creator/AggregatesCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ private Route SetUpAggregateRoute(IEnumerable<Route> routes, FileAggregateRoute
{
var applicableRoutes = new List<DownstreamRoute>();
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);
Expand Down
16 changes: 14 additions & 2 deletions src/Ocelot/Multiplexer/MultiplexingMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,10 @@ private IEnumerable<Task<HttpContext>> 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<PlaceholderNameAndValue>(httpContext.Items.TemplatePlaceholderNameAndValues())
{
new('{' + matchAdvancedAgg.Parameter + '}', value),
};
processing.Add(ProcessRouteAsync(httpContext, downstreamRoute, tPnv));
}

Expand Down Expand Up @@ -255,6 +257,16 @@ protected virtual Task MapAsync(HttpContext httpContext, Route route, List<HttpC
return Task.CompletedTask;
}

// ensure each context retains its correct aggregate key for proper response mapping
if (route.DownstreamRouteConfig != null && route.DownstreamRouteConfig.Count > 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);
}
Expand Down
128 changes: 128 additions & 0 deletions test/Ocelot.AcceptanceTests/AggregateTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http;
using Moq.Protected;
using Ocelot.Configuration;
using Ocelot.Configuration.Builder;
Expand Down Expand Up @@ -264,6 +264,50 @@ public async Task If_Using_3_Routes_WithAggregator_ProcessSingleRoute_Is_Never_C
ItExpr.IsAny<List<HttpContext>>());

_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<Header>(), "test"));
if (!context.Items.ContainsKey("TemplatePlaceholderNameAndValues"))
context.Items.Add("TemplatePlaceholderNameAndValues", new List<PlaceholderNameAndValue>());
_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<Task>("MapAsync", Times.Once(),
ItExpr.IsAny<HttpContext>(),
ItExpr.IsAny<Route>(),
ItExpr.Is<List<HttpContext>>(list => list.Count == 3));
}

private RequestDelegate AggregateRequestDelegateFactory()
Expand Down