Elf.AccessRateLimit.Samples
1.0.15
dotnet add package Elf.AccessRateLimit.Samples --version 1.0.15
NuGet\Install-Package Elf.AccessRateLimit.Samples -Version 1.0.15
<PackageReference Include="Elf.AccessRateLimit.Samples" Version="1.0.15" />
<PackageVersion Include="Elf.AccessRateLimit.Samples" Version="1.0.15" />
<PackageReference Include="Elf.AccessRateLimit.Samples" />
paket add Elf.AccessRateLimit.Samples --version 1.0.15
#r "nuget: Elf.AccessRateLimit.Samples, 1.0.15"
#:package Elf.AccessRateLimit.Samples@1.0.15
#addin nuget:?package=Elf.AccessRateLimit.Samples&version=1.0.15
#tool nuget:?package=Elf.AccessRateLimit.Samples&version=1.0.15
Elf.AccessRateLimit
Distributed, Redis-backed access rate limiting for expensive endpoints in ASP.NET Core (.NET 8).
Overview
Elf.AccessRateLimit protects heavy endpoints (downloads, exports, reports) from abuse by enforcing rate limits across multiple app instances using Redis as the source of truth. It uses atomic Lua scripts for concurrency safety and supports per-endpoint policies, escalation penalties, and simple extension methods.
Key features:
- Distributed enforcement via StackExchange.Redis
- Per-endpoint policy selection (attribute or endpoint mapping)
- Escalating blocks for repeated violations
- Token-bucket algorithm with atomic Redis Lua script
- Optional headers and custom response body
- Structured logging and optional metrics hooks
Requirements
- .NET 8
- StackExchange.Redis connection (IConnectionMultiplexer)
Install (NuGet)
NuGet package: https://www.nuget.org/packages/Elf.AccessRateLimit/
dotnet add package Elf.AccessRateLimit
Or add a reference in your project file:
<ItemGroup>
<PackageReference Include="Elf.AccessRateLimit" Version="1.0.1" />
</ItemGroup>
Quick start (code)
using StackExchange.Redis;
using Elf.AccessRateLimit;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IConnectionMultiplexer>(
_ => ConnectionMultiplexer.Connect(builder.Configuration.GetConnectionString("Redis")!));
builder.Services.AddElfAccessRateLimit(options =>
{
options.DefaultPolicyName = "download";
options.AddPolicy("download", p =>
{
p.WithLimit(10, TimeSpan.FromMinutes(1));
p.WithKeyResolverSpecs("ip", "user");
p.WithPenalty(penalty =>
{
penalty.ViolationWindow = TimeSpan.FromMinutes(10);
penalty.Penalties = new List<TimeSpan>
{
TimeSpan.FromSeconds(10),
TimeSpan.FromMinutes(1),
TimeSpan.FromMinutes(5),
TimeSpan.FromMinutes(30)
};
});
});
});
var app = builder.Build();
app.UseElfAccessRateLimit();
app.MapGet("/download/{id}", () => Results.Ok())
.RequireAccessRateLimit("download");
app.Run();
Quick start (appsettings.json)
{
"Elf": {
"AccessRateLimit": {
"DefaultPolicyName": "download",
"RedisKeyPrefix": "elf:accessrl",
"AddRateLimitHeaders": true,
"FailOpen": true,
"Logging": {
"Detail": "Detailed"
},
"Policies": {
"download": {
"LimitPerMinute": 10,
"KeyStrategy": "ip,user",
"Penalty": {
"ViolationWindow": "00:10:00",
"Penalties": [ "00:00:10", "00:01:00", "00:05:00", "00:30:00" ]
}
}
}
}
}
}
builder.Services.AddElfAccessRateLimit(builder.Configuration);
By default, AddElfAccessRateLimit(IConfiguration) binds to Elf:AccessRateLimit. To load
settings from a different section, pass the section key explicitly:
builder.Services.AddElfAccessRateLimit("RateLimit");
{
"RateLimit": {
"DefaultPolicyName": "download"
}
}
API usage and integration
Middleware
Register services:
builder.Services.AddElfAccessRateLimit(options => { /* policies */ });
Add the middleware:
app.UseElfAccessRateLimit();
Place the middleware after routing (so endpoint metadata is available) and after auth if limits depend on claims.
Endpoint mapping
app.MapGet("/reports/{id}", () => Results.Ok())
.RequireAccessRateLimit("download");
You can override the scope or cost:
app.MapGet("/reports/{id}", () => Results.Ok())
.RequireAccessRateLimit("download", scope: "reports", cost: 5);
Attribute usage (MVC/Web API)
[AccessRateLimit("download", Scope = "reports", Cost = 5)]
[HttpGet("/reports/{id}")]
public IActionResult GetReport(string id) => Ok();
Policies (code)
options.AddPolicy("export", p =>
{
p.WithLimit(5, TimeSpan.FromMinutes(1));
p.WithSharedBucket("exports");
p.ForAuthenticated(10);
p.ForAnonymous(3);
p.WithCost(2);
p.WithKeyResolverSpecs("ip", "header:X-Api-Key");
});
Policies (config)
"Policies": {
"export": {
"Limit": 5,
"Window": "00:01:00",
"SharedBucket": "exports",
"AuthenticatedLimit": 10,
"AnonymousLimit": 3,
"Cost": 2,
"KeyStrategy": "ip,header:X-Api-Key"
}
}
Key strategies
Built-in specs:
ip(prefersX-Forwarded-For/X-Real-IP, falls back toRemoteIpAddress)useroruser-id(ClaimTypes.NameIdentifier)subapi-key(X-Api-Key header)client-id(X-Client-Id header)claim:<type>header:<name>
Example:
p.WithKeyResolverSpecs("ip", "claim:tenant_id", "header:X-Api-Key");
Custom resolver:
public sealed class CustomResolver : IRateLimitKeyResolver
{
public ValueTask<string?> ResolveAsync(HttpContext context, CancellationToken token = default)
=> new ValueTask<string?>(context.Request.Headers["X-Custom"].ToString());
}
p.WithKeyResolver(new CustomResolver());
Escalating penalties
Use Penalty to increase block duration for repeated violations:
p.WithPenalty(penalty =>
{
penalty.ViolationWindow = TimeSpan.FromMinutes(10);
penalty.Penalties = new List<TimeSpan>
{
TimeSpan.FromSeconds(10),
TimeSpan.FromMinutes(1),
TimeSpan.FromMinutes(5),
TimeSpan.FromMinutes(30)
};
});
Headers and responses
When limited, the middleware returns HTTP 429 with:
Retry-AfterX-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-Reset
Customize the response:
options.Response.ContentType = "application/json";
options.Response.Body = "{\"error\":\"rate_limited\"}";
options.Response.OnRejected = (ctx, decision) =>
{
ctx.Response.ContentType = "application/json";
return ctx.Response.WriteAsync("{\"error\":\"rate_limited\"}");
};
Metrics hooks
Implement and register a metrics handler:
public sealed class RateLimitMetrics : IAccessRateLimitMetrics
{
public void OnAllowed(AccessRateLimitDecision decision) { }
public void OnLimited(AccessRateLimitDecision decision) { }
public void OnBlocked(AccessRateLimitDecision decision) { }
}
builder.Services.AddSingleton<IAccessRateLimitMetrics, RateLimitMetrics>();
Logging
Choose between normal or detailed logs (information level or higher):
options.Logging.Detail = AccessRateLimitLogDetail.Detailed;
Normal logs emit limited/blocked decisions. Detailed logs also include allowed decisions and extra fields.
Whitelisting
options.ExemptWhen = ctx =>
ctx.User.IsInRole("Admin") ||
ctx.Connection.RemoteIpAddress?.ToString() == "10.0.0.1";
Notes
- Redis is the source of truth; optional local caching is not required.
FailOpen = trueallows requests if Redis is unavailable (logged as error).- Use
SharedBucketor per-requestscopeto share a limit across multiple endpoints. Costlets heavy endpoints consume more tokens per request.
Samples
The console app Elf.AccessRateLimit.Samples spins up a local in-process WebApplication and exercises the limiter.
dotnet run --project Elf.AccessRateLimit.Samples -- --redis localhost:6379 --sample all
Samples available: all, basic, keys, escalation.
Folder layout
- Configuration: options and validation
- Policies: policy models and provider
- Keys: key resolvers and key utilities
- Store: Redis Lua store
- Middleware: rate limit pipeline
- Extensions: service/middleware/endpoint registration
- Metadata: endpoint metadata + attribute
- Metrics: metrics contracts
- Models: decision model
Design
The limiter runs in middleware, resolves a policy per endpoint, builds a stable caller key, and evaluates a token bucket in Redis with an atomic Lua script. The Redis result drives allow/deny, headers, logging, and escalation penalties.
Key points that enforce the limit:
- Policy resolution chooses the endpoint policy or the configured default.
- Key resolution uses policy resolvers (IP, header, claim, composite) and hashes the result before Redis.
- Bucket scope defaults to the route pattern, but can be overridden to share limits.
- Redis Lua script checks blocks, refills tokens, applies cost, and increments violations safely.
- Escalation penalties set a block key with growing TTLs for repeated violations.
- Responses return 429 with Retry-After and optional rate limit headers.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Elf.AccessRateLimit (>= 1.0.15)
- StackExchange.Redis (>= 2.7.33)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.