@ -71,6 +71,74 @@ public class SmsAppService : ApplicationService
`CheckAsync` checks the current usage against the limit and throws `AbpOperationRateLimitingException` (HTTP 429) if the limit is already exceeded. If the check passes, it then increments the counter and proceeds. ABP's exception pipeline catches this automatically and returns a standard error response. Put `CheckAsync` first — the rate limit check is the gate, and everything else only runs if it passes.
## Declarative Usage with `[OperationRateLimiting]`
The explicit `CheckAsync` approach is useful when you need fine-grained control — for example, when you want to check the limit conditionally, or when the parameter value comes from somewhere other than a method argument. But for the common case where you simply want to enforce a policy on every invocation of a specific method, there's a cleaner way: the `[OperationRateLimiting]` attribute.
```csharp
public class SmsAppService : ApplicationService
{
[OperationRateLimiting("SendSmsCode")]
public virtual async Task SendCodeAsync([RateLimitingParameter] string phoneNumber)
{
// Rate limit is enforced automatically — no manual CheckAsync needed.
The attribute works on both **Application Service methods** (via ABP's interceptor) and **MVC Controller actions** (via an action filter). No manual injection of `IOperationRateLimitingChecker` required.
### Providing the Partition Key
When using the attribute, the partition key is resolved from the method's parameters automatically:
- Mark a parameter with `[RateLimitingParameter]` to use its `ToString()` value as the key — this is the most common case when the key is a single primitive like a phone number or email.
- Have your input DTO implement `IHasOperationRateLimitingParameter` and provide a `GetPartitionParameter()` method — useful when the key is a property buried inside a complex input object.
```csharp
public class SendSmsCodeInput : IHasOperationRateLimitingParameter
{
public string PhoneNumber { get; set; }
public string Language { get; set; }
public string? GetPartitionParameter() => PhoneNumber;
}
[OperationRateLimiting("SendSmsCode")]
public virtual async Task SendCodeAsync(SendSmsCodeInput input)
{
// input.GetPartitionParameter() = input.PhoneNumber is used as the partition key.
}
```
If neither is provided, `Parameter` is `null` — which is perfectly valid for policies that use `PartitionByCurrentUser`, `PartitionByClientIp`, or similar partition types that don't rely on an explicit value.
```csharp
// Policy uses PartitionByCurrentUser — no partition key needed.
[OperationRateLimiting("GenerateReport")]
public virtual async Task<ReportDto> GenerateMonthlyReportAsync()
{
// Rate limit is checked per current user, automatically.
}
```
> The resolution order is: `[RateLimitingParameter]` first, then `IHasOperationRateLimitingParameter`, then `null`. If the method has parameters but none is resolved, a warning is logged to help you catch the misconfiguration early.
You can also place `[OperationRateLimiting]` on the class itself to apply the policy to all public methods:
```csharp
[OperationRateLimiting("MyServiceLimit")]
public class MyAppService : ApplicationService
{
public virtual async Task MethodAAsync([RateLimitingParameter] string key) { ... }
public virtual async Task MethodBAsync([RateLimitingParameter] string key) { ... }
}
```
A method-level attribute always takes precedence over the class-level one.
## Choosing a Partition Type
The partition type controls **how counters are isolated from each other** — it's the most important decision when setting up a policy, because it determines *what dimension you're counting across*.
@ -237,7 +305,7 @@ public override void ConfigureServices(ServiceConfigurationContext context)
## Summary
ABP's Operation Rate Limiting fills the gap that ASP.NET Core's HTTP middleware can't: rate limiting with real awareness of *who* is doing *what*. Define a named policy, pick a time window, a max count, and a partition type. Call `CheckAsync` wherever you need it. Counter storage, distributed locking, and exception handling are all taken care of.
ABP's Operation Rate Limiting fills the gap that ASP.NET Core's HTTP middleware can't: rate limiting with real awareness of *who* is doing *what*. Define a named policy, pick a time window, a max count, and a partition type. Then either call `CheckAsync` explicitly, or just add `[OperationRateLimiting]` to your method and let the framework handle the rest. Counter storage, distributed locking, and exception handling are all taken care of.
@ -74,6 +74,117 @@ public class SmsAppService : ApplicationService
That's the basic usage. The following sections explain each concept in detail.
## Declarative Usage (Attribute)
Instead of injecting `IOperationRateLimitingChecker` manually, you can use the `[OperationRateLimiting]` attribute to enforce a policy declaratively on Application Service methods or MVC Controller actions.
> **Application Services** are handled by the ABP interceptor (built into the Domain layer).
> **MVC Controllers** are handled by `AbpOperationRateLimitingActionFilter`, which is automatically registered when you reference the `Volo.Abp.OperationRateLimiting.AspNetCore` package.
### Applying to an Application Service
````csharp
public class SmsAppService : ApplicationService
{
[OperationRateLimiting("SendSmsCode")]
public virtual async Task SendCodeAsync([RateLimitingParameter] string phoneNumber)
{
// Rate limit is checked automatically before this line executes.
The `[OperationRateLimiting]` attribute resolves `OperationRateLimitingContext.Parameter` automatically using the following priority order:
1. **`[RateLimitingParameter]`** — a method parameter marked with this attribute. Its `ToString()` value is used as the partition key.
2. **`IHasOperationRateLimitingParameter`** — a method parameter whose type implements this interface. The value returned by `GetPartitionParameter()` is used as the partition key.
3. **`null`** — no parameter is resolved; suitable for policies that use `PartitionByCurrentUser`, `PartitionByClientIp`, etc.
#### Using `[RateLimitingParameter]`
Mark a single parameter to use its value as the partition key:
````csharp
[OperationRateLimiting("SendSmsCode")]
public virtual async Task SendCodeAsync([RateLimitingParameter] string phoneNumber)
{
// partition key = phoneNumber
}
````
#### Using `IHasOperationRateLimitingParameter`
Implement the interface on an input DTO when the partition key is a property of the DTO:
````csharp
public class SendSmsCodeInput : IHasOperationRateLimitingParameter
{
public string PhoneNumber { get; set; }
public string Language { get; set; }
public string? GetPartitionParameter() => PhoneNumber;
}
````
````csharp
[OperationRateLimiting("SendSmsCode")]
public virtual async Task SendCodeAsync(SendSmsCodeInput input)
If no parameter is marked and no DTO implements the interface, the policy is checked without a `Parameter` value. This is appropriate for policies that use `PartitionByCurrentUser`, `PartitionByClientIp`, or `PartitionByCurrentTenant`:
````csharp
// Policy uses PartitionByCurrentUser — no explicit parameter needed.
[OperationRateLimiting("GenerateReport")]
public virtual async Task<ReportDto> GenerateMonthlyReportAsync()
{
// Rate limit is checked per current user automatically.
}
````
> If the method has parameters but none is resolved, a **warning log** is emitted to help you catch misconfigured usages early.
### Applying to a Class
You can also place `[OperationRateLimiting]` on the class to apply it to **all public methods** of that class:
````csharp
[OperationRateLimiting("MyServiceLimit")]
public class MyAppService : ApplicationService
{
public virtual async Task MethodAAsync([RateLimitingParameter] string key) { ... }
public virtual async Task MethodBAsync([RateLimitingParameter] string key) { ... }
}
````
> A method-level attribute takes precedence over the class-level attribute.
## Defining Policies
Policies are defined using `AbpOperationRateLimitingOptions` in the `ConfigureServices` method of your [module class](../framework/architecture/modularity/basics.md). Each policy has a unique name, one or more rules, and a partition strategy.
@ -502,7 +613,7 @@ This module and ASP.NET Core's built-in [rate limiting middleware](https://learn