using System.Threading.RateLimiting; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Shared.Models.Settings; namespace StartupHelpers; public static class RateLimitingExtensions { public static void AddPublicApiRateLimiting( this IServiceCollection services, IConfiguration configuration, string sectionName = "RateLimiting") { var settings = configuration.GetSection(sectionName).Get() ?? new RateLimitingSettings(); services.AddRateLimiter(options => { var global = settings.Global ?? new RateLimitPolicySettings(); options.GlobalLimiter = PartitionedRateLimiter.Create(httpContext => { var ip = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; return RateLimitPartition.GetFixedWindowLimiter( partitionKey: ip, factory: _ => new FixedWindowRateLimiterOptions { PermitLimit = global.PermitLimit, Window = global.Window, QueueLimit = global.QueueLimit, AutoReplenishment = global.AutoReplenishment }); }); foreach (var entry in settings.Policies) { var policyName = entry.Key; var policy = entry.Value ?? new RateLimitPolicySettings(); options.AddPolicy(policyName, httpContext => { var ip = httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; return RateLimitPartition.GetFixedWindowLimiter( partitionKey: ip, factory: _ => new FixedWindowRateLimiterOptions { PermitLimit = policy.PermitLimit, Window = policy.Window, QueueLimit = policy.QueueLimit, AutoReplenishment = policy.AutoReplenishment }); }); } options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.OnRejected = async (context, ct) => { var logger = context.HttpContext.RequestServices .GetRequiredService() .CreateLogger("RateLimiting"); var ip = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown"; var endpoint = context.HttpContext.Request.Path; logger.LogWarning("Rate limit exceeded for {Endpoint} from IP {IP}", endpoint, ip); context.HttpContext.Response.ContentType = "application/json"; await context.HttpContext.Response.WriteAsync("""{"error":"Too many requests. Try again later."}""", ct); }; }); } }