Skip to content

Conversation

@yjorayev
Copy link

@yjorayev yjorayev commented Nov 6, 2024

Closes #2138

Discussion thread

Proposed Changes

  • Includes changes to use .Net middleware
  • Middleware type for rate limiting can be specified in ocelot.json. It defaults to Ocelot's existing rate limiter if not specified
  • Also added some Unit Tests, Integration test and a sample

Documentation block

Predecessor

@raman-m raman-m added feature A new feature Rate Limiting Ocelot feature: Rate Limiting labels Nov 6, 2024
Copy link
Member

@raman-m raman-m left a comment

Choose a reason for hiding this comment

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

Thanks for PR creation!

Please move current test/Ocelot.IntegrationTests/RateLimitingTests to Ocelot.AcceptanceTests project, we already have a suitable class: ClientRateLimitingTests. However, these tests only cover the Ocelot feature "Rate Limit by Client's Header." Since you are proposing a new feature, a new testing class should be created.

P.S. I recommend (and require) inheriting from Steps, which offers many useful testing helpers, so there's no need to reinvent the wheel.

@raman-m raman-m changed the title Users/yjorayev/dotnet ratelimit Introduce ASP.NET rate limiting Nov 6, 2024
@raman-m raman-m changed the title Introduce ASP.NET rate limiting #2138 Introduce ASP.NET rate limiting Nov 6, 2024
@raman-m
Copy link
Member

raman-m commented Nov 6, 2024

Yhlas, thank you once again. Here's the current status: I will request an official code review after transitioning tests to acceptance testing. Please remember to draft the preliminary docs.

Regarding the delivery vision, I cannot include it in the current Autumn'24 milestone due to anticipated multiple development rounds. Post-release, our team intends to upgrade Ocelot to .NET 9. It appears your feature may be incorporated into the release alongside this upgrade. Therefore, I suggest testing the PR code with the current .NET 9 RC. Is that okay? Consequently, we will target two frameworks in the project files: net8.0 and net9.0

@yjorayev
Copy link
Author

yjorayev commented Nov 7, 2024

... I suggest testing the PR code with the current .NET 9 RC. Is that okay? Consequently, we will target two frameworks in the project files: net8.0 and net9.0

I targeted Ocelot project and a sample to net 9.0-rc. No issue on manual testing.

I will start working on docs for this in a few days. Thanks!

@yjorayev yjorayev requested a review from raman-m November 7, 2024 06:36
@raman-m raman-m requested review from RaynaldM and ggnaegi November 8, 2024 10:32
Copy link
Member

@raman-m raman-m left a comment

Choose a reason for hiding this comment

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

We can enhance the quality of the PR: my suggestions are outlined below 👇
FYI, I will revisit this PR once it has been reviewed by a team member as well.

P.S. Keep in mind Line-Ending Gotchas aka EOL Fun. I saw some files have fake changes because of changed line endings.

@ggnaegi
Copy link
Collaborator

ggnaegi commented Nov 8, 2024

@raman-m That's a great idea, but we are stuck with the .NET rate limiters specs, you can't modify them on the fly, as you could with the current limiters. The rate limiters policies must be known, and therefore configured during startup

@raman-m
Copy link
Member

raman-m commented Nov 9, 2024

but we are stuck with the .NET rate limiters specs, you can't modify them on the fly, as you could with the current limiters.

@ggnaegi, what is your suggestion?
Could you please clarify the problem for the author?
Why not conduct a separate code review yourself?

@raman-m raman-m added the conflicts Feature branch has merge conflicts label Nov 11, 2024
@raman-m
Copy link
Member

raman-m commented Nov 11, 2024

Unfortunately, merge conflicts have arisen due to the recent merging of PR #1307 and its commit da9d6fa into the develop branch. I recommend to perform rebasing.

yjorayev and others added 11 commits November 12, 2024 23:56
…Context` response accessed via `IHttpContextAccessor` (ThreeMammals#1307)

* set rate limiting headers on the proper httpcontext

* fix Retry-After header

* merge fix

* refactor of ClientRateLimitTests

* merge fix

* Fix build after rebasing

* EOL: test/Ocelot.AcceptanceTests/Steps.cs

* Add `RateLimitingSteps`

* code review by @raman-m

* Inject IHttpContextAccessor, not IServiceProvider

* Ocelot's rate-limiting headers have become legacy

* Headers definition life hack

* A good StackOverflow link

---------

Co-authored-by: Jolanta Łukawska <jolanta.lukawska@outlook.com>
Co-authored-by: Raman Maksimchuk <dotnet044@gmail.com>
Co-authored-by: Raman Maksimchuk <dotnet044@gmail.com>
@yjorayev yjorayev force-pushed the users/yjorayev/dotnet-ratelimit branch from 59ef966 to e8987ad Compare November 13, 2024 06:15
@raman-m raman-m force-pushed the develop branch 7 times, most recently from 908d84f to 0678e7a Compare April 19, 2025 15:13
@raman-m
Copy link
Member

raman-m commented May 27, 2025

@yjorayev Hi Yhlas!
Do you have the enthusiasm proceed with development? Could you begin by addressing the merge conflicts, please?

If you have no objections, I would like to invite you to review the following PR →

@raman-m raman-m added conflicts Feature branch has merge conflicts Winter'26 Winter 2026 release labels May 27, 2025
@raman-m raman-m added this to the Summer'25 milestone May 27, 2025
@raman-m
Copy link
Member

raman-m commented Aug 13, 2025

Predecessor

@yjorayev FYI
We need to merge this PR first due to the agreement on the JSON schema, which includes global configuration and specific Metadata options.

P.S. Could you fix all the merge conflicts by simply merging develop into the feature branch, please?

@raman-m raman-m removed the conflicts Feature branch has merge conflicts label Sep 24, 2025
@ThreeMammals ThreeMammals deleted a comment from yjorayev Sep 24, 2025
@ThreeMammals ThreeMammals deleted a comment from yjorayev Sep 24, 2025
@ThreeMammals ThreeMammals deleted a comment from yjorayev Sep 24, 2025
Copy link
Member

@raman-m raman-m left a comment

Choose a reason for hiding this comment

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

Hi Yhlas!
Do you recall what you were doing a year ago? Do you plan to continue contributing further?

With the merge conflicts resolved and #2294 successfully integrated, it seems like the perfect time to move forward with development. Unfortunately, the acceptance test is currently failing.

Comment on lines +10 to +19
builder.Services.AddRateLimiter(op =>
{
op.AddFixedWindowLimiter(policyName: "fixed", options =>
{
options.PermitLimit = 2;
options.Window = TimeSpan.FromSeconds(12);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = 0;
});
});
Copy link
Member

Choose a reason for hiding this comment

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

I disagree! In this case, we haven't proposed anything new.
I think we should provide the user with an option to choose between configuration approaches. We could consider having a separate Ocelot rate-limiting rule to manage app auto-configurations.

Copy link
Collaborator

@ggnaegi ggnaegi Sep 25, 2025

Choose a reason for hiding this comment

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

@raman-m it's basically what i mentioned:
Yes, that's the main concern; we essentially need to reinvent the wheel for the middleware part. I've tried to "dynamize" the policies myself, but I believe it’s not possible.
The policies are setup during startup, you can't modify them after that, and you can't mix them (as of 2024, maybe now it's possible).

Copy link
Member

Choose a reason for hiding this comment

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

We can't mix policies, but we can define as many as needed, right?
From your words, did you mean that the "fixed" policy is global in this code?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yes, it's global and they can't be modified.

/// <value>
/// A string of rate limit policy name.
/// </value>
public string Policy { get; set; }
Copy link
Member

Choose a reason for hiding this comment

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

You might want to consider removing this property and using FileRateLimitByAspNetRule.Policy instead.

When(route => route.RateLimitOptions != null && route.RateLimitOptions.EnableRateLimiting != false, () =>
When(route => route.RateLimitOptions != null
&& route.RateLimitOptions.EnableRateLimiting != false
&& string.IsNullOrWhiteSpace(route.RateLimitOptions.Policy), () =>
Copy link
Member

Choose a reason for hiding this comment

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

We still need to validate the rule algorithm properties, even if the ASP.NET policy is in place.

fromRule.Period.IfEmpty(RateLimitRule.DefaultPeriod),
fromRule.PeriodTimespan.HasValue ? $"{fromRule.PeriodTimespan.Value}s" : fromRule.Wait,
fromRule.Limit ?? RateLimitRule.ZeroLimit);
Policy = fromRule.Policy;
Copy link
Member

Choose a reason for hiding this comment

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

An option of a flawed rule 😄 Consider removing!

QuotaMessage = fromOptions.QuotaMessage.IfEmpty(DefaultQuotaMessage);
KeyPrefix = fromOptions.KeyPrefix.IfEmpty(DefaultCounterPrefix);
Rule = fromOptions.Rule ?? RateLimitRule.Empty;
Policy = fromOptions.Policy;
Copy link
Member

Choose a reason for hiding this comment

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

It's fine because of the copy constructor ✔️

Comment on lines +59 to +61
var globalRateLimitOptions = configurationRoot.Get<FileConfiguration>()?.GlobalConfiguration?.RateLimitOptions;
var rejectStatusCode = globalRateLimitOptions?.HttpStatusCode ?? StatusCodes.Status429TooManyRequests;
var rejectedMessage = globalRateLimitOptions?.QuotaExceededMessage ?? "API calls quota exceeded!";
Copy link
Member

Choose a reason for hiding this comment

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

Merging in the wrong place can be risky.
We've assigned the IRateLimitOptionsCreator service specifically for merging options. Please refer to the RateLimitOptionsCreator class for details.

Comment on lines +66 to +67
rejectedContext.HttpContext.Response.StatusCode = rejectStatusCode;
await rejectedContext.HttpContext.Response.WriteAsync(rejectedMessage, token);
Copy link
Member

Choose a reason for hiding this comment

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

It's amusing that we implement the same approach in Ocelot's middleware. Perhaps adding a virtual method in the middleware to connect to the handler could work? 😉


Services.AddRateLimiting(); // Feature: Rate Limiting
#if NET7_0_OR_GREATER
Services.AddAspNetRateLimiting(configurationRoot); // Feature: AspNet Rate Limiting
Copy link
Member

Choose a reason for hiding this comment

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

I strongly believe we shouldn't introduce a new feature, whether public or internal. Since the method is already auto-enabled internally, it seems we can leave it as-is. However, we definitely need to review this design.


public static class RateLimitingMiddlewareExtensions
{
public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder builder)
Copy link
Member

Choose a reason for hiding this comment

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

Great! It looks like the number of references is zero.

Image

So, how are we supposed to use this extension method? Just mentioning in the documentation that users should call it?


[Fact]
[Trait("Feat", "2138")]
public async Task Should_RateLimit()
Copy link
Member

Choose a reason for hiding this comment

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

Currently, this test fails ❗

@raman-m raman-m self-assigned this Sep 24, 2025
@raman-m
Copy link
Member

raman-m commented Sep 24, 2025

@ggnaegi commented on Nov 14, 2024

Yes, that's the main concern; we essentially need to reinvent the wheel for the middleware part. I've tried to "dynamize" the policies myself, but I believe it’s not possible.

We don't need to reinvent anything, just a useful JSON configuration.
ASP.NET middleware can be part of Ocelot pipeline at position before Ocelot's own middleware, here:

app.UseMiddleware<DownstreamRequestInitialiserMiddleware>();
// We check whether the request is ratelimit, and if there is no continue processing
app.UseMiddleware<RateLimitingMiddleware>();

Place it after DownstreamRequestInitialiserMiddleware but before RateLimitingMiddleware. Inheriting from the ASP.NET Core one might seem like a crazy idea, but it’s definitely possible!

However, at least we can retain Microsoft's RateLimiter implementations and replace the current ones.

Since we seem to agree on independent evolution, we should avoid replacing or combining anything. Instead, we need to design an effective embedded module using JSON configurations, supplemented by additional C# helpers if necessary.

@raman-m raman-m added the conflicts Feature branch has merge conflicts label Dec 27, 2025
@raman-m raman-m removed the conflicts Feature branch has merge conflicts label Jan 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature A new feature Rate Limiting Ocelot feature: Rate Limiting Winter'26 Winter 2026 release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Apply rate limiting globally Global rate limiting for all, multiple routes instead of every route

5 participants