-
Notifications
You must be signed in to change notification settings - Fork 369
Add implementing-rate-limiting skill #267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f299ab4
12d9eb7
75eff40
0382dfc
ab41c24
f127d50
33f802d
d086904
3354acd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| "name": "aspnetcore", | ||
| "version": "0.1.0", | ||
| "description": "ASP.NET Core web development skills including middleware, endpoints, real-time communication, and API patterns.", | ||
| "skills": "./skills/" | ||
| } | ||
|
Comment on lines
+1
to
+6
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,212 @@ | ||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||
| name: implementing-rate-limiting | ||||||||||||||||||||||||||
| description: > | ||||||||||||||||||||||||||
| Implement .NET 7+ built-in rate limiting middleware (AddRateLimiter, UseRateLimiter) with correct | ||||||||||||||||||||||||||
| algorithm selection, partitioning, and response handling. USE FOR: adding API rate limiting, | ||||||||||||||||||||||||||
| per-client/per-IP/per-user throttling, configuring sliding window or token bucket algorithms, | ||||||||||||||||||||||||||
| fixing rate limiter returning 503 instead of 429, fixing silently inactive rate limiting. | ||||||||||||||||||||||||||
| DO NOT USE FOR: distributed rate limiting across multiple servers (need Redis-backed solution), | ||||||||||||||||||||||||||
| rate limiting at API gateway/reverse proxy layer (YARP, nginx, Azure API Management), | ||||||||||||||||||||||||||
| pre-.NET 7 projects (no built-in support). | ||||||||||||||||||||||||||
| --- | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| # Implementing Rate Limiting in ASP.NET Core (.NET 7+) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ## Inputs | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| | Input | Required | Description | | ||||||||||||||||||||||||||
| |-------|----------|-------------| | ||||||||||||||||||||||||||
| | API endpoints to protect | Yes | Which routes need rate limiting | | ||||||||||||||||||||||||||
| | Rate limit requirements | Yes | Requests per window, per-client vs global | | ||||||||||||||||||||||||||
| | .NET version | No | Must be .NET 7+ for built-in support | | ||||||||||||||||||||||||||
|
Comment on lines
+17
to
+21
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ## Workflow | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ### Step 1: Choose the right algorithm | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| | Algorithm | Best For | Behavior | | ||||||||||||||||||||||||||
| |-----------|----------|----------| | ||||||||||||||||||||||||||
| | **Fixed Window** | Simple per-minute/per-hour limits | Counter resets at window boundary. ⚠️ Burst problem: 100 req at end of window + 100 at start of next = 200 in 1 second | | ||||||||||||||||||||||||||
| | **Sliding Window** | Smoother rate distribution | Divides window into segments, slides across time. Avoids burst problem | | ||||||||||||||||||||||||||
| | **Token Bucket** | Allowing controlled bursts | Tokens replenish at fixed rate, requests consume tokens. Good for APIs that should allow short bursts | | ||||||||||||||||||||||||||
| | **Concurrency Limiter** | Limiting simultaneous requests | Caps concurrent in-flight requests, not rate. Good for protecting expensive endpoints | | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
Comment on lines
+27
to
+33
|
||||||||||||||||||||||||||
| **Common mistake:** Using Fixed Window when you need smooth distribution. A fixed window of "100 per minute" allows 200 requests in 2 seconds if they straddle the window boundary. | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ### Step 2: Configure the rate limiter in Program.cs | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ```csharp | ||||||||||||||||||||||||||
| using Microsoft.AspNetCore.RateLimiting; | ||||||||||||||||||||||||||
| using System.Security.Claims; | ||||||||||||||||||||||||||
| using System.Threading.RateLimiting; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| builder.Services.AddRateLimiter(options => | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| // CRITICAL: Set rejection status code — default is 503! Most APIs should use 429 | ||||||||||||||||||||||||||
| options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Global rate limiter: sliding window (avoids fixed window burst problem) | ||||||||||||||||||||||||||
| options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context => | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| // Use the resolved remote IP. If behind a reverse proxy, configure | ||||||||||||||||||||||||||
| // ForwardedHeadersOptions (KnownProxies/KnownNetworks) so RemoteIpAddress | ||||||||||||||||||||||||||
| // reflects the real client IP. Do NOT trust X-Forwarded-For directly. | ||||||||||||||||||||||||||
| var ipAddress = context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| return RateLimitPartition.GetSlidingWindowLimiter( | ||||||||||||||||||||||||||
| partitionKey: ipAddress, | ||||||||||||||||||||||||||
| factory: _ => new SlidingWindowRateLimiterOptions | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| PermitLimit = 100, | ||||||||||||||||||||||||||
| Window = TimeSpan.FromMinutes(1), | ||||||||||||||||||||||||||
| SegmentsPerWindow = 6, // 10-second segments for smooth distribution | ||||||||||||||||||||||||||
| QueueProcessingOrder = QueueProcessingOrder.OldestFirst, | ||||||||||||||||||||||||||
| QueueLimit = 0 // Reject immediately, don't queue | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Named policy: sliding window for sensitive endpoints | ||||||||||||||||||||||||||
| options.AddSlidingWindowLimiter("api-sensitive", slidingOptions => | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| slidingOptions.PermitLimit = 10; | ||||||||||||||||||||||||||
| slidingOptions.Window = TimeSpan.FromMinutes(1); | ||||||||||||||||||||||||||
| slidingOptions.SegmentsPerWindow = 6; // 10-second segments | ||||||||||||||||||||||||||
| slidingOptions.QueueLimit = 0; | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Named policy: token bucket for search API (allows bursts) | ||||||||||||||||||||||||||
| options.AddTokenBucketLimiter("search", tokenOptions => | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| tokenOptions.TokenLimit = 20; // Max burst size | ||||||||||||||||||||||||||
| tokenOptions.ReplenishmentPeriod = TimeSpan.FromSeconds(10); | ||||||||||||||||||||||||||
| tokenOptions.TokensPerPeriod = 5; // 5 tokens every 10 seconds | ||||||||||||||||||||||||||
| tokenOptions.QueueLimit = 0; | ||||||||||||||||||||||||||
| tokenOptions.AutoReplenishment = true; | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Named policy: concurrency limiter for expensive operations | ||||||||||||||||||||||||||
| options.AddConcurrencyLimiter("reports", concurrencyOptions => | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| concurrencyOptions.PermitLimit = 5; // Max 5 simultaneous report generations | ||||||||||||||||||||||||||
| concurrencyOptions.QueueLimit = 10; | ||||||||||||||||||||||||||
| concurrencyOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Custom response for rejected requests | ||||||||||||||||||||||||||
| options.OnRejected = async (context, cancellationToken) => | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter)) | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| context.HttpContext.Response.Headers["Retry-After"] = | ||||||||||||||||||||||||||
| ((int)retryAfter.TotalSeconds).ToString(); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| await context.HttpContext.Response.WriteAsJsonAsync(new | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| error = "Too many requests", | ||||||||||||||||||||||||||
| retryAfterSeconds = context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var r) | ||||||||||||||||||||||||||
| ? (int)r.TotalSeconds : 60 | ||||||||||||||||||||||||||
| }, cancellationToken); | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ### Step 3: CRITICAL — Middleware ordering and application | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| ```csharp | ||||||||||||||||||||||||||
| var app = builder.Build(); | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Middleware ordering matters: | ||||||||||||||||||||||||||
| // UseRouting → UseAuthentication → UseAuthorization → UseRateLimiter | ||||||||||||||||||||||||||
| // Placing UseRateLimiter() AFTER UseAuthorization() lets you partition by authenticated user | ||||||||||||||||||||||||||
| app.UseRouting(); | ||||||||||||||||||||||||||
| app.UseAuthentication(); | ||||||||||||||||||||||||||
| app.UseAuthorization(); | ||||||||||||||||||||||||||
| app.UseRateLimiter(); // ← AFTER auth so user claims are available for partitioning | ||||||||||||||||||||||||||
|
Comment on lines
+120
to
+125
|
||||||||||||||||||||||||||
| // UseRouting → UseAuthentication → UseAuthorization → UseRateLimiter | |
| // Placing UseRateLimiter() AFTER UseAuthorization() lets you partition by authenticated user | |
| app.UseRouting(); | |
| app.UseAuthentication(); | |
| app.UseAuthorization(); | |
| app.UseRateLimiter(); // ← AFTER auth so user claims are available for partitioning | |
| // UseRouting → UseAuthentication → UseRateLimiter → UseAuthorization | |
| // Placing UseRateLimiter() AFTER authentication (but BEFORE authorization) lets you partition by authenticated user | |
| app.UseRouting(); | |
| app.UseAuthentication(); | |
| app.UseRateLimiter(); // ← AFTER auth so user claims are available for partitioning | |
| app.UseAuthorization(); |
Copilot
AI
Mar 6, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The per-user partitioning snippet references ClaimTypes.NameIdentifier but the snippet doesn’t include using System.Security.Claims; (and other snippets in this file explicitly include needed usings). Add the missing using (or fully-qualify ClaimTypes) so the example compiles as-written.
Copilot
AI
Mar 24, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This row says UseRateLimiter() should be moved after UseAuthorization() so user claims are available. User claims are available after UseAuthentication(), and recommended guidance is to run UseRateLimiter() after routing + authentication, but before authorization, to ensure rate limiting still applies to unauthorized requests when desired.
| | `UseRateLimiter()` before `UseRouting()` | Endpoint-specific policies silently don't apply | Move after `UseRouting()` and after `UseAuthorization()` so user claims are available for partitioning | | |
| | `UseRateLimiter()` before `UseRouting()` | Endpoint-specific policies silently don't apply | Move after `UseRouting()` and `UseAuthentication()`, but before `UseAuthorization()`, so user claims are available for partitioning while still rate limiting unauthorized requests | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR description/file list mentions
plugins/dotnet/...andtests/dotnet/..., but this change introduces a newaspnetcoreplugin (plugins/aspnetcore/...) and correspondingtests/aspnetcore/.... Please align the PR description (or the paths) so reviewers and automation know which plugin this skill belongs to.