Skip to content

Commit 61d748c

Browse files
authored
Merge pull request #25082 from abpframework/enhance/operation-rate-limiting-configure-policy
Add `ConfigurePolicy` and `ClearRules` to Operation Rate Limiting
2 parents 7cfe9bc + 9664b68 commit 61d748c

File tree

5 files changed

+366
-1
lines changed

5 files changed

+366
-1
lines changed

docs/en/Community-Articles/2026-03-10-Operation-Rate-Limiting-in-ABP-Framework/POST.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,70 @@ The two counters are completely independent. If `alice` fails 5 times, her accou
114114

115115
When multiple rules are present, the module uses a two-phase approach: it checks all rules first, and only increments counters if every rule passes. This prevents a rule from consuming quota on a request that would have been rejected by another rule anyway.
116116

117+
## Customizing Policies from Reusable Modules
118+
119+
ABP modules (including your own) can ship with built-in rate limiting policies. For example, an Account module might define a `"Account.SendPasswordResetCode"` policy with conservative defaults that make sense for most applications. When you need different rules in your specific application, you have two options.
120+
121+
**Complete replacement with `AddPolicy`:** call `AddPolicy` with the same name and the second registration wins, replacing all rules from the module:
122+
123+
```csharp
124+
Configure<AbpOperationRateLimitingOptions>(options =>
125+
{
126+
options.AddPolicy("Account.SendPasswordResetCode", policy =>
127+
{
128+
policy.AddRule(rule => rule
129+
.WithFixedWindow(TimeSpan.FromMinutes(5), maxCount: 3)
130+
.PartitionByEmail());
131+
});
132+
});
133+
```
134+
135+
**Partial modification with `ConfigurePolicy`:** when you only want to tweak part of a policy — change the error code, add a secondary rule, or tighten the window — use `ConfigurePolicy`. The builder starts pre-populated with the module's existing rules, so you only express what changes.
136+
137+
For example, keep the module's default rules but assign your own localized error code:
138+
139+
```csharp
140+
Configure<AbpOperationRateLimitingOptions>(options =>
141+
{
142+
options.ConfigurePolicy("Account.SendPasswordResetCode", policy =>
143+
{
144+
policy.WithErrorCode("MyApp:PasswordResetLimit");
145+
});
146+
});
147+
```
148+
149+
Or add a secondary IP-based rule on top of what the module already defined, without touching it:
150+
151+
```csharp
152+
Configure<AbpOperationRateLimitingOptions>(options =>
153+
{
154+
options.ConfigurePolicy("Account.SendPasswordResetCode", policy =>
155+
{
156+
policy.AddRule(rule => rule
157+
.WithFixedWindow(TimeSpan.FromHours(1), maxCount: 20)
158+
.PartitionByClientIp());
159+
});
160+
});
161+
```
162+
163+
If you want a clean slate, call `ClearRules()` first and then define entirely new rules — this gives you the same result as `AddPolicy` but makes the intent explicit:
164+
165+
```csharp
166+
Configure<AbpOperationRateLimitingOptions>(options =>
167+
{
168+
options.ConfigurePolicy("Account.SendPasswordResetCode", policy =>
169+
{
170+
policy.ClearRules()
171+
.WithFixedWindow(TimeSpan.FromMinutes(10), maxCount: 5)
172+
.PartitionByEmail();
173+
});
174+
});
175+
```
176+
177+
`ConfigurePolicy` throws if the policy name doesn't exist — which catches typos at startup rather than silently doing nothing.
178+
179+
The general rule: use `AddPolicy` for full replacements, `ConfigurePolicy` for surgical modifications.
180+
117181
## Beyond Just Checking
118182

119183
Not every scenario calls for throwing an exception. `IOperationRateLimitingChecker` provides three additional methods for more nuanced control.

docs/en/framework/infrastructure/operation-rate-limiting.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,78 @@ options.AddPolicy("Login", policy =>
115115

116116
> When multiple rules are present, the module uses a **two-phase check**: it first verifies all rules without incrementing counters, then increments only if all rules pass. This prevents wasted quota when one rule would block the request.
117117
118+
### Overriding an Existing Policy
119+
120+
If a reusable module (e.g., ABP's Account module) defines a policy with default rules, you have two ways to customize it in your own module's `ConfigureServices`.
121+
122+
**Option 1 — Full replacement with `AddPolicy`:**
123+
124+
Call `AddPolicy` with the same name. The last registration wins and completely replaces all rules:
125+
126+
````csharp
127+
// In your application module — runs after the Account module
128+
Configure<AbpOperationRateLimitingOptions>(options =>
129+
{
130+
options.AddPolicy("Account.SendPasswordResetCode", policy =>
131+
{
132+
// Replaces all rules defined by the Account module for this policy
133+
policy.AddRule(rule => rule
134+
.WithFixedWindow(TimeSpan.FromMinutes(5), maxCount: 3)
135+
.PartitionByEmail());
136+
});
137+
});
138+
````
139+
140+
> `AddPolicy` stores policies in a dictionary keyed by name, so calling it again with the same name fully replaces the previous policy and all its rules.
141+
142+
**Option 2 — Partial modification with `ConfigurePolicy`:**
143+
144+
Use `ConfigurePolicy` to modify an existing policy without replacing it entirely. The builder is pre-populated with the existing rules, so you only need to express what changes:
145+
146+
````csharp
147+
Configure<AbpOperationRateLimitingOptions>(options =>
148+
{
149+
// Only override the error code, keeping the module's original rules
150+
options.ConfigurePolicy("Account.SendPasswordResetCode", policy =>
151+
{
152+
policy.WithErrorCode("MyApp:SmsCodeLimit");
153+
});
154+
});
155+
````
156+
157+
You can also add a rule on top of the existing ones:
158+
159+
````csharp
160+
options.ConfigurePolicy("Account.SendPasswordResetCode", policy =>
161+
{
162+
// Keep the module's per-email rule and add a per-IP rule on top
163+
policy.AddRule(rule => rule
164+
.WithFixedWindow(TimeSpan.FromHours(1), maxCount: 20)
165+
.PartitionByClientIp());
166+
});
167+
````
168+
169+
Or clear all inherited rules first and define entirely new ones using `ClearRules()`:
170+
171+
````csharp
172+
options.ConfigurePolicy("Account.SendPasswordResetCode", policy =>
173+
{
174+
policy.ClearRules()
175+
.WithFixedWindow(TimeSpan.FromMinutes(5), maxCount: 3)
176+
.PartitionByEmail();
177+
});
178+
````
179+
180+
`ConfigurePolicy` returns `AbpOperationRateLimitingOptions`, so you can chain multiple calls:
181+
182+
````csharp
183+
options
184+
.ConfigurePolicy("Account.SendPasswordResetCode", p => p.WithErrorCode("MyApp:SmsLimit"))
185+
.ConfigurePolicy("Account.Login", p => p.WithErrorCode("MyApp:LoginLimit"));
186+
````
187+
188+
> `ConfigurePolicy` throws `AbpException` if the policy name is not found. Use `AddPolicy` first (in the module that owns the policy), then `ConfigurePolicy` in downstream modules to customize it.
189+
118190
### Custom Error Code
119191

120192
By default, the exception uses the error code `Volo.Abp.OperationRateLimiting:010001`. You can override it per policy:

framework/src/Volo.Abp.OperationRateLimiting/Volo/Abp/OperationRateLimiting/AbpOperationRateLimitingOptions.cs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,38 @@ public class AbpOperationRateLimitingOptions
1111

1212
public Dictionary<string, OperationRateLimitingPolicy> Policies { get; } = new();
1313

14-
public void AddPolicy(string name, Action<OperationRateLimitingPolicyBuilder> configure)
14+
public AbpOperationRateLimitingOptions AddPolicy(string name, Action<OperationRateLimitingPolicyBuilder> configure)
1515
{
16+
Check.NotNullOrWhiteSpace(name, nameof(name));
17+
Check.NotNull(configure, nameof(configure));
18+
1619
var builder = new OperationRateLimitingPolicyBuilder(name);
1720
configure(builder);
1821
Policies[name] = builder.Build();
22+
return this;
23+
}
24+
25+
/// <summary>
26+
/// Configures an existing rate limiting policy by name.
27+
/// The builder is pre-populated with the existing policy's rules and error code,
28+
/// so you can add, clear, or replace rules while keeping what you don't change.
29+
/// Throws <see cref="AbpException"/> if the policy is not found.
30+
/// </summary>
31+
public AbpOperationRateLimitingOptions ConfigurePolicy(string name, Action<OperationRateLimitingPolicyBuilder> configure)
32+
{
33+
Check.NotNullOrWhiteSpace(name, nameof(name));
34+
Check.NotNull(configure, nameof(configure));
35+
36+
if (!Policies.TryGetValue(name, out var existingPolicy))
37+
{
38+
throw new AbpException(
39+
$"Could not find operation rate limiting policy: '{name}'. " +
40+
"Make sure the policy is defined with AddPolicy() before calling ConfigurePolicy().");
41+
}
42+
43+
var builder = OperationRateLimitingPolicyBuilder.FromPolicy(existingPolicy);
44+
configure(builder);
45+
Policies[name] = builder.Build();
46+
return this;
1947
}
2048
}

framework/src/Volo.Abp.OperationRateLimiting/Volo/Abp/OperationRateLimiting/Policies/OperationRateLimitingPolicyBuilder.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,29 @@ public OperationRateLimitingPolicyBuilder WithErrorCode(string errorCode)
6262
return this;
6363
}
6464

65+
/// <summary>
66+
/// Clears all rules and custom rule types from this policy builder,
67+
/// allowing a full replacement of the inherited rules.
68+
/// </summary>
69+
/// <returns>The current builder instance for method chaining.</returns>
70+
public OperationRateLimitingPolicyBuilder ClearRules()
71+
{
72+
_rules.Clear();
73+
_customRuleTypes.Clear();
74+
return this;
75+
}
76+
77+
internal static OperationRateLimitingPolicyBuilder FromPolicy(OperationRateLimitingPolicy policy)
78+
{
79+
Check.NotNull(policy, nameof(policy));
80+
81+
var builder = new OperationRateLimitingPolicyBuilder(policy.Name);
82+
builder._errorCode = policy.ErrorCode;
83+
builder._rules.AddRange(policy.Rules);
84+
builder._customRuleTypes.AddRange(policy.CustomRuleTypes);
85+
return builder;
86+
}
87+
6588
internal void AddRuleDefinition(OperationRateLimitingRuleDefinition definition)
6689
{
6790
_rules.Add(definition);

0 commit comments

Comments
 (0)