-
Notifications
You must be signed in to change notification settings - Fork 368
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 3 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 | |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,143 @@ | ||||||
| scenarios: | ||||||
| - name: "Add per-client rate limiting to an ASP.NET Core API" | ||||||
| prompt: | | ||||||
| I need to add rate limiting to my ASP.NET Core 8 API. Requirements: | ||||||
| 1. Global limit: 100 requests per minute per IP address | ||||||
| 2. Stricter limit on POST /api/orders: 10 requests per minute per authenticated user | ||||||
| 3. No limit on GET /healthz | ||||||
| 4. Return a proper 429 response with Retry-After header when rate limited | ||||||
|
|
||||||
| Show me the full Program.cs configuration and how to apply it to the endpoints. | ||||||
| assertions: | ||||||
| - type: "output_matches" | ||||||
| pattern: "(AddRateLimiter|UseRateLimiter)" | ||||||
| - type: "output_matches" | ||||||
| pattern: "(429|TooManyRequests)" | ||||||
| - type: "output_matches" | ||||||
| pattern: "(RequireRateLimiting|EnableRateLimiting)" | ||||||
| - type: "output_matches" | ||||||
| pattern: "DisableRateLimiting" | ||||||
| - type: "output_matches" | ||||||
| pattern: "/healthz" | ||||||
| rubric: | ||||||
| - "Set RejectionStatusCode to 429 (NOT the default 503) — this is critical" | ||||||
| - "Placed UseRateLimiter() AFTER UseRouting() in the middleware pipeline" | ||||||
| - "Used partitioned rate limiter for per-IP global limiting" | ||||||
| - "Applied a named policy to the POST /api/orders endpoint with per-user partitioning" | ||||||
| - "Disabled rate limiting on the healthz endpoint" | ||||||
| - "Configured OnRejected callback that sets Retry-After header" | ||||||
| - "Chose an appropriate algorithm (sliding window or token bucket preferred over fixed window to avoid burst problem)" | ||||||
| timeout: 120 | ||||||
|
|
||||||
| - name: "Rate limiting silently inactive without UseRateLimiter" | ||||||
| prompt: | | ||||||
| I configured rate limiting in my ASP.NET Core 8 API with AddRateLimiter and applied RequireRateLimiting to my endpoints, but requests are never rate limited — even when I send 1000 requests per second, they all succeed. What's wrong? | ||||||
| setup: | ||||||
| files: | ||||||
| - path: "Program.cs" | ||||||
| content: | | ||||||
| using Microsoft.AspNetCore.RateLimiting; | ||||||
| using System.Threading.RateLimiting; | ||||||
|
|
||||||
| var builder = WebApplication.CreateBuilder(args); | ||||||
|
|
||||||
| builder.Services.AddRateLimiter(options => | ||||||
| { | ||||||
| options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; | ||||||
| options.AddFixedWindowLimiter("api", o => | ||||||
| { | ||||||
| o.PermitLimit = 10; | ||||||
| o.Window = TimeSpan.FromMinutes(1); | ||||||
| }); | ||||||
| }); | ||||||
|
|
||||||
| var app = builder.Build(); | ||||||
| app.UseRouting(); | ||||||
| app.UseAuthorization(); | ||||||
| app.MapGet("/api/data", () => "Hello!").RequireRateLimiting("api"); | ||||||
| app.Run(); | ||||||
| - path: "SilentBug.csproj" | ||||||
| content: | | ||||||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | ||||||
| <PropertyGroup> | ||||||
| <TargetFramework>net8.0</TargetFramework> | ||||||
| </PropertyGroup> | ||||||
| </Project> | ||||||
| assertions: | ||||||
| - type: "output_matches" | ||||||
| pattern: "(UseRateLimiter|app\\.UseRateLimiter)" | ||||||
| rubric: | ||||||
| - "Identified the root cause: app.UseRateLimiter() is missing from the middleware pipeline" | ||||||
| - "Explained that AddRateLimiter only registers services but UseRateLimiter is required to activate the middleware" | ||||||
| - "Showed the correct middleware ordering: UseRateLimiter() after UseRouting() and UseAuthorization()" | ||||||
|
||||||
| - "Showed the correct middleware ordering: UseRateLimiter() after UseRouting() and UseAuthorization()" | |
| - "Showed the correct middleware ordering: UseRouting() → UseAuthentication() → UseRateLimiter() → UseAuthorization()" |
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 output_not_matches pattern is likely too broad and can fail correct answers that merely mention these APIs while explaining what not to do. Consider tightening it to only reject code-like output (e.g., matching builder.Services.AddRateLimiter\s*\() or disallowing affirmative recommendations rather than the presence of AddRateLimiter/UseRateLimiter tokens.
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 scenario says “no bursting allowed” and “steady, even flow”, but the rubric explicitly lists token bucket as an acceptable recommendation. Token bucket is typically used specifically to allow bursts unless configured with a very small TokenLimit. Consider updating the rubric wording to prefer sliding window for this scenario (or clarify the token bucket configuration needed to prevent bursts).
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.