Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nimanikoo/Dotnet-RateLimiter/llms.txt

Use this file to discover all available pages before exploring further.

RedisRateLimitingMiddleware sits in the ASP.NET Core request pipeline and acts as the bridge between the routing layer and RedisRateLimiter. For every incoming request it checks whether the matched endpoint is decorated with [RedisRateLimit], identifies the calling client (authenticated user or anonymous guest), constructs a composite Redis key, and either lets the request proceed or short-circuits it with an HTTP 429 Too Many Requests response before any controller code runs.

Namespace

namespace Dotnet_RateLimiter.Middlewares;

Constructor

public RedisRateLimitingMiddleware(RequestDelegate next, RedisRateLimiter rateLimiter)
Both dependencies are injected automatically by the ASP.NET Core middleware factory when app.UseMiddleware<RedisRateLimitingMiddleware>() is called.
next
RequestDelegate
required
The next middleware in the pipeline. Invoked when the current request is within the rate limit quota and should continue processing.
rateLimiter
RedisRateLimiter
required
The singleton RedisRateLimiter service that executes the atomic Lua counter check against Redis. Injected from the DI container.

Method: InvokeAsync

public async Task InvokeAsync(HttpContext context)
The single entry point called by the ASP.NET Core pipeline for every HTTP request. The method follows a strict, linear decision flow:

Execution Flow

1

Resolve the current endpoint

Calls context.GetEndpoint(). If no endpoint is matched (e.g. a 404 path), the middleware immediately forwards to _next(context) without any rate-limit check.
var endpoint = context.GetEndpoint();
if (endpoint == null)
{
    await _next(context);
    return;
}
2

Check for RedisRateLimitAttribute

Looks up RedisRateLimitAttribute in the endpoint’s metadata collection. If the attribute is absent, the endpoint is not rate-limited and the request passes through.
var rateLimitAttr = endpoint.Metadata.GetMetadata<RedisRateLimitAttribute>();
if (rateLimitAttr == null)
{
    await _next(context);
    return;
}
3

Resolve client identity

Determines whether the caller is authenticated or anonymous and sets both identityKey and userType accordingly:
  • Authenticated — reads ClaimTypes.NameIdentifier from the user’s claims; falls back to Identity.Name, then to the string "authenticated_unknown". Sets userType = "user".
  • Anonymous — reads context.Connection.RemoteIpAddress; falls back to "unknown". Sets userType = "guest".
string identityKey;
string userType;

if (context.User.Identity?.IsAuthenticated == true)
{
    var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
                 ?? context.User.Identity.Name
                 ?? "authenticated_unknown";

    identityKey = userId;
    userType = "user";
}
else
{
    var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
    identityKey = ip;
    userType = "guest";
}
4

Build the rate-limit key

Combines userType, identityKey, and the request path into a single composite string. RedisRateLimiter will further prefix this with rate_limit: before writing it to Redis.
var limitKey = $"{userType}:{identityKey}:{context.Request.Path}";
// e.g. "user:42:/api/weatherforecast"
// e.g. "guest:192.168.1.1:/api/weatherforecast"
5

Evaluate the rate limit in Redis

Delegates to RedisRateLimiter.IsAllowedAsync, passing the composite key, the MaxRequests and the WindowSeconds (converted to a TimeSpan) from the attribute.
var isAllowed = await _rateLimiter.IsAllowedAsync(
    limitKey,
    rateLimitAttr.MaxRequests,
    TimeSpan.FromSeconds(rateLimitAttr.WindowSeconds)
);
6

Allow or block the request

If isAllowed is true, the request is forwarded to the next middleware. If false, the response is short-circuited with HTTP 429 (see below).
if (!isAllowed)
{
    // → 429 branch (see below)
    return;
}

await _next(context);

Full InvokeAsync Implementation

public async Task InvokeAsync(HttpContext context)
{
    var endpoint = context.GetEndpoint();
    if (endpoint == null)
    {
        await _next(context);
        return;
    }

    var rateLimitAttr = endpoint.Metadata.GetMetadata<RedisRateLimitAttribute>();
    if (rateLimitAttr == null)
    {
        await _next(context);
        return;
    }

    string identityKey;
    string userType;

    if (context.User.Identity?.IsAuthenticated == true)
    {
        var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
                     ?? context.User.Identity.Name
                     ?? "authenticated_unknown";

        identityKey = userId;
        userType = "user";
    }
    else
    {
        var ip = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
        identityKey = ip;
        userType = "guest";
    }

    var limitKey = $"{userType}:{identityKey}:{context.Request.Path}";

    var isAllowed = await _rateLimiter.IsAllowedAsync(
        limitKey,
        rateLimitAttr.MaxRequests,
        TimeSpan.FromSeconds(rateLimitAttr.WindowSeconds)
    );

    if (!isAllowed)
    {
        context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
        context.Response.Headers.RetryAfter = rateLimitAttr.WindowSeconds.ToString();

        var message = userType == "user"
            ? "Dear user, you've reached your limit. Take a breath!"
            : "Guest limit reached. Please sign up for more quota!";

        await context.Response.WriteAsync(message);
        return;
    }

    await _next(context);
}

HTTP 429 Response Details

When a request exceeds the configured limit the middleware writes the following response and does not call _next, so no controller code executes:
PropertyValue
Status code429 Too Many Requests
Retry-After header{WindowSeconds} (the raw integer from the attribute)
Body — authenticated userDear user, you've reached your limit. Take a breath!
Body — anonymous guestGuest limit reached. Please sign up for more quota!

Registration

Register the middleware in Program.cs after UseAuthorization so that authentication claims are already populated when InvokeAsync reads context.User:
app.UseAuthorization();

app.UseMiddleware<RedisRateLimitingMiddleware>();

app.MapControllers();
UseMiddleware<RedisRateLimitingMiddleware>() must be placed after app.UseRouting() / endpoint routing is set up (which MapControllers implies) and after app.UseAuthorization(). Placing it before authentication or routing will cause the endpoint and user identity lookups to return null for every request.

Build docs developers (and LLMs) love