diff --git a/fundamentals/middleware/rate-limit/WebRateLimitAuth/Models/MyRateLimitOptions.cs b/fundamentals/middleware/rate-limit/WebRateLimitAuth/Models/MyRateLimitOptions.cs
new file mode 100644
index 0000000..5d87e46
--- /dev/null
+++ b/fundamentals/middleware/rate-limit/WebRateLimitAuth/Models/MyRateLimitOptions.cs
@@ -0,0 +1,16 @@
+namespace WebRateLimitAuth.Models;
+
+public class MyRateLimitOptions
+{
+ public const string MyRateLimit = "MyRateLimit";
+
+ public int permitLimit { get; set; } = 100;
+ public int window { get; set; } = 10;
+ public int replenishmentPeriod { get; set; } = 2;
+ public int queueLimit { get; set; } = 100;
+ public int segmentsPerWindow { get; set; } = 8;
+ public int tokenLimit { get; set; } = 10;
+ public int tokenLimit2 { get; set; } = 20;
+ public int tokensPerPeriod { get; set; } = 4;
+ public bool autoReplenishment { get; set; } = false;
+}
diff --git a/fundamentals/middleware/rate-limit/WebRateLimitAuth/Pages/Shared/_Layout.cshtml b/fundamentals/middleware/rate-limit/WebRateLimitAuth/Pages/Shared/_Layout.cshtml
index 055a107..c476ede 100644
--- a/fundamentals/middleware/rate-limit/WebRateLimitAuth/Pages/Shared/_Layout.cshtml
+++ b/fundamentals/middleware/rate-limit/WebRateLimitAuth/Pages/Shared/_Layout.cshtml
@@ -31,6 +31,12 @@
MVC Privacy
+
+ /b
+
+
+ /c
+
diff --git a/fundamentals/middleware/rate-limit/WebRateLimitAuth/Program.cs b/fundamentals/middleware/rate-limit/WebRateLimitAuth/Program.cs
new file mode 100644
index 0000000..a91b1a8
--- /dev/null
+++ b/fundamentals/middleware/rate-limit/WebRateLimitAuth/Program.cs
@@ -0,0 +1,447 @@
+#define FIRST // FIRST ADMIN FIXED SLIDING CONCUR TOKEN FIXED2 JWT
+#if NEVER
+#elif FIXED
+//
+using Microsoft.AspNetCore.RateLimiting;
+using System.Threading.RateLimiting;
+
+var builder = WebApplication.CreateBuilder(args);
+var app = builder.Build();
+
+static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
+
+app.UseRateLimiter(new RateLimiterOptions()
+ .AddFixedWindowLimiter(policyName: "fixed",
+ new FixedWindowRateLimiterOptions(permitLimit: 4,
+ window: TimeSpan.FromSeconds(12),
+ queueProcessingOrder: QueueProcessingOrder.OldestFirst,
+ queueLimit: 2)));
+
+app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}"))
+ .RequireRateLimiting("fixed");
+
+app.Run();
+//
+#elif FIXED2
+//
+using System.Threading.RateLimiting;
+using Microsoft.AspNetCore.RateLimiting;
+using WebRateLimitAuth.Models;
+
+var builder = WebApplication.CreateBuilder(args);
+builder.Services.Configure(
+ builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
+var app = builder.Build();
+
+static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
+
+var myOptions = new MyRateLimitOptions();
+app.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
+var fixedPolicy = "fixed";
+
+app.UseRateLimiter(new RateLimiterOptions()
+ .AddFixedWindowLimiter(policyName: fixedPolicy,
+ new FixedWindowRateLimiterOptions(permitLimit: myOptions.permitLimit,
+ window: TimeSpan.FromSeconds(myOptions.window),
+ queueProcessingOrder: QueueProcessingOrder.OldestFirst,
+ queueLimit: myOptions.queueLimit)));
+
+app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}"))
+ .RequireRateLimiting(fixedPolicy);
+
+app.Run();
+//
+#elif SLIDING
+//
+using Microsoft.AspNetCore.RateLimiting;
+using System.Threading.RateLimiting;
+using WebRateLimitAuth.Models;
+
+var builder = WebApplication.CreateBuilder(args);
+var app = builder.Build();
+
+static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
+
+var myOptions = new MyRateLimitOptions();
+app.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
+var slidingPolicy = "sliding";
+
+app.UseRateLimiter(new RateLimiterOptions()
+ .AddSlidingWindowLimiter(policyName: slidingPolicy,
+ new SlidingWindowRateLimiterOptions(permitLimit: myOptions.permitLimit,
+ window: TimeSpan.FromSeconds(myOptions.window),
+ segmentsPerWindow: myOptions.segmentsPerWindow,
+ queueProcessingOrder: QueueProcessingOrder.OldestFirst,
+ queueLimit: myOptions.queueLimit)));
+
+app.MapGet("/", () => Results.Ok($"Hello {GetTicks()}"))
+ .RequireRateLimiting(slidingPolicy);
+
+app.Run();
+//
+#elif CONCUR
+// Quicktest 10 users, 9 seconds -> 982 requests, 900 errors
+//
+using Microsoft.AspNetCore.RateLimiting;
+using System.Threading.RateLimiting;
+using WebRateLimitAuth.Models;
+
+var builder = WebApplication.CreateBuilder(args);
+var app = builder.Build();
+
+static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
+
+var concurrencyPolicy = "Concurrency";
+var myOptions = new MyRateLimitOptions();
+app.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
+
+app.UseRateLimiter(new RateLimiterOptions()
+ .AddConcurrencyLimiter(policyName: concurrencyPolicy,
+ new ConcurrencyLimiterOptions(permitLimit: myOptions.permitLimit,
+ queueProcessingOrder: QueueProcessingOrder.OldestFirst,
+ queueLimit: myOptions.queueLimit)));
+
+app.MapGet("/", async () =>
+{
+ await Task.Delay(500);
+ return Results.Ok($"Concurrency Limiter {GetTicks()}");
+
+}).RequireRateLimiting(concurrencyPolicy);
+
+app.Run();
+//
+#elif TOKEN
+// Quicktest 20 users, 20 seconds -> 8965 requests 2,250 errors
+//
+using Microsoft.AspNetCore.RateLimiting;
+using System.Threading.RateLimiting;
+using WebRateLimitAuth.Models;
+
+var builder = WebApplication.CreateBuilder(args);
+var app = builder.Build();
+
+static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
+
+var tokenPolicy = "token";
+var myOptions = new MyRateLimitOptions();
+app.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
+
+app.UseRateLimiter(new RateLimiterOptions()
+ .AddTokenBucketLimiter(policyName: tokenPolicy,
+ new TokenBucketRateLimiterOptions(tokenLimit: myOptions.tokenLimit,
+ queueProcessingOrder: QueueProcessingOrder.OldestFirst,
+ queueLimit: myOptions.queueLimit,
+ replenishmentPeriod: TimeSpan.FromSeconds(2),
+ tokensPerPeriod: myOptions.tokensPerPeriod,
+ autoReplenishment: myOptions.autoReplenishment)));
+
+app.MapGet("/", () => Results.Ok($"Token Limiter {GetTicks()}"))
+ .RequireRateLimiting(tokenPolicy);
+
+app.Run();
+//
+#elif FIRST
+//
+using System.Globalization;
+using System.Net;
+using System.Threading.RateLimiting;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.RateLimiting;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using WebRateLimitAuth;
+using WebRateLimitAuth.Data;
+using WebRateLimitAuth.Models;
+
+var builder = WebApplication.CreateBuilder(args);
+
+var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ??
+ throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
+builder.Services.AddDbContext(options =>
+ options.UseSqlServer(connectionString));
+builder.Services.AddDatabaseDeveloperPageExceptionFilter();
+
+builder.Services.AddDefaultIdentity(options => options.SignIn.RequireConfirmedAccount = true)
+ .AddEntityFrameworkStores();
+
+builder.Services.Configure(
+ builder.Configuration.GetSection(MyRateLimitOptions.MyRateLimit));
+
+builder.Services.AddRazorPages();
+builder.Services.AddControllersWithViews();
+
+var app = builder.Build();
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseMigrationsEndPoint();
+}
+else
+{
+ app.UseExceptionHandler("/Error");
+ app.UseHsts();
+}
+
+app.UseHttpsRedirection();
+app.UseStaticFiles();
+
+app.UseRouting();
+
+//
+app.UseAuthentication();
+app.UseAuthorization();
+
+var userPolicyName = "user";
+var helloPolicy = "hello";
+var myOptions = new MyRateLimitOptions();
+var myConfigSection = app.Configuration.GetSection(MyRateLimitOptions.MyRateLimit);
+myConfigSection.Bind(myOptions);
+
+
+var options = new RateLimiterOptions()
+{
+ OnRejected = (context, cancellationToken) =>
+ {
+ if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
+ {
+ context.HttpContext.Response.Headers.RetryAfter =
+ ((int)retryAfter.TotalSeconds).ToString(NumberFormatInfo.InvariantInfo);
+ }
+
+ context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
+ context?.HttpContext?.RequestServices?.GetService()?
+ .CreateLogger("Microsoft.AspNetCore.RateLimitingMiddleware")
+ .LogWarning($"OnRejected: {GetUserEndPoint(context.HttpContext)}");
+
+ return new ValueTask();
+ }
+}
+ .AddPolicy(helloPolicy)
+ .AddPolicy(userPolicyName, context =>
+ {
+ if (context.User?.Identity?.IsAuthenticated is not true)
+ {
+ var username = "anonymous user";
+
+ return RateLimitPartition.CreateSlidingWindowLimiter(username,
+ key => new SlidingWindowRateLimiterOptions(
+ permitLimit: myOptions.permitLimit,
+ queueProcessingOrder: QueueProcessingOrder.OldestFirst,
+ queueLimit: myOptions.queueLimit,
+ window: TimeSpan.FromSeconds(myOptions.window),
+ segmentsPerWindow: myOptions.segmentsPerWindow
+ ));
+ }
+ else
+ {
+ return RateLimitPartition.CreateNoLimiter(string.Empty);
+ }
+ });
+
+options.GlobalLimiter = PartitionedRateLimiter.Create(context =>
+{
+ IPAddress? remoteIPaddress = context?.Connection?.RemoteIpAddress;
+
+ if (!IPAddress.IsLoopback(remoteIPaddress!))
+ {
+ return RateLimitPartition.CreateTokenBucketLimiter
+ (remoteIPaddress!, key =>
+ new TokenBucketRateLimiterOptions(tokenLimit: myOptions.tokenLimit2,
+ queueProcessingOrder: QueueProcessingOrder.OldestFirst,
+ queueLimit: myOptions.queueLimit,
+ replenishmentPeriod: TimeSpan.FromSeconds(myOptions.replenishmentPeriod),
+ tokensPerPeriod: myOptions.tokensPerPeriod,
+ autoReplenishment: myOptions.autoReplenishment));
+ }
+ else
+ {
+ return RateLimitPartition.CreateNoLimiter(IPAddress.Loopback);
+ }
+});
+
+app.UseRateLimiter(options);
+
+app.MapRazorPages().RequireRateLimiting(userPolicyName);
+app.MapDefaultControllerRoute();
+
+static string GetUserEndPoint(HttpContext context) =>
+ $"User {context.User?.Identity?.Name ?? "Anonymous"} endpoint: {context.Request.Path}" +
+ $" {context.Connection.RemoteIpAddress}";
+static string GetTicks() => (DateTime.Now.Ticks & 0x11111).ToString("00000");
+
+app.MapGet("/a", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}")
+ .RequireRateLimiting(userPolicyName);
+
+app.MapGet("/b", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}")
+ .RequireRateLimiting(helloPolicy);
+
+app.MapGet("/c", (HttpContext context) => $"{GetUserEndPoint(context)} {GetTicks()}");
+
+app.Run();
+//
+//
+#elif ADMIN
+//
+using System.Threading.RateLimiting;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.RateLimiting;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Primitives;
+using WebRateLimitAuth.Data;
+using WebRateLimitAuth.Models;
+
+var builder = WebApplication.CreateBuilder(args);
+
+var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
+ ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
+builder.Services.AddDbContext(options =>
+ options.UseSqlServer(connectionString));
+builder.Services.AddDatabaseDeveloperPageExceptionFilter();
+
+builder.Services.AddDefaultIdentity(options =>
+ options.SignIn.RequireConfirmedAccount = true)
+ .AddEntityFrameworkStores();
+
+builder.Services.AddRazorPages();
+builder.Services.AddControllersWithViews();
+
+var app = builder.Build();
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseMigrationsEndPoint();
+}
+else
+{
+ app.UseExceptionHandler("/Error");
+ app.UseHsts();
+}
+
+app.UseHttpsRedirection();
+app.UseStaticFiles();
+
+app.UseRouting();
+
+app.UseAuthentication();
+app.UseAuthorization();
+
+//
+var getPolicyName = "get";
+var postPolicyName = "post";
+var myOptions = new MyRateLimitOptions();
+app.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
+
+app.UseRateLimiter(new RateLimiterOptions()
+ .AddConcurrencyLimiter(policyName: getPolicyName,
+ new ConcurrencyLimiterOptions(permitLimit: myOptions.permitLimit,
+ queueProcessingOrder: QueueProcessingOrder.OldestFirst,
+ queueLimit: myOptions.queueLimit))
+ .AddPolicy(policyName: postPolicyName, partitioner: httpContext =>
+ {
+ string userName = httpContext?.User?.Identity?.Name ?? string.Empty;
+
+ if (!StringValues.IsNullOrEmpty(userName))
+ {
+ return RateLimitPartition.CreateTokenBucketLimiter(userName, key =>
+ new TokenBucketRateLimiterOptions(tokenLimit: myOptions.tokenLimit2,
+ queueProcessingOrder: QueueProcessingOrder.OldestFirst,
+ queueLimit: myOptions.queueLimit,
+ replenishmentPeriod: TimeSpan.FromSeconds(myOptions.replenishmentPeriod),
+ tokensPerPeriod: myOptions.tokensPerPeriod,
+ autoReplenishment: myOptions.autoReplenishment));
+ }
+ else
+ {
+ return RateLimitPartition.CreateTokenBucketLimiter("Anon", key =>
+ new TokenBucketRateLimiterOptions(tokenLimit: myOptions.tokenLimit,
+ queueProcessingOrder: QueueProcessingOrder.OldestFirst,
+ queueLimit: myOptions.queueLimit,
+ replenishmentPeriod: TimeSpan.FromSeconds(myOptions.replenishmentPeriod),
+ tokensPerPeriod: myOptions.tokensPerPeriod,
+ autoReplenishment: true));
+ }
+ }));
+//
+
+static string GetUserEndPointMethod(HttpContext context) =>
+ $"Hello {context.User?.Identity?.Name ?? "Anonymous"} " +
+ $"Endpoint:{context.Request.Path} Method: {context.Request.Method}";
+
+app.MapGet("/test", (HttpContext context) => $"{GetUserEndPointMethod(context)}")
+ .RequireRateLimiting(getPolicyName);
+
+app.MapRazorPages().RequireRateLimiting(getPolicyName)
+ .RequireRateLimiting(postPolicyName);
+
+app.MapDefaultControllerRoute();
+
+app.Run();
+//
+#elif JWT
+//
+using System.Threading.RateLimiting;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.RateLimiting;
+using Microsoft.Extensions.Primitives;
+using WebRateLimitAuth.Models;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddAuthorization();
+builder.Services.AddAuthentication("Bearer").AddJwtBearer();
+
+var app = builder.Build();
+
+app.UseAuthorization();
+
+var jwtPolicyName = "jwt";
+var myOptions = new MyRateLimitOptions();
+app.Configuration.GetSection(MyRateLimitOptions.MyRateLimit).Bind(myOptions);
+
+app.UseRateLimiter(new RateLimiterOptions()
+ .AddPolicy(policyName: jwtPolicyName, partitioner: httpContext =>
+ {
+ var accessToken = httpContext?.Features?.Get()?
+ .AuthenticateResult?.Properties?.GetTokenValue("access_token")?.ToString()
+ ?? string.Empty;
+ if (!StringValues.IsNullOrEmpty(accessToken))
+ {
+ return RateLimitPartition.CreateTokenBucketLimiter( accessToken, key =>
+ new TokenBucketRateLimiterOptions(tokenLimit: myOptions.tokenLimit2,
+ queueProcessingOrder: QueueProcessingOrder.OldestFirst,
+ queueLimit: myOptions.queueLimit,
+ replenishmentPeriod: TimeSpan.FromSeconds(myOptions.replenishmentPeriod),
+ tokensPerPeriod: myOptions.tokensPerPeriod,
+ autoReplenishment: myOptions.autoReplenishment));
+ }
+ else
+ {
+ return RateLimitPartition.CreateTokenBucketLimiter("Anon", key =>
+ new TokenBucketRateLimiterOptions(tokenLimit: myOptions.tokenLimit,
+ queueProcessingOrder: QueueProcessingOrder.OldestFirst,
+ queueLimit: myOptions.queueLimit,
+ replenishmentPeriod: TimeSpan.FromSeconds(myOptions.replenishmentPeriod),
+ tokensPerPeriod: myOptions.tokensPerPeriod,
+ autoReplenishment: true));
+ }
+ }));
+
+app.MapGet("/", () => "Hello, World!");
+
+app.MapGet("/jwt", (HttpContext context) => $"Hello {GetUserEndPointMethod(context)}")
+ .RequireRateLimiting(jwtPolicyName)
+ .RequireAuthorization();
+
+app.MapPost("/post", (HttpContext context) => $"Hello {GetUserEndPointMethod(context)}")
+ .RequireRateLimiting(jwtPolicyName)
+ .RequireAuthorization();
+
+app.Run();
+
+static string GetUserEndPointMethod(HttpContext context) =>
+ $"Hello {context.User?.Identity?.Name ?? "Anonymous"} " +
+ $"Endpoint:{context.Request.Path} Method: {context.Request.Method}";
+
+//
+#endif
diff --git a/fundamentals/middleware/rate-limit/WebRateLimitAuth/SampleRateLimiterPolicy.cs b/fundamentals/middleware/rate-limit/WebRateLimitAuth/SampleRateLimiterPolicy.cs
new file mode 100644
index 0000000..e027265
--- /dev/null
+++ b/fundamentals/middleware/rate-limit/WebRateLimitAuth/SampleRateLimiterPolicy.cs
@@ -0,0 +1,39 @@
+using System.Threading.RateLimiting;
+using Microsoft.AspNetCore.RateLimiting;
+using Microsoft.Extensions.Options;
+using WebRateLimitAuth.Models;
+
+namespace WebRateLimitAuth;
+
+public class SampleRateLimiterPolicy : IRateLimiterPolicy
+{
+ private Func? _onRejected;
+ private readonly MyRateLimitOptions _options;
+
+ public SampleRateLimiterPolicy(ILogger logger,
+ IOptions options)
+ {
+ _onRejected = (context, token) =>
+ {
+ context.HttpContext.Response.StatusCode = 429;
+ logger.LogWarning($"Request rejected by {nameof(SampleRateLimiterPolicy)}");
+ return ValueTask.CompletedTask;
+ };
+ _options = options.Value;
+ }
+
+ public Func?
+ OnRejected { get => _onRejected; }
+
+ public RateLimitPartition GetPartition(HttpContext httpContext)
+ {
+ return RateLimitPartition.CreateSlidingWindowLimiter(string.Empty,
+ key => new SlidingWindowRateLimiterOptions(
+ permitLimit: _options.permitLimit,
+ queueProcessingOrder: QueueProcessingOrder.OldestFirst,
+ queueLimit: _options.queueLimit,
+ window: TimeSpan.FromSeconds(_options.window),
+ segmentsPerWindow: _options.segmentsPerWindow));
+
+ }
+}
diff --git a/fundamentals/middleware/rate-limit/WebRateLimitAuth/WebRateLimitAuth.csproj b/fundamentals/middleware/rate-limit/WebRateLimitAuth/WebRateLimitAuth.csproj
index 0419be6..8b00507 100644
--- a/fundamentals/middleware/rate-limit/WebRateLimitAuth/WebRateLimitAuth.csproj
+++ b/fundamentals/middleware/rate-limit/WebRateLimitAuth/WebRateLimitAuth.csproj
@@ -8,12 +8,16 @@
-
-
-
-
-
-
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
diff --git a/fundamentals/middleware/rate-limit/WebRateLimitAuth/appsettings.Development.json b/fundamentals/middleware/rate-limit/WebRateLimitAuth/appsettings.Development.json
index 770d3e9..d3279b1 100644
--- a/fundamentals/middleware/rate-limit/WebRateLimitAuth/appsettings.Development.json
+++ b/fundamentals/middleware/rate-limit/WebRateLimitAuth/appsettings.Development.json
@@ -5,5 +5,29 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
+ },
+ "MyRateLimit": {
+ "permitLimit": "4",
+ "window": "3",
+ "replenishmentPeriod": "1",
+ "queueLimit": "2",
+ "segmentsPerWindow": "2",
+ "tokenLimit": 4,
+ "tokenLimit2": 6,
+ "tokensPerPeriod": 4,
+ "autoReplenishment": true
+ },
+ "Authentication": {
+ "Schemes": {
+ "Bearer": {
+ "ValidAudiences": [
+ "http://localhost:58245",
+ "https://localhost:44387",
+ "http://localhost:5266",
+ "https://localhost:7266"
+ ],
+ "ValidIssuer": "dotnet-user-jwts"
+ }
+ }
}
}
diff --git a/fundamentals/middleware/rate-limit/WebRateLimitAuth/appsettings.json b/fundamentals/middleware/rate-limit/WebRateLimitAuth/appsettings.json
index 768ff94..4ff58e9 100644
--- a/fundamentals/middleware/rate-limit/WebRateLimitAuth/appsettings.json
+++ b/fundamentals/middleware/rate-limit/WebRateLimitAuth/appsettings.json
@@ -8,5 +8,16 @@
"Microsoft.AspNetCore": "Warning"
}
},
- "AllowedHosts": "*"
+ "AllowedHosts": "*",
+ "MyRateLimit": {
+ "permitLimit": "10",
+ "window": "2",
+ "replenishmentPeriod": "1",
+ "queueLimit": "10",
+ "segmentsPerWindow": "4",
+ "tokenLimit": 8,
+ "tokenLimit2": 12,
+ "tokensPerPeriod": 4,
+ "autoReplenishment": true
+ }
}