Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
28 changes: 14 additions & 14 deletions src/Ocelot/Configuration/Creator/AggregatesCreator.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
using Ocelot.Configuration.Builder;
using Ocelot.Configuration.File;
using Ocelot.Configuration.Builder;
using Ocelot.Configuration.File;

namespace Ocelot.Configuration.Creator;

public class AggregatesCreator : IAggregatesCreator
{
private readonly IUpstreamTemplatePatternCreator _creator;
private readonly IUpstreamTemplatePatternCreator _creator;
private readonly IUpstreamHeaderTemplatePatternCreator _headerCreator;

public AggregatesCreator(IUpstreamTemplatePatternCreator creator, IUpstreamHeaderTemplatePatternCreator headerCreator)
{
_creator = creator;
_creator = creator;
_headerCreator = headerCreator;
}

Expand All @@ -26,18 +26,18 @@ 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);
var orderedKeys = aggregateRoute.RouteKeys.ToList();
foreach (var key in orderedKeys)
{
var match = allRoutes.FirstOrDefault(r => r.Key == key);
if (match is null)
{
return null;
}
applicableRoutes.Add(match);
}

var upstreamTemplatePattern = _creator.Create(aggregateRoute);
var upstreamTemplatePattern = _creator.Create(aggregateRoute);
var upstreamHeaderTemplates = _headerCreator.Create(aggregateRoute);
var upstreamHttpMethod = (aggregateRoute.UpstreamHttpMethod.Count == 0) ? new List<HttpMethod>()
: aggregateRoute.UpstreamHttpMethod.Select(x => new HttpMethod(x.Trim())).ToList();
Expand Down
96 changes: 55 additions & 41 deletions src/Ocelot/Multiplexer/MultiplexingMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,21 +162,25 @@ private Task MapResponsesAsync(HttpContext context, Route route, HttpContext mai
/// <summary>
/// Processing a route with aggregation.
/// </summary>
private IEnumerable<Task<HttpContext>> ProcessRouteWithComplexAggregation(AggregateRouteConfig matchAdvancedAgg,
JToken jObject, HttpContext httpContext, DownstreamRoute downstreamRoute)
private IEnumerable<Task<HttpContext>> ProcessRouteWithComplexAggregation(
AggregateRouteConfig matchAdvancedAgg,
JToken jObject,
HttpContext httpContext,
DownstreamRoute downstreamRoute)
{
var processing = new List<Task<HttpContext>>();
var values = jObject.SelectTokens(matchAdvancedAgg.JsonPath).Select(s => s.ToString()).Distinct();
var values = jObject.SelectTokens(matchAdvancedAgg.JsonPath).Select(s => s.ToString()).Distinct().ToList();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var values = jObject.SelectTokens(matchAdvancedAgg.JsonPath).Select(s => s.ToString()).Distinct().ToList();
var values = jObject.SelectTokens(matchAdvancedAgg.JsonPath).Select(s => s.ToString()).Distinct().ToArray();

Copy link
Member

@raman-m raman-m Oct 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be fair, creating a list or another collection isn't necessary since the foreach operator already processes IEnumerable object 🤣 LoL →

var values = jObject.SelectTokens(matchAdvancedAgg.JsonPath).Select(s => s.ToString()).Distinct();
foreach (var value in values)

@NandanDevHub Please avoid using ToList() to create the list, as it consumes a small amount of heap and adds unnecessary pressure on the garbage collector!
FYI, Distinct() is essentially a wrapper algorithm applied to a collection generated by multiple Select() operations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My suggestion:

-        var values = jObject.SelectTokens(matchAdvancedAgg.JsonPath).Select(s => s.ToString()).Distinct();
+        var values = jObject.SelectTokens(matchAdvancedAgg.JsonPath).Select(ToString).Distinct();

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@raman-m I've kept the explicit lambda here since SelectTokens() returns JToken items, i felt this way each tokens value is safely converted to string.

Also in below cmmit i confirmed the EOL formatting is fine now.

Copy link
Member

@raman-m raman-m Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, fair enough!
Could the code line be like this?
My suggestion:

var values = jObject.SelectTokens(matchAdvancedAgg.JsonPath).Distinct().Select(s => s.ToString());

The idea is to skip creating the collection and applying the Distinct algorithm afterward. Instead, it's more efficient to search for distinct objects directly and then perform the casting to strings.
I'm not sure about the correctness because Distinct might return all J-tokens.


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 PlaceholderNameAndValue('{' + matchAdvancedAgg.Parameter + '}', value)
};
processing.Add(ProcessRouteAsync(httpContext, downstreamRoute, tPnv));
}

return processing;
}

/// <summary>
/// Process a downstream route asynchronously.
/// </summary>
Expand All @@ -200,18 +204,18 @@ private static void CopyItemsToNewContext(HttpContext target, HttpContext source
target.Items.SetIInternalConfiguration(source.Items.IInternalConfiguration());
target.Items.UpsertTemplatePlaceholderNameAndValues(placeholders ??
source.Items.TemplatePlaceholderNameAndValues());
}
}

/// <summary>
/// Creates a new HttpContext based on the source.
/// </summary>
/// <param name="source">The base http context.</param>
/// <param name="source">The base http context.</param>
/// <param name="route">Downstream route.</param>
/// <returns>The cloned context.</returns>
protected virtual async Task<HttpContext> CreateThreadContextAsync(HttpContext source, DownstreamRoute route)
{
var from = source.Request;
var bodyStream = await CloneRequestBodyAsync(from, route, source.RequestAborted);
var from = source.Request;
var bodyStream = await CloneRequestBodyAsync(from, route, source.RequestAborted);
var target = new DefaultHttpContext
{
Request =
Expand Down Expand Up @@ -241,12 +245,12 @@ protected virtual async Task<HttpContext> CreateThreadContextAsync(HttpContext s
foreach (var header in from.Headers)
{
target.Request.Headers[header.Key] = header.Value.ToArray();
}
// Once the downstream request is completed and the downstream response has been read, the downstream response object can dispose of the body's Stream object
}

// Once the downstream request is completed and the downstream response has been read, the downstream response object can dispose of the body's Stream object
target.Response.RegisterForDisposeAsync(bodyStream); // manage Stream lifetime by HttpResponse object
return target;
}
}

protected virtual Task MapAsync(HttpContext httpContext, Route route, List<HttpContext> contexts)
{
Expand All @@ -255,31 +259,41 @@ 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);
}
protected virtual async Task<Stream> CloneRequestBodyAsync(HttpRequest request, DownstreamRoute route, CancellationToken aborted)
{
request.EnableBuffering();
if (request.Body.Position != 0)
{
Logger.LogWarning(() => $"Ocelot does not support body copy without stream in initial position 0 for the route {route.Name()}.");
return request.Body;
}
var targetBuffer = new MemoryStream();
if (request.ContentLength is not null)
{
await request.Body.CopyToAsync(targetBuffer, (int)request.ContentLength, aborted);
targetBuffer.Position = 0;
request.Body.Position = 0;
}
else
{
Logger.LogInformation(() => $"Aggregation does not support body copy without Content-Length header, skipping body copy for the route {route.Name()}.");
}
return targetBuffer;
}
}

protected virtual async Task<Stream> CloneRequestBodyAsync(HttpRequest request, DownstreamRoute route, CancellationToken aborted)
{
request.EnableBuffering();
if (request.Body.Position != 0)
{
Logger.LogWarning(() => $"Ocelot does not support body copy without stream in initial position 0 for the route {route.Name()}.");
return request.Body;
}

var targetBuffer = new MemoryStream();
if (request.ContentLength is not null)
{
await request.Body.CopyToAsync(targetBuffer, (int)request.ContentLength, aborted);
targetBuffer.Position = 0;
request.Body.Position = 0;
}
else
{
Logger.LogInformation(() => $"Aggregation does not support body copy without Content-Length header, skipping body copy for the route {route.Name()}.");
}

return targetBuffer;
}
}