From 0d431331731052f4085df332daadb76af0655404 Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 6 Mar 2026 16:27:28 +0800 Subject: [PATCH] feat: add tenant-wide limit, ban policy, and pre-check functionality to RateLimitDemo --- .../Controllers/RateLimitDemoController.cs | 64 +++++++++- .../MyProjectNameWebModule.cs | 25 +++- .../Pages/RateLimitDemo/Index.cshtml | 112 +++++++++++++++++- 3 files changed, 197 insertions(+), 4 deletions(-) diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Controllers/RateLimitDemoController.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Controllers/RateLimitDemoController.cs index 271b5b6af7..60f7b8c01a 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Controllers/RateLimitDemoController.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Controllers/RateLimitDemoController.cs @@ -198,6 +198,52 @@ public class RateLimitDemoController : AbpController return Ok(new { success = true, message = $"Tenant-isolated operation completed for key: {input.Key}" }); } + /// + /// Demo 11: Authenticated - PartitionByCurrentTenant (all users in a tenant share one counter) + /// + [HttpPost("tenant-wide")] + [Authorize] + public async Task TenantWide() + { + await _checker.CheckAsync("Demo_TenantWide"); + + return Ok(new { success = true, message = "Tenant-wide operation completed" }); + } + + /// + /// Demo 12: Ban policy (maxCount: 0) - always blocked + /// + [HttpPost("ban-policy")] + [AllowAnonymous] + public async Task BanPolicy([FromBody] BanPolicyInput input) + { + await _checker.CheckAsync("Demo_BanPolicy", new OperationRateLimitingContext + { + Parameter = input.Key + }); + + return Ok(new { success = true, message = $"Operation completed for key: {input.Key}" }); + } + + /// + /// Demo 13: IsAllowedAsync - read-only pre-check (does NOT increment counter) + /// + [HttpPost("pre-check")] + [AllowAnonymous] + public async Task PreCheck([FromBody] PreCheckInput input) + { + var context = new OperationRateLimitingContext { Parameter = input.Key }; + + if (input.ReadOnly) + { + var isAllowed = await _checker.IsAllowedAsync("Demo_PreCheck", context); + return Ok(new { success = true, message = $"IsAllowedAsync (read-only): {isAllowed}. Counter was NOT incremented." }); + } + + await _checker.CheckAsync("Demo_PreCheck", context); + return Ok(new { success = true, message = "CheckAsync: Counter incremented." }); + } + /// /// Get status without consuming quota /// @@ -243,7 +289,9 @@ public class RateLimitDemoController : AbpController [FromQuery] string? compositeParamKey = null, [FromQuery] string? compositeTripleKey = null, [FromQuery] string? customMultiKeyResourceId = null, - [FromQuery] string? tenantIsolatedKey = null) + [FromQuery] string? tenantIsolatedKey = null, + [FromQuery] string? banPolicyKey = null, + [FromQuery] string? preCheckKey = null) { var policies = new[] { @@ -259,6 +307,9 @@ public class RateLimitDemoController : AbpController ("Demo_LongDays", (string?)null), ("Demo_CustomMultiKey", customMultiKeyResourceId), ("Demo_TenantIsolated", tenantIsolatedKey), + ("Demo_TenantWide", (string?)null), + ("Demo_BanPolicy", banPolicyKey), + ("Demo_PreCheck", preCheckKey), }; foreach (var (policyName, parameter) in policies) @@ -317,3 +368,14 @@ public class DemoTenantIsolatedInput { public string Key { get; set; } = default!; } + +public class BanPolicyInput +{ + public string Key { get; set; } = default!; +} + +public class PreCheckInput +{ + public string Key { get; set; } = default!; + public bool ReadOnly { get; set; } +} diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs index ea5ebf3e10..17becbed7e 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Extensions.DependencyInjection; using Microsoft.AspNetCore.Hosting; @@ -226,7 +227,7 @@ public class MyProjectNameWebModule : AbpModule .PartitionBy(ctx => { var userId = ctx.GetRequiredService().Id?.ToString() ?? "anonymous"; - return $"{ctx.Parameter}:{userId}"; + return Task.FromResult($"{ctx.Parameter}:{userId}"); }); }); @@ -238,6 +239,28 @@ public class MyProjectNameWebModule : AbpModule .WithMultiTenancy() .PartitionByParameter(); }); + + // Demo 11: Authenticated - PartitionByCurrentTenant + // All users within the same tenant share one counter. + options.AddPolicy("Demo_TenantWide", policy => + { + policy.WithFixedWindow(TimeSpan.FromSeconds(20), maxCount: 3) + .PartitionByCurrentTenant(); + }); + + // Demo 12: Ban policy (maxCount: 0) - blocks all requests for the duration + options.AddPolicy("Demo_BanPolicy", policy => + { + policy.WithFixedWindow(TimeSpan.FromSeconds(30), maxCount: 0) + .PartitionByParameter(); + }); + + // Demo 13: IsAllowedAsync - read-only check without incrementing counter + options.AddPolicy("Demo_PreCheck", policy => + { + policy.WithFixedWindow(TimeSpan.FromSeconds(20), maxCount: 3) + .PartitionByParameter(); + }); }); } diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Pages/RateLimitDemo/Index.cshtml b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Pages/RateLimitDemo/Index.cshtml index c86eff0f4f..ee82fe4c2a 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Pages/RateLimitDemo/Index.cshtml +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/Pages/RateLimitDemo/Index.cshtml @@ -405,6 +405,90 @@ + +
+
+
+ 11. Tenant-Wide Limit + Auth +
+
+
+ Partition: PartitionByCurrentTenant
+ Rule: 3 requests per tenant within 20 seconds
+ Note: All users within the same tenant share a single counter. Different tenants have independent counters. Uses "host" as the key on the host side. +
+
+ + +
+
+ +
+
+
+
+
+ + +
+
+
+ 12. Ban Policy (maxCount: 0) + Public +
+
+
+ Partition: PartitionByParameter
+ Rule: maxCount: 0 within 30 seconds
+ Note: Setting maxCount to 0 creates a ban policy — every request is blocked for the entire window duration. Useful for temporarily banning a user or resource. +
+
+ + +
+
+ + +
+
+ +
+
+
+
+
+ + +
+
+
+ 13. IsAllowedAsync (Pre-Check) + Public +
+
+
+ Partition: PartitionByParameter
+ Rule: 3 requests per key within 20 seconds
+ Note: Demonstrates the difference between IsAllowedAsync (read-only, does not increment counter) and CheckAsync (increments counter). Click "Pre-Check" repeatedly — it never exhausts the quota. Click "Consume" to actually increment.
+ How to test: Click "Pre-Check" many times (always OK) → Click "Consume" 3 times → Click "Pre-Check" again (now returns false). +
+
+ + +
+
+ + + +
+
+ +
+
+
+
+
@@ -598,6 +682,26 @@ { key: key }, 'tenantIsolatedResult', 'tenantIsolatedCountdown'); } + // Demo 11 + function demoTenantWide() { + apiCall('/api/rate-limit-demo/tenant-wide', 'POST', + null, 'tenantWideResult', 'tenantWideCountdown'); + } + + // Demo 12 + function demoBanPolicy() { + var key = document.getElementById('banPolicyKey').value; + apiCall('/api/rate-limit-demo/ban-policy', 'POST', + { key: key }, 'banPolicyResult', 'banPolicyCountdown'); + } + + // Demo 13 + function demoPreCheck(readOnly) { + var key = document.getElementById('preCheckKey').value; + apiCall('/api/rate-limit-demo/pre-check', 'POST', + { key: key, readOnly: readOnly }, 'preCheckResult', 'preCheckCountdown'); + } + // Reset single policy function demoReset(policyName, parameter, resultId) { var url = '/api/rate-limit-demo/reset/' + policyName; @@ -626,6 +730,8 @@ var compositeTripleKey = document.getElementById('compositeTripleKey').value; var customMultiKeyResourceId = document.getElementById('customMultiKeyResourceId').value; var tenantIsolatedKey = document.getElementById('tenantIsolatedKey').value; + var banPolicyKey = document.getElementById('banPolicyKey').value; + var preCheckKey = document.getElementById('preCheckKey').value; var url = '/api/rate-limit-demo/reset-all?' + 'smsPhone=' + encodeURIComponent(smsPhone) + '&feedbackEmail=' + encodeURIComponent(feedbackEmail) + @@ -633,7 +739,9 @@ '&compositeParamKey=' + encodeURIComponent(compositeParamKey) + '&compositeTripleKey=' + encodeURIComponent(compositeTripleKey) + '&customMultiKeyResourceId=' + encodeURIComponent(customMultiKeyResourceId) + - '&tenantIsolatedKey=' + encodeURIComponent(tenantIsolatedKey); + '&tenantIsolatedKey=' + encodeURIComponent(tenantIsolatedKey) + + '&banPolicyKey=' + encodeURIComponent(banPolicyKey) + + '&preCheckKey=' + encodeURIComponent(preCheckKey); $.ajax({ url: url, type: 'POST', dataType: 'json' }) .done(function() { @@ -641,7 +749,7 @@ var resultAreas = ['smsResult', 'loginResult', 'apiKeyResult', 'emailResult', 'compositeUserIpResult', 'compositeParamUserResult', 'compositeTripleResult', 'feedbackResult', 'longHoursResult', 'longDaysResult', 'customMultiKeyResult', - 'tenantIsolatedResult']; + 'tenantIsolatedResult', 'tenantWideResult', 'banPolicyResult', 'preCheckResult']; resultAreas.forEach(function(id) { var el = document.getElementById(id); if (el) el.innerHTML = '
[' + now() + '] All counters reset
';