using System;
using System.Threading.RateLimiting;
using GB5Shared.DTO.Framework.Enum;
using GB5Shared.DTO.Framework.ResponseStandard;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using System.Text.Json;
namespace GB5Shared.RateLimit
{
///
/// Per-tenant sliding-window rate limiter for all GB5 SL projects.
///
/// Usage in Program.cs (add once, before app.UseAuthentication()):
///
/// builder.Services.AddGB5RateLimiting(builder.Configuration);
/// // ...
/// app.UseRateLimiter(); // must be before UseFastEndpoints()
///
///
/// appsettings.json configuration (all keys optional — defaults shown):
///
/// "RateLimit": {
/// "Enabled": true,
/// "PermitLimit": 1000,
/// "WindowSeconds": 60,
/// "SegmentsPerWindow": 6,
/// "QueueLimit": 0
/// }
///
///
/// On-prem deployments: set "Enabled": false to bypass rate limiting entirely.
/// SaaS deployments: tune PermitLimit per tier (e.g., 500 for standard, 2000 for enterprise).
///
/// Partitioning:
/// Requests are partitioned by the ClientId extracted from the Login JSON header.
/// Unauthenticated / malformed-header requests are grouped under "anonymous".
/// Dapr sidecar routes (/dapr/*) and health probes are never rate-limited.
///
/// Rejection:
/// Returns HTTP 429 with a fully-structured ResponseStandardDTO matching all
/// other GB5 API error envelopes — the client receives a clear, actionable message.
///
public static class GB5RateLimitExtensions
{
private static readonly JsonSerializerOptions _jsonOpts = new()
{
PropertyNamingPolicy = null,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
};
public static IServiceCollection AddGB5RateLimiting(
this IServiceCollection services,
IConfiguration configuration)
{
bool enabled = configuration.GetValue("RateLimit:Enabled", defaultValue: true);
int permitLimit = configuration.GetValue("RateLimit:PermitLimit", 1000);
int windowSeconds = configuration.GetValue("RateLimit:WindowSeconds", 60);
int segmentsPerWin = configuration.GetValue("RateLimit:SegmentsPerWindow", 6);
int queueLimit = configuration.GetValue("RateLimit:QueueLimit", 0);
if (!enabled)
return services; // no-op for on-prem / local dev
services.AddRateLimiter(opts =>
{
opts.AddPolicy("per-tenant", context =>
{
// Bypass: Dapr sidecar, health probes — never subject to rate limiting
var path = context.Request.Path.Value ?? string.Empty;
if (path.StartsWith("/dapr/", StringComparison.OrdinalIgnoreCase)
|| path.Equals("/healthz", StringComparison.OrdinalIgnoreCase)
|| path.Equals("/health", StringComparison.OrdinalIgnoreCase))
{
return RateLimitPartition.GetNoLimiter("system");
}
string partitionKey = ExtractClientId(context);
return RateLimitPartition.GetSlidingWindowLimiter(
partitionKey,
_ => new SlidingWindowRateLimiterOptions
{
PermitLimit = permitLimit,
Window = TimeSpan.FromSeconds(windowSeconds),
SegmentsPerWindow = segmentsPerWin,
QueueLimit = queueLimit,
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
AutoReplenishment = true
});
});
opts.RejectionStatusCode = 429;
opts.OnRejected = async (ctx, token) =>
{
ctx.HttpContext.Response.StatusCode = 429;
ctx.HttpContext.Response.ContentType = "application/json";
var body = new ResponseStandardDTO