From c6170ba804892a5e5aa037890404de2be6c1cc94 Mon Sep 17 00:00:00 2001 From: maliming Date: Mon, 16 Mar 2026 18:15:21 +0800 Subject: [PATCH] Update operation rate limiting docs for named partition resolver API --- .../POST.md | 2 +- docs/en/modules/operation-rate-limiting.md | 76 ++++++++++++++++++- 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/docs/en/Community-Articles/2026-03-10-Operation-Rate-Limiting-in-ABP-Framework/POST.md b/docs/en/Community-Articles/2026-03-10-Operation-Rate-Limiting-in-ABP-Framework/POST.md index d87caeb085..d3a851247f 100644 --- a/docs/en/Community-Articles/2026-03-10-Operation-Rate-Limiting-in-ABP-Framework/POST.md +++ b/docs/en/Community-Articles/2026-03-10-Operation-Rate-Limiting-in-ABP-Framework/POST.md @@ -149,7 +149,7 @@ Getting this wrong can make your rate limiting completely ineffective. Using `Pa - **`PartitionByCurrentUser`** — uses the authenticated user's ID, with no value to pass. Perfect for "each user gets N per day" scenarios where user identity is all you need. - **`PartitionByClientIp`** — uses the client's IP address. Don't rely on this alone — it's too easy to rotate. Use it as a secondary layer alongside another partition type, as in the login example below. - **`PartitionByEmail`** and **`PartitionByPhoneNumber`** — designed for pre-authentication flows where the user isn't logged in yet. They prefer the `Parameter` value you explicitly pass, and fall back to the current user's email or phone number if none is provided. -- **`PartitionBy`** — a custom async delegate that can produce any partition key you need. When the built-in options don't fit, you're free to implement whatever logic makes sense: look up a resource's owner in the database, derive a key from the user's subscription tier, partition by tenant — anything that returns a string. +- **`PartitionBy`** — a named custom resolver that can produce any partition key you need. Register a resolver function under a unique name via `options.AddPartitionKeyResolver("MyResolver", ctx => ...)`, then reference it by name: `.PartitionBy("MyResolver")`. You can also register and reference in one step: `.PartitionBy("MyResolver", ctx => ...)`. When the built-in options don't fit, you're free to implement whatever logic makes sense: look up a resource's owner in the database, derive a key from the user's subscription tier, partition by tenant — anything that returns a string. Because the resolver is stored by name (not as an anonymous delegate), it can be serialized and managed from a UI or database. > The rule of thumb: partition by the identity of whoever's behavior you're trying to limit. diff --git a/docs/en/modules/operation-rate-limiting.md b/docs/en/modules/operation-rate-limiting.md index b8bf5f8196..f285793695 100644 --- a/docs/en/modules/operation-rate-limiting.md +++ b/docs/en/modules/operation-rate-limiting.md @@ -374,14 +374,59 @@ Works the same way as `PartitionByEmail`: resolves from `context.Parameter` firs ### Custom Partition (PartitionBy) -You can provide a custom async function to generate the partition key. The async signature allows you to perform database queries or other I/O operations: +You can register a named custom resolver to generate the partition key. The resolver is an async function, so you can perform database queries or other I/O operations. Because the resolver is stored by name (not as an anonymous delegate), it can be serialized and managed from a UI or database. + +**Step 1 — Register the resolver by name:** + +````csharp +Configure(options => +{ + options.AddPartitionKeyResolver("ByDevice", ctx => + Task.FromResult($"{ctx.Parameter}:{ctx.ExtraProperties["DeviceId"]}")); +}); +```` + +**Step 2 — Reference it in a policy:** + +````csharp +policy.WithFixedWindow(TimeSpan.FromHours(1), maxCount: 100) + .PartitionBy("ByDevice"); +```` + +You can also register and reference in one step (inline): ````csharp policy.WithFixedWindow(TimeSpan.FromHours(1), maxCount: 100) - .PartitionBy(ctx => Task.FromResult( - $"{ctx.Parameter}:{ctx.ExtraProperties["DeviceId"]}")); + .PartitionBy("ByDevice", ctx => + Task.FromResult($"{ctx.Parameter}:{ctx.ExtraProperties["DeviceId"]}")); +```` + +> If you call `PartitionBy("name")` with a resolver name that hasn't been registered, an exception is thrown at configuration time (not at runtime), so typos are caught early. + +To replace an existing resolver (e.g., in a downstream module), use `ReplacePartitionKeyResolver`: + +````csharp +options.ReplacePartitionKeyResolver("ByDevice", ctx => + Task.FromResult($"v2:{ctx.Parameter}:{ctx.ExtraProperties["DeviceId"]}")); ```` +### Named Rules (WithName) + +By default, a rule's store key is derived from its `Duration`, `MaxCount`, and `PartitionType`. This means that if you change a rule's parameters (e.g., increase `maxCount` from 5 to 10), the counter resets because the key changes. + +To keep a stable key across parameter changes, give the rule a name: + +````csharp +policy.AddRule(rule => rule + .WithName("HourlyLimit") + .WithFixedWindow(TimeSpan.FromHours(1), maxCount: 100) + .PartitionByCurrentUser()); +```` + +When a name is set, it is used as the store key instead of the content-based descriptor. This is particularly useful when rules are managed from a database or UI — changing the `maxCount` or `duration` will not reset existing counters. + +> Rule names must be unique within a policy. Duplicate names cause an exception at build time. + ## Multi-Tenancy By default, partition keys do not include tenant information — for partition types like `PartitionByParameter`, `PartitionByCurrentUser`, `PartitionByClientIp`, etc., counters are shared across tenants unless you call `WithMultiTenancy()`. Note that `PartitionByCurrentTenant()` is inherently per-tenant since the partition key is the tenant ID itself, and `PartitionByClientIp()` is typically kept global since the same IP should share a counter regardless of tenant. @@ -664,6 +709,31 @@ Replace `IOperationRateLimitingFormatter` to customize how time durations are di Replace `IOperationRateLimitingPolicyProvider` to load policies from a database or external configuration source instead of the in-memory options. +When loading pre-built policies from an external source, use the `AddPolicy` overload that accepts an `OperationRateLimitingPolicy` object directly (bypassing the builder): + +````csharp +options.AddPolicy(new OperationRateLimitingPolicy +{ + Name = "DynamicPolicy", + Rules = + [ + new OperationRateLimitingRuleDefinition + { + Name = "HourlyLimit", + Duration = TimeSpan.FromHours(1), + MaxCount = 100, + PartitionType = OperationRateLimitingPartitionType.CurrentUser + } + ] +}); +```` + +To remove a policy (e.g., when it is deleted from the database), use `RemovePolicy`: + +````csharp +options.RemovePolicy("DynamicPolicy"); +```` + ## See Also * [ASP.NET Core Rate Limiting Middleware](https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit)