Skip to content

Commit 98f133c

Browse files
committed
feat: rate limiting configuration with IP-based partitioning
1 parent 7f10db6 commit 98f133c

File tree

3 files changed

+106
-15
lines changed

3 files changed

+106
-15
lines changed

src/Dotnet.Samples.AspNetCore.WebApi/Extensions/ServiceCollectionExtensions.cs

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
namespace Dotnet.Samples.AspNetCore.WebApi.Extensions;
1515

1616
/// <summary>
17-
/// Extension methods for WebApplicationBuilder to encapsulate service configuration.
17+
/// Extension methods for IServiceCollection to encapsulate service configuration.
1818
/// </summary>
19-
public static class ServiceCollectionExtensions
19+
public static partial class ServiceCollectionExtensions
2020
{
2121
/// <summary>
2222
/// Adds DbContextPool with SQLite configuration for PlayerDbContext.
@@ -54,16 +54,25 @@ IWebHostEnvironment environment
5454
/// <see href="https://learn.microsoft.com/en-us/aspnet/core/security/cors"/>
5555
/// </summary>
5656
/// <param name="services">The IServiceCollection instance.</param>
57+
/// <param name="environment">The web host environment.</param>
5758
/// <returns>The IServiceCollection for method chaining.</returns>
58-
public static IServiceCollection AddCorsDefaultPolicy(this IServiceCollection services)
59+
public static IServiceCollection AddCorsDefaultPolicy(
60+
this IServiceCollection services,
61+
IWebHostEnvironment environment
62+
)
5963
{
60-
services.AddCors(options =>
64+
if (environment.IsDevelopment())
6165
{
62-
options.AddDefaultPolicy(corsBuilder =>
66+
services.AddCors(options =>
6367
{
64-
corsBuilder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
68+
options.AddDefaultPolicy(corsBuilder =>
69+
{
70+
corsBuilder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
71+
});
6572
});
66-
});
73+
}
74+
75+
// No CORS configured in Production or other environments
6776

6877
return services;
6978
}
@@ -143,4 +152,39 @@ public static IServiceCollection RegisterPlayerRepository(this IServiceCollectio
143152
services.AddScoped<IPlayerRepository, PlayerRepository>();
144153
return services;
145154
}
155+
156+
/// <summary>
157+
/// Adds rate limiting configuration with IP-based partitioning.
158+
/// <br />
159+
/// <see href="https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit"/>
160+
/// </summary>
161+
/// <param name="services">The IServiceCollection instance.</param>
162+
/// <returns>The IServiceCollection for method chaining.</returns>
163+
public static IServiceCollection AddFixedWindowRateLimiter(this IServiceCollection services)
164+
{
165+
services.AddRateLimiter(options =>
166+
{
167+
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(
168+
httpContext =>
169+
{
170+
var partitionKey = HttpContextUtilities.ExtractIpAddress(httpContext);
171+
172+
return RateLimitPartition.GetFixedWindowLimiter(
173+
partitionKey: partitionKey,
174+
factory: _ => new FixedWindowRateLimiterOptions
175+
{
176+
PermitLimit = 60,
177+
Window = TimeSpan.FromSeconds(60),
178+
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
179+
QueueLimit = 0
180+
}
181+
);
182+
}
183+
);
184+
185+
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
186+
});
187+
188+
return services;
189+
}
146190
}

src/Dotnet.Samples.AspNetCore.WebApi/Program.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@
2222

2323
/* Controllers -------------------------------------------------------------- */
2424

25-
builder.Services.AddControllers();
26-
builder.Services.AddCorsDefaultPolicy();
2725
builder.Services.AddHealthChecks();
26+
builder.Services.AddControllers();
2827
builder.Services.AddValidators();
28+
builder.Services.AddCorsDefaultPolicy(builder.Environment);
29+
builder.Services.AddFixedWindowRateLimiter();
2930

3031
if (builder.Environment.IsDevelopment())
3132
{
@@ -54,17 +55,16 @@
5455
* -------------------------------------------------------------------------- */
5556

5657
app.UseSerilogRequestLogging();
58+
app.UseHttpsRedirection();
59+
app.MapHealthChecks("/health");
60+
app.UseRateLimiter();
61+
app.MapControllers();
5762

5863
if (app.Environment.IsDevelopment())
5964
{
65+
app.UseCors();
6066
app.UseSwagger();
6167
app.UseSwaggerUI();
6268
}
6369

64-
app.UseHttpsRedirection();
65-
app.UseCors();
66-
app.UseRateLimiter();
67-
app.MapHealthChecks("/health");
68-
app.MapControllers();
69-
7070
await app.RunAsync();
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using System.Net;
2+
3+
namespace Dotnet.Samples.AspNetCore.WebApi.Utilities;
4+
5+
/// <summary>
6+
/// Utility class for HTTP context operations.
7+
/// </summary>
8+
public static class HttpContextUtilities
9+
{
10+
/// <summary>
11+
/// This method checks for the "X-Forwarded-For" and "X-Real-IP" headers,
12+
/// which are commonly used by proxies to forward the original client IP address.
13+
/// If these headers are not present or the IP address cannot be parsed,
14+
/// it falls back to the remote IP address from the connection.
15+
/// If no valid IP address can be determined, it returns "unknown".
16+
/// </summary>
17+
/// <param name="httpContext">The HTTP context.</param>
18+
/// <returns>The client IP address or "unknown" if not available.</returns>
19+
public static string ExtractIpAddress(HttpContext httpContext)
20+
{
21+
ArgumentNullException.ThrowIfNull(httpContext);
22+
23+
var headers = httpContext.Request.Headers;
24+
IPAddress? ipAddress;
25+
26+
if (headers.TryGetValue("X-Forwarded-For", out var xForwardedFor))
27+
{
28+
var clientIp = xForwardedFor
29+
.ToString()
30+
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
31+
.FirstOrDefault();
32+
33+
if (!string.IsNullOrWhiteSpace(clientIp) && IPAddress.TryParse(clientIp, out ipAddress))
34+
return ipAddress.ToString();
35+
}
36+
37+
if (
38+
headers.TryGetValue("X-Real-IP", out var xRealIp)
39+
&& IPAddress.TryParse(xRealIp.ToString(), out ipAddress)
40+
)
41+
{
42+
return ipAddress.ToString();
43+
}
44+
45+
return httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown-{Guid.NewGuid()}";
46+
}
47+
}

0 commit comments

Comments
 (0)