diff --git a/samples/Ocelot.Samples.sln b/samples/Ocelot.Samples.sln
index f855def53..1cc35ceef 100644
--- a/samples/Ocelot.Samples.sln
+++ b/samples/Ocelot.Samples.sln
@@ -33,6 +33,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Samples.Configuratio
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Samples.Metadata", "Metadata\Ocelot.Samples.Metadata.csproj", "{80EE7EA9-BB02-4F2D-B7E3-BB6A42F50658}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ocelot.Samples.RateLimiter", "RateLimiter\Ocelot.Samples.RateLimiter.csproj", "{C4B2D4B9-D568-42DA-A203-6C33BA2E055D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -99,6 +101,10 @@ Global
{80EE7EA9-BB02-4F2D-B7E3-BB6A42F50658}.Debug|Any CPU.Build.0 = Debug|Any CPU
{80EE7EA9-BB02-4F2D-B7E3-BB6A42F50658}.Release|Any CPU.ActiveCfg = Release|Any CPU
{80EE7EA9-BB02-4F2D-B7E3-BB6A42F50658}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C4B2D4B9-D568-42DA-A203-6C33BA2E055D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C4B2D4B9-D568-42DA-A203-6C33BA2E055D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C4B2D4B9-D568-42DA-A203-6C33BA2E055D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C4B2D4B9-D568-42DA-A203-6C33BA2E055D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/samples/RateLimiter/Ocelot.Samples.RateLimiter.csproj b/samples/RateLimiter/Ocelot.Samples.RateLimiter.csproj
new file mode 100644
index 000000000..c49a854ca
--- /dev/null
+++ b/samples/RateLimiter/Ocelot.Samples.RateLimiter.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/samples/RateLimiter/Ocelot.Samples.RateLimiter.http b/samples/RateLimiter/Ocelot.Samples.RateLimiter.http
new file mode 100644
index 000000000..f35158ae9
--- /dev/null
+++ b/samples/RateLimiter/Ocelot.Samples.RateLimiter.http
@@ -0,0 +1,11 @@
+@RateLimiterSample_HostAddress = http://localhost:5202
+
+GET {{RateLimiterSample_HostAddress}}/laura
+Accept: application/json
+
+###
+
+GET {{RateLimiterSample_HostAddress}}/tom
+Accept: application/json
+
+###
diff --git a/samples/RateLimiter/Program.cs b/samples/RateLimiter/Program.cs
new file mode 100644
index 000000000..99c78bd49
--- /dev/null
+++ b/samples/RateLimiter/Program.cs
@@ -0,0 +1,26 @@
+using Microsoft.AspNetCore.RateLimiting;
+using Ocelot.DependencyInjection;
+using Ocelot.Middleware;
+using System.Threading.RateLimiting;
+
+var builder = WebApplication.CreateBuilder(args);
+builder.Configuration.AddJsonFile("ocelot.json");
+builder.Services.AddOcelot();
+
+builder.Services.AddRateLimiter(op =>
+{
+ op.AddFixedWindowLimiter(policyName: "fixed", options =>
+ {
+ options.PermitLimit = 2;
+ options.Window = TimeSpan.FromSeconds(12);
+ options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
+ options.QueueLimit = 0;
+ });
+});
+
+var app = builder.Build();
+app.UseHttpsRedirection();
+
+await app.UseOcelot();
+
+app.Run();
diff --git a/samples/RateLimiter/Properties/launchSettings.json b/samples/RateLimiter/Properties/launchSettings.json
new file mode 100644
index 000000000..9a5cdd083
--- /dev/null
+++ b/samples/RateLimiter/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:12083",
+ "sslPort": 44358
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "weatherforecast",
+ "applicationUrl": "http://localhost:5202",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "weatherforecast",
+ "applicationUrl": "https://localhost:7116;http://localhost:5202",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "weatherforecast",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/samples/RateLimiter/appsettings.Development.json b/samples/RateLimiter/appsettings.Development.json
new file mode 100644
index 000000000..0c208ae91
--- /dev/null
+++ b/samples/RateLimiter/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/samples/RateLimiter/appsettings.json b/samples/RateLimiter/appsettings.json
new file mode 100644
index 000000000..10f68b8c8
--- /dev/null
+++ b/samples/RateLimiter/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/samples/RateLimiter/ocelot.json b/samples/RateLimiter/ocelot.json
new file mode 100644
index 000000000..64b2c4f04
--- /dev/null
+++ b/samples/RateLimiter/ocelot.json
@@ -0,0 +1,46 @@
+{
+ "Routes": [
+ {
+ "UpstreamHttpMethod": [ "Get" ],
+ "UpstreamPathTemplate": "/laura",
+ "DownstreamPathTemplate": "/fact",
+ "DownstreamScheme": "https",
+ "DownstreamHostAndPorts": [
+ { "Host": "catfact.ninja", "Port": 443 }
+ ],
+ "Key": "Laura",
+ "RateLimitOptions": {
+ "EnableRateLimiting": true,
+ "Period": "5s",
+ "PeriodTimespan": 1,
+ "Limit": 1
+ }
+ },
+ {
+ "UpstreamHttpMethod": [ "Get" ],
+ "UpstreamPathTemplate": "/tom",
+ "DownstreamPathTemplate": "/fact",
+ "DownstreamScheme": "https",
+ "DownstreamHostAndPorts": [
+ { "Host": "catfact.ninja", "Port": 443 }
+ ],
+ "Key": "Tom",
+ "RateLimitOptions": {
+ "EnableRateLimiting": true,
+ "Policy": "fixed"
+ }
+ }
+ ],
+ "Aggregates": [
+ {
+ "UpstreamPathTemplate": "/",
+ "RouteKeys": [ "Tom", "Laura" ]
+ }
+ ],
+ "GlobalConfiguration": {
+ "RateLimitOptions": {
+ "QuotaExceededMessage": "Customize Tips!",
+ "HttpStatusCode": 418 // I'm a teapot
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs b/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs
index 3c1ffd749..668368cfa 100644
--- a/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs
+++ b/src/Ocelot/Configuration/Creator/RateLimitOptionsCreator.cs
@@ -81,6 +81,7 @@ protected virtual RateLimitOptions MergeHeaderRules(FileRateLimitByHeaderRule ru
rule.Wait = rule.Wait.IfEmpty(globalRule.Wait.IfEmpty(RateLimitRule.ZeroWait));
rule.Limit ??= globalRule.Limit ?? RateLimitRule.ZeroLimit;
+ rule.Policy = rule.Policy.IfEmpty(globalRule.Policy);
return new(rule);
}
}
diff --git a/src/Ocelot/Configuration/File/FileRateLimitRule.cs b/src/Ocelot/Configuration/File/FileRateLimitRule.cs
index d98ad0fbe..531c0d255 100644
--- a/src/Ocelot/Configuration/File/FileRateLimitRule.cs
+++ b/src/Ocelot/Configuration/File/FileRateLimitRule.cs
@@ -18,6 +18,7 @@ public FileRateLimitRule(FileRateLimitRule from)
Limit = from.Limit;
Period = from.Period;
PeriodTimespan = from.PeriodTimespan;
+ Policy = from.Policy;
Wait = from.Wait;
StatusCode = from.StatusCode;
QuotaMessage = from.QuotaMessage;
@@ -74,6 +75,14 @@ public FileRateLimitRule(FileRateLimitRule from)
/// A object which value defaults to "Ocelot.RateLimiting", see the property.
public string KeyPrefix { get; set; }
+ ///
+ /// Rate limit policy name. It only takes effect if rate limit middleware type is set to DotNet.
+ ///
+ ///
+ /// A string of rate limit policy name.
+ ///
+ public string Policy { get; set; }
+
///
/// Returns a string that represents the current rule in the format, which defaults to empty string if rate limiting is disabled ( is ).
///
@@ -85,6 +94,10 @@ public override string ToString()
{
return string.Empty;
}
+ else if (!string.IsNullOrWhiteSpace(Policy))
+ {
+ return $"{nameof(Policy)}:{Policy}";
+ }
char hdrSign = EnableHeaders == false ? '-' : '+';
string waitWindow = PeriodTimespan.HasValue ? PeriodTimespan.Value.ToString("F3") + 's' : Wait.IfEmpty(None);
diff --git a/src/Ocelot/Configuration/RateLimitOptions.cs b/src/Ocelot/Configuration/RateLimitOptions.cs
index 9785ba7a0..1b5860dbc 100644
--- a/src/Ocelot/Configuration/RateLimitOptions.cs
+++ b/src/Ocelot/Configuration/RateLimitOptions.cs
@@ -35,7 +35,8 @@ public RateLimitOptions(bool enableRateLimiting) : this()
}
public RateLimitOptions(bool enableRateLimiting, string clientIdHeader, IList clientWhitelist, bool enableHeaders,
- string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule, int httpStatusCode)
+ string quotaExceededMessage, string rateLimitCounterPrefix, RateLimitRule rateLimitRule, int httpStatusCode,
+ string policy = null)
{
ClientIdHeader = clientIdHeader.IfEmpty(DefaultClientHeader);
ClientWhitelist = clientWhitelist ?? [];
@@ -45,8 +46,9 @@ public RateLimitOptions(bool enableRateLimiting, string clientIdHeader, IListGets a Rate Limit rule.
@@ -121,4 +125,6 @@ public RateLimitOptions(RateLimitOptions fromOptions)
/// Enables or disables X-RateLimit-* and Retry-After headers.
/// A value.
public bool EnableHeaders { get; init; }
+
+ public string Policy { get; init; }
}
diff --git a/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs b/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs
index a6ae145c6..bbf84350d 100644
--- a/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs
+++ b/src/Ocelot/Configuration/Validator/RouteFluentValidator.cs
@@ -56,7 +56,9 @@ public RouteFluentValidator(
.WithMessage("{PropertyName} {PropertyValue} contains scheme");
});
- When(route => route.RateLimitOptions != null && route.RateLimitOptions.EnableRateLimiting != false, () =>
+ When(route => route.RateLimitOptions != null
+ && route.RateLimitOptions.EnableRateLimiting != false
+ && string.IsNullOrWhiteSpace(route.RateLimitOptions.Policy), () =>
{
RuleFor(route => route.RateLimitOptions.Limit)
.Must(limit => !limit.HasValue || (limit.HasValue && limit.Value > 0))
diff --git a/src/Ocelot/DependencyInjection/Features.cs b/src/Ocelot/DependencyInjection/Features.cs
index 3e2457e31..5d1689512 100644
--- a/src/Ocelot/DependencyInjection/Features.cs
+++ b/src/Ocelot/DependencyInjection/Features.cs
@@ -1,4 +1,7 @@
using FluentValidation;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Ocelot.Cache;
using Ocelot.Configuration.Creator;
@@ -35,6 +38,32 @@ public static IServiceCollection AddConfigurationValidators(this IServiceCollect
public static IServiceCollection AddOcelotRateLimiting(this IServiceCollection services) => services
.AddSingleton()
.AddSingleton();
+
+ ///
+ /// Ocelot feature: AspNet Rate Limiting.
+ ///
+ ///
+ /// Read The Docs: Rate Limiting.
+ ///
+ /// The services collection to add the feature to.
+ /// Root configuration object.
+ /// The same object.
+ public static IServiceCollection AddAspNetRateLimiting(this IServiceCollection services, IConfiguration configurationRoot)
+ {
+ var globalRateLimitOptions = configurationRoot.Get()?.GlobalConfiguration?.RateLimitOptions;
+ var rejectStatusCode = globalRateLimitOptions?.HttpStatusCode ?? StatusCodes.Status429TooManyRequests;
+ var rejectedMessage = globalRateLimitOptions?.QuotaExceededMessage ?? "API calls quota exceeded!";
+ services.AddRateLimiter(options =>
+ {
+ options.OnRejected = async (rejectedContext, token) =>
+ {
+ rejectedContext.HttpContext.Response.StatusCode = rejectStatusCode;
+ await rejectedContext.HttpContext.Response.WriteAsync(rejectedMessage, token);
+ };
+ });
+
+ return services;
+ }
///
/// Ocelot feature: Request Caching.
diff --git a/src/Ocelot/DependencyInjection/OcelotBuilder.cs b/src/Ocelot/DependencyInjection/OcelotBuilder.cs
index ff343fafe..86aa16b06 100644
--- a/src/Ocelot/DependencyInjection/OcelotBuilder.cs
+++ b/src/Ocelot/DependencyInjection/OcelotBuilder.cs
@@ -137,6 +137,7 @@ public OcelotBuilder(IServiceCollection services, IConfiguration configurationRo
Services.AddOcelotMessageInvokerPool();
Services.AddOcelotMetadata();
Services.AddOcelotRateLimiting();
+ Services.AddAspNetRateLimiting(configurationRoot); // Feature: AspNet Rate Limiting
// Add ASP.NET services
var assembly = typeof(FileConfigurationController).GetTypeInfo().Assembly;
diff --git a/src/Ocelot/RateLimiting/RateLimitingMiddleware.cs b/src/Ocelot/RateLimiting/RateLimitingMiddleware.cs
index 296d60ceb..a032358ae 100644
--- a/src/Ocelot/RateLimiting/RateLimitingMiddleware.cs
+++ b/src/Ocelot/RateLimiting/RateLimitingMiddleware.cs
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using Ocelot.Configuration;
@@ -39,6 +40,15 @@ public Task Invoke(HttpContext context)
return _next.Invoke(context);
}
+ if (!options.Policy.IsEmpty())
+ {
+ // Add EnableRateLimiting attribute to endpoint, so that .Net rate limiter can pick it up and do its thing
+ var metadata = new EndpointMetadataCollection(new EnableRateLimitingAttribute(options.Policy));
+ var endpoint = new Endpoint(null, metadata, "tempEndpoint");
+ context.SetEndpoint(endpoint);
+ return _next.Invoke(context);
+ }
+
var identity = Identify(context, options, downstreamRoute);
if (IsWhitelisted(identity, options))
{
diff --git a/src/Ocelot/RateLimiting/RateLimitingMiddlewareExtensions.cs b/src/Ocelot/RateLimiting/RateLimitingMiddlewareExtensions.cs
new file mode 100644
index 000000000..6e9f1219a
--- /dev/null
+++ b/src/Ocelot/RateLimiting/RateLimitingMiddlewareExtensions.cs
@@ -0,0 +1,26 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Ocelot.Middleware;
+
+namespace Ocelot.RateLimiting.Middleware;
+
+public static class RateLimitingMiddlewareExtensions
+{
+ public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder builder)
+ {
+ builder.UseMiddleware();
+
+ //use AspNet rate limiter
+ builder.UseWhen(UseAspNetRateLimiter, rateLimitedApp =>
+ {
+ rateLimitedApp.UseRateLimiter();
+ });
+ return builder;
+ }
+
+ private static bool UseAspNetRateLimiter(HttpContext httpContext)
+ {
+ var downstreamRoute = httpContext.Items.DownstreamRoute();
+ return !string.IsNullOrWhiteSpace(downstreamRoute?.RateLimitOptions?.Policy);
+ }
+}
diff --git a/test/Ocelot.AcceptanceTests/RateLimiting/AspNetRateLimitingTests.cs b/test/Ocelot.AcceptanceTests/RateLimiting/AspNetRateLimitingTests.cs
new file mode 100644
index 000000000..c0b0a00bb
--- /dev/null
+++ b/test/Ocelot.AcceptanceTests/RateLimiting/AspNetRateLimitingTests.cs
@@ -0,0 +1,76 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.RateLimiting;
+using Microsoft.Extensions.DependencyInjection;
+using Ocelot.Configuration.File;
+using Ocelot.DependencyInjection;
+using System.Threading.RateLimiting;
+
+namespace Ocelot.AcceptanceTests.RateLimiting;
+
+public class AspNetRateLimitingTests: RateLimitingSteps
+{
+ private const string FixedWindowPolicyName = "RateLimitPolicy";
+ private int _rateLimitLimit;
+ private int _rateLimitWindow;
+ private string _quotaExceededMessage;
+
+ [Fact]
+ [Trait("Feat", "2138")]
+ public async Task Should_RateLimit()
+ {
+ _rateLimitLimit = 3;
+ _rateLimitWindow = 1;
+ _quotaExceededMessage = "woah!";
+ var port = PortFinder.GetRandomPort();
+ var route = GivenRoute(port, FixedWindowPolicyName);
+ var configuration = GivenConfiguration(route);
+ GivenThereIsAServiceRunningOnPath(port, "/");
+ GivenThereIsAConfiguration(configuration);
+ GivenOcelotIsRunning(WithRateLimiter);
+ await WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 1);
+ ThenTheStatusCodeShouldBeOK();
+ await WhenIGetUrlOnTheApiGatewayMultipleTimes("/", _rateLimitLimit - 1);
+ ThenTheStatusCodeShouldBeOK();
+ await WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 1);
+ ThenTheStatusCodeShouldBe(HttpStatusCode.TooManyRequests);
+ ThenTheResponseBodyShouldBe(_quotaExceededMessage);
+ GivenIWait((1000 * _rateLimitWindow) + 100);
+ await WhenIGetUrlOnTheApiGatewayMultipleTimes("/", 1);
+ ThenTheStatusCodeShouldBeOK();
+ }
+
+ private FileRoute GivenRoute(int port, string rateLimitPolicyName)
+ {
+ var route = GivenRoute(port);
+ route.RateLimitOptions = new()
+ {
+ Policy = rateLimitPolicyName,
+ };
+ return route;
+ }
+
+ private FileConfiguration GivenConfiguration(params FileRoute[] routes)
+ {
+ var config = base.GivenConfiguration(routes);
+ config.GlobalConfiguration.RateLimitOptions = new()
+ {
+ QuotaExceededMessage = _quotaExceededMessage,
+ HttpStatusCode = StatusCodes.Status429TooManyRequests,
+ };
+ return config;
+ }
+
+ private void WithRateLimiter(IServiceCollection services) => services
+ .AddOcelot().Services
+ .AddRateLimiter(op =>
+ {
+ op.AddFixedWindowLimiter(FixedWindowPolicyName, options =>
+ {
+ options.PermitLimit = _rateLimitLimit;
+ options.Window = TimeSpan.FromSeconds(_rateLimitWindow);
+ options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
+ options.QueueLimit = 0;
+ });
+ });
+}
diff --git a/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs b/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs
index eaa5de7fe..d01bc104a 100644
--- a/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs
+++ b/test/Ocelot.UnitTests/Configuration/Validation/RouteFluentValidatorTests.cs
@@ -1,6 +1,7 @@
using FluentValidation.Results;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
+using Ocelot.Configuration;
using Ocelot.Configuration.File;
using Ocelot.Configuration.Validator;
using System.Reflection;
diff --git a/test/Ocelot.UnitTests/DownstreamRouteFinder/DiscoveryDownstreamRouteFinderTests.cs b/test/Ocelot.UnitTests/DownstreamRouteFinder/DiscoveryDownstreamRouteFinderTests.cs
index c11dca83a..090cf1c06 100644
--- a/test/Ocelot.UnitTests/DownstreamRouteFinder/DiscoveryDownstreamRouteFinderTests.cs
+++ b/test/Ocelot.UnitTests/DownstreamRouteFinder/DiscoveryDownstreamRouteFinderTests.cs
@@ -57,6 +57,7 @@ public void Should_create_downstream_route()
[Fact]
[Trait("Feat", "585")]
[Trait("Feat", "1229")]
+ [Trait("Feat", "2138")]
public void Should_create_downstream_route_with_rate_limit_options()
{
// Arrange
@@ -64,6 +65,7 @@ public void Should_create_downstream_route_with_rate_limit_options()
{
EnableRateLimiting = true,
ClientIdHeader = "test",
+ Policy = "test",
};
var downstreamRoute = new DownstreamRouteBuilder()
.WithServiceName("auth")
diff --git a/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs b/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs
index 4eba538ce..6d0924ea9 100644
--- a/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs
+++ b/test/Ocelot.UnitTests/RateLimiting/RateLimitingMiddlewareTests.cs
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Caching.Memory;
using Ocelot.Configuration;
using Ocelot.Configuration.Builder;
@@ -257,6 +258,36 @@ public async Task Invoke_NoClientHeader_Status503_ShouldLogWarning()
_logger.Verify(x => x.LogWarning(err.Message), Times.Once);
}
+ [Fact]
+ [Trait("Feat", "2138")]
+ public async Task Should_add_EnableRateLimittingAttribute_When_AspNetRateLimiting()
+ {
+ // Arrange
+ const long limit = 3L;
+ RateLimitOptions options = new()
+ {
+ ClientIdHeader = "ClientId",
+ Rule = new("1s", "1s", limit),
+ Policy = "testPolicy",
+ };
+ var downstreamRoute = GivenDownstreamRoute(options);
+ var route = GivenRoute(downstreamRoute);
+ var dsHolder = new _DownstreamRouteHolder_(new(), route);
+
+ // Act
+ var contexts = await WhenICallTheMiddlewareMultipleTimes(limit+1, dsHolder);
+
+ // Assert
+ _downstreamResponses.ForEach(dsr => dsr.ShouldBeNull());
+ contexts.ForEach(ctx =>
+ {
+ var endpoint = ctx.GetEndpoint();
+ endpoint.ShouldNotBeNull();
+ var rateLimitAttribute = endpoint.Metadata.GetMetadata();
+ rateLimitAttribute.PolicyName.ShouldBe("testPolicy");
+ });
+ }
+
private static RateLimitOptions GivenRateLimitOptions(RateLimitRule rule = null, [CallerMemberName] string testName = null) => new(
enableRateLimiting: true,
clientIdHeader: "ClientId",