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
2 changes: 1 addition & 1 deletion docs/features/aggregation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ Gotchas
-------

* You cannot use routes with specific ``RequestIdKeys``, as this would be overly complicated to track.
* *Aggregation* supports only the ``GET`` HTTP verb.
* *Aggregation* supports the ``GET`` HTTP verb for pure REST, and also supports other verbs for APIs that do not strictly follow REST.
* *Aggregation* allows the forwarding of ``HttpRequest.Body`` to downstream services by duplicating the body data.
Form data and attached files should also be forwarded.
It is essential to specify the ``Content-Length`` header in requests to the upstream; otherwise, Ocelot will log warnings such as: *"Aggregation does not support body copy without a Content-Length header!"*
Expand Down
5 changes: 5 additions & 0 deletions src/Ocelot/Configuration/Creator/AggregatesCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ public List<Route> Create(FileConfiguration fileConfiguration, IReadOnlyList<Rou

private Route SetUpAggregateRoute(IEnumerable<Route> routes, FileAggregateRoute aggregateRoute, FileGlobalConfiguration globalConfiguration)
{
if (aggregateRoute.UpstreamHttpMethod.Count == 0)
{
aggregateRoute.UpstreamHttpMethod.Add(FileAggregateRoute.DefaultHttpMethod.ToString());
}

var applicableRoutes = new List<DownstreamRoute>();
var allRoutes = routes.SelectMany(x => x.DownstreamRoute);
var downstreamRoutes = aggregateRoute.RouteKeys.Select(routeKey => allRoutes.FirstOrDefault(q => q.Key == routeKey));
Expand Down
13 changes: 8 additions & 5 deletions src/Ocelot/Configuration/File/FileAggregateRoute.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using Microsoft.AspNetCore.Http;

namespace Ocelot.Configuration.File;

public class FileAggregateRoute : IRouteUpstream, IRouteGroup
{
public string Aggregator { get; set; }
Expand All @@ -23,7 +21,12 @@ public FileAggregateRoute()
RouteKeysConfig = new();
UpstreamHeaderTemplates = new Dictionary<string, string>();
UpstreamHost = default;
UpstreamHttpMethod = [ HttpMethods.Get ]; // Only supports GET..are you crazy!! POST, PUT WOULD BE CRAZY!! :)
UpstreamHttpMethod = [];
UpstreamPathTemplate = default;
}
}

/// <summary>This allows to override global default HTTP verb value.</summary>
/// <remarks>Defaults: The <see cref="HttpMethod.Get"/> value.</remarks>
/// <value>A <see cref="HttpMethod"/> value.</value>
public static HttpMethod DefaultHttpMethod { get; set; } = HttpMethod.Get;
}
110 changes: 58 additions & 52 deletions test/Ocelot.AcceptanceTests/AggregateTests.cs
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
//using IdentityServer4.AccessTokenValidation;
//using IdentityServer4.Extensions;
//using IdentityServer4.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Ocelot.AcceptanceTests.Authentication;
using Ocelot.Configuration.File;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Ocelot.Multiplexer;
using System.Text;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Ocelot.AcceptanceTests.Authentication;
using Ocelot.Configuration.File;
using Ocelot.DependencyInjection;
using Ocelot.Middleware;
using Ocelot.Multiplexer;
using System.Text;

namespace Ocelot.AcceptanceTests;

public sealed class AggregateTests : Steps
{
{
private readonly string[] _downstreamPaths;

public AggregateTests()
{
_downstreamPaths = new string[3];
}

[Fact]
[Trait("Issue", "597")]
public void Should_fix_issue_597()
Expand Down Expand Up @@ -120,7 +120,7 @@ public void Should_fix_issue_597()
RequestIdKey = "CorrelationID",
},
};

var expected = "{\"key1\":some_data,\"key2\":some_data}";
this.Given(x => x.GivenServiceIsRunning(port, HttpStatusCode.OK, "some_data"))
.And(x => GivenThereIsAConfiguration(configuration))
Expand All @@ -130,7 +130,7 @@ public void Should_fix_issue_597()
.And(x => ThenTheResponseBodyShouldBe(expected))
.BDDfy();
}

[Fact]
public void Should_return_response_200_with_advanced_aggregate_configs()
{
Expand Down Expand Up @@ -207,13 +207,13 @@ public void Should_return_response_200_with_advanced_aggregate_configs()
},
},
};

var userDetailsResponseContent = @"{""id"":1,""firstName"":""abolfazl"",""lastName"":""rajabpour""}";
var postDetailsResponseContent = @"{""id"":1,""title"":""post1""}";
var commentsResponseContent = @"[{""id"":1,""writerId"":1,""postId"":2,""text"":""text1""},{""id"":2,""writerId"":1,""postId"":2,""text"":""text2""}]";

var expected = "{\"Comments\":" + commentsResponseContent + ",\"UserDetails\":" + userDetailsResponseContent + ",\"PostDetails\":" + postDetailsResponseContent + "}";

this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 200, commentsResponseContent))
.Given(x => x.GivenServiceIsRunning(1, port2, "/users/1", 200, userDetailsResponseContent))
.Given(x => x.GivenServiceIsRunning(2, port3, "/posts/2", 200, postDetailsResponseContent))
Expand All @@ -224,7 +224,7 @@ public void Should_return_response_200_with_advanced_aggregate_configs()
.And(x => ThenTheResponseBodyShouldBe(expected))
.BDDfy();
}

[Fact]
public void Should_return_response_200_with_simple_url_user_defined_aggregate()
{
Expand All @@ -250,7 +250,7 @@ public void Should_return_response_200_with_simple_url_user_defined_aggregate()
UpstreamHttpMethod = ["Get"],
Key = "Laura",
},

new FileRoute
{
DownstreamPathTemplate = "/",
Expand Down Expand Up @@ -279,9 +279,9 @@ public void Should_return_response_200_with_simple_url_user_defined_aggregate()
},
},
};

var expected = "Bye from Laura, Bye from Tom";

this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 200, "{Hello from Laura}"))
.Given(x => x.GivenServiceIsRunning(1, port2, "/", 200, "{Hello from Tom}"))
.And(x => GivenThereIsAConfiguration(configuration))
Expand All @@ -292,7 +292,7 @@ public void Should_return_response_200_with_simple_url_user_defined_aggregate()
.And(x => ThenTheDownstreamUrlPathShouldBe("/", "/"))
.BDDfy();
}

[Fact]
public void Should_return_response_200_with_simple_url()
{
Expand All @@ -301,7 +301,7 @@ public void Should_return_response_200_with_simple_url()
var route1 = GivenRoute(port1, "/laura", "Laura");
var route2 = GivenRoute(port2, "/tom", "Tom");
var configuration = GivenConfiguration(route1, route2);

this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 200, "{Hello from Laura}"))
.Given(x => x.GivenServiceIsRunning(1, port2, "/", 200, "{Hello from Tom}"))
.And(x => GivenThereIsAConfiguration(configuration))
Expand All @@ -312,7 +312,7 @@ public void Should_return_response_200_with_simple_url()
.And(x => ThenTheDownstreamUrlPathShouldBe("/", "/"))
.BDDfy();
}

[Fact]
public void Should_return_response_200_with_simple_url_one_service_404()
{
Expand Down Expand Up @@ -365,9 +365,9 @@ public void Should_return_response_200_with_simple_url_one_service_404()
},
},
};

var expected = "{\"Laura\":,\"Tom\":{Hello from Tom}}";

this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 404, ""))
.Given(x => x.GivenServiceIsRunning(1, port2, "/", 200, "{Hello from Tom}"))
.And(x => GivenThereIsAConfiguration(configuration))
Expand All @@ -378,7 +378,7 @@ public void Should_return_response_200_with_simple_url_one_service_404()
.And(x => ThenTheDownstreamUrlPathShouldBe("/", "/"))
.BDDfy();
}

[Fact]
public void Should_return_response_200_with_simple_url_both_service_404()
{
Expand Down Expand Up @@ -431,9 +431,9 @@ public void Should_return_response_200_with_simple_url_both_service_404()
},
},
};

var expected = "{\"Laura\":,\"Tom\":}";

this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 404, ""))
.Given(x => x.GivenServiceIsRunning(1, port2, "/", 404, ""))
.And(x => GivenThereIsAConfiguration(configuration))
Expand All @@ -444,7 +444,7 @@ public void Should_return_response_200_with_simple_url_both_service_404()
.And(x => ThenTheDownstreamUrlPathShouldBe("/", "/"))
.BDDfy();
}

[Fact]
public void Should_be_thread_safe()
{
Expand Down Expand Up @@ -497,7 +497,7 @@ public void Should_be_thread_safe()
},
},
};

this.Given(x => x.GivenServiceIsRunning(0, port1, "/", 200, "{Hello from Laura}"))
.Given(x => x.GivenServiceIsRunning(1, port2, "/", 200, "{Hello from Tom}"))
.And(x => GivenThereIsAConfiguration(configuration))
Expand All @@ -506,7 +506,7 @@ public void Should_be_thread_safe()
.And(x => ThenTheDownstreamUrlPathShouldBe("/", "/"))
.BDDfy();
}

private void WhenIMakeLotsOfDifferentRequestsToTheApiGateway()
{
var numberOfRequests = 100;
Expand Down Expand Up @@ -539,7 +539,7 @@ private void WhenIMakeLotsOfDifferentRequestsToTheApiGateway()
Task.WaitAll(tomTasks);
Task.WaitAll(aggregateTasks);
}

private async Task Fire(string url, string expectedBody, Random random)
{
var request = new HttpRequestMessage(new HttpMethod("GET"), url);
Expand All @@ -548,7 +548,7 @@ private async Task Fire(string url, string expectedBody, Random random)
var content = await response.Content.ReadAsStringAsync();
content.ShouldBe(expectedBody);
}

//[Fact]
//[Trait("Bug", "1396")]
//public void Should_return_response_200_with_user_forwarding()
Expand Down Expand Up @@ -644,7 +644,7 @@ public void Should_return_response_200_with_copied_body_sent_on_multiple_service
var sub1ResponseContent = @"{""id"":1,""response"":""fromBody-s1""}";
var sub2ResponseContent = @"{""id"":1,""response"":""fromBody-s2""}";
var expected = $"{{\"Service1\":{sub1ResponseContent},\"Service2\":{sub2ResponseContent}}}";

this.Given(x => x.GivenServiceIsRunning(0, port1, "/Sub1", 200, reqBody => reqBody.Replace("#REPLACESTRING#", "s1")))
.Given(x => x.GivenServiceIsRunning(1, port2, "/Sub2", 200, reqBody => reqBody.Replace("#REPLACESTRING#", "s2")))
.And(x => GivenThereIsAConfiguration(configuration))
Expand Down Expand Up @@ -685,6 +685,12 @@ public void Should_return_response_200_with_copied_form_sent_on_multiple_service
.BDDfy();
}

[Fact]
[Trait("Feat", "1389")]
public void TODO()
{
}

private static string FormatFormCollection(IFormCollection reqForm)
{
var sb = new StringBuilder()
Expand All @@ -699,7 +705,7 @@ private static string FormatFormCollection(IFormCollection reqForm)
.Append('"')
.ToString();
}

private void GivenServiceIsRunning(int port, HttpStatusCode statusCode, string responseBody)
{
handler.GivenThereIsAServiceRunningOn(port, context =>
Expand All @@ -708,7 +714,7 @@ private void GivenServiceIsRunning(int port, HttpStatusCode statusCode, string r
return context.Response.WriteAsync(responseBody);
});
}

private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, string responseBody)
=> GivenServiceIsRunning(index, port, basePath, statusCode,
async context =>
Expand All @@ -724,15 +730,15 @@ private void GivenServiceIsRunning(int index, int port, string basePath, int sta
var responseBody = responseFromBody(requestBody);
await context.Response.WriteAsync(responseBody);
});

private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, Func<IFormCollection, string> responseFromForm)
=> GivenServiceIsRunning(index, port, basePath, statusCode,
async context =>
{
var responseBody = responseFromForm(context.Request.Form);
await context.Response.WriteAsync(responseBody);
});

private void GivenServiceIsRunning(int index, int port, string basePath, int statusCode, Action<HttpContext> processContext)
{
handler.GivenThereIsAServiceRunningOn(port, basePath, async context =>
Expand All @@ -753,7 +759,7 @@ private void GivenServiceIsRunning(int index, int port, string basePath, int sta
}
});
}

private void GivenOcelotIsRunningWithSpecificAggregatorsRegisteredInDi<TAggregator, TDependency>()
where TAggregator : class, IDefinedAggregator
where TDependency : class
Expand All @@ -764,13 +770,13 @@ static void WithSpecificAggregators(IServiceCollection services) => services
.AddSingletonDefinedAggregator<TAggregator>();
GivenOcelotIsRunning(WithSpecificAggregators);
}

private void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPathOne, string expectedDownstreamPath)
{
_downstreamPaths[0].ShouldBe(expectedDownstreamPathOne);
_downstreamPaths[1].ShouldBe(expectedDownstreamPath);
}

private static FileRoute GivenRoute(int port, string upstream, string key, string downstream = null) => new()
{
DownstreamPathTemplate = downstream ?? "/",
Expand All @@ -780,7 +786,7 @@ private void ThenTheDownstreamUrlPathShouldBe(string expectedDownstreamPathOne,
UpstreamHttpMethod = [HttpMethods.Get],
Key = key,
};

private FileConfiguration GivenConfiguration(params FileRoute[] routes)
{
var obj = base.GivenConfiguration(routes);
Expand All @@ -795,11 +801,11 @@ private FileConfiguration GivenConfiguration(params FileRoute[] routes)
return obj;
}
}

public class FakeDep
{
}

public class FakeDefinedAggregator : IDefinedAggregator
{
public FakeDefinedAggregator(FakeDep dep)
Expand Down
Loading
Loading