question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Microsoft.AspNetCore.RateLimiting RateLimiterPolicy GetPartitionAsync

See original GitHub issue

Background and Motivation

I want to save API rate limits in the database and have the possibility of changing them at runtime (eg: based on the customer’s subscription/licensing/etc).

The problem is that IRateLimiterPolicy doesn’t have an async method

public class ClientIdRateLimiterPolicy : IRateLimiterPolicy<string>
{
    private readonly IServiceProvider _serviceProvider;

    public ClientIdRateLimiterPolicy(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public RateLimitPartition<string> GetPartition(HttpContext httpContext)
    {
        var clientId = httpContext.Request.Headers["X-ClientId"].ToString();

        using var scope = _serviceProvider.CreateScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<SampleDbContext>();

        var rateLimit = dbContext.Clients.Where(x => x.Identifier == clientId).Select(x => x.RateLimit).FirstOrDefault();

        return RateLimitPartition.GetConcurrencyRateLimiter(clientId, key => new ConcurrencyRateLimiterOptions
        {
            PermitLimit = rateLimit?.PermitLimit ?? 1
        });
    }
}

Proposed API

public interface IRateLimiterPolicy<TPartitionKey>
{
    Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected { get; }

-   RateLimitPartition<TPartitionKey> GetPartition(HttpContext httpContext);
    
+   ValueTask<RateLimitPartition<TPartitionKey>> GetPartitionAsync(HttpContext httpContext);
}

Usage Examples

public class ClientIdRateLimiterPolicy : IRateLimiterPolicy<string>
{
    private readonly IServiceProvider _serviceProvider;

    public ClientIdRateLimiterPolicy(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public async ValueTask<RateLimitPartition<string>> GetPartitionAsync(HttpContext httpContext)
    {
        var clientId = httpContext.Request.Headers["X-ClientId"].ToString();

        using var scope = _serviceProvider.CreateScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<SampleDbContext>();

        var rateLimit = await redisCache.GetAsync(clientId);
        var rateLimit = await dbContext.Clients.Where(x => x.Identifier == clientId).Select(x => x.RateLimit).FirstOrDefaultAsync();

        return RateLimitPartition.GetConcurrencyRateLimiter(clientId, key => new ConcurrencyRateLimiterOptions
        {
            PermitLimit = rateLimit?.PermitLimit ?? 1
        });
    }
}

Risks

Find a way w/o breaking changes

Issue Analytics

  • State:closed
  • Created 9 months ago
  • Reactions:1
  • Comments:6 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
halter73commented, Dec 15, 2022

This started as an edit to my previous comment, but it became big enough to deserve it’s own comment for the folks following along via email.

I think your best bet for now is to read the rate-limit asynchronously before calling into the rate limiting middleware. You can look for the EnableRateLimitingAttribute to make sure you’re not doing this unnecessarily when the endpoint doesn’t have your policy defined. This attribute should be present in the endpoint metadata whether or not it was configured as an attribute on the method or via the RequireRateLimiting() extension method.

// ...

app.Use(async (httpContext, next) =>
{
    var metadata = httpContext.GetEndpoint()?.Metadata;
    if (metadata?.GetMetadata<EnableRateLimitingAttribute>()?.PolicyName == "my-client-id-policy"
        && metadata?.GetMetadata<DisableRateLimitingAttribute>() is null)
    {
        using var scope = httpContext.RequestServices.CreateScope();
        var dbContext = scope.ServiceProvider.GetRequiredService<SampleDbContext>();

        var rateLimit = await dbContext.Clients.Where(x => x.Identifier == clientId).Select(x => x.RateLimit).FirstOrDefaultAsync();
        httpContext.Items["rate-limit"] = rateLimit;
    }

    await next(httpContext);
});

app.UseRateLimiter();

// ...

public class ClientIdRateLimiterPolicy : IRateLimiterPolicy<string>
{
    public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected => null;

    public RateLimitPartition<string> GetPartition(HttpContext httpContext)
    {
        var clientId = httpContext.Request.Headers["X-ClientId"].ToString();

        return RateLimitPartition.GetConcurrencyLimiter(clientId, key => new ConcurrencyLimiterOptions
        {
            PermitLimit = (int?)httpContext.Items["rate-limit"] ?? 1
        });
    }
}

The downside of this logic is that it will get run even if the middleware only needed the key and not the rate limiter instance from the factory. The originally proposed API has the same downside though.

Yet another alternative might be to add something like Func<TKey, HttpContext, ValueTask<RateLimiter>>? RateLimitPartition<TKey>.AsyncFactory and updating our callers to use that if it’s set. We’d also need corresponding RateLimitPartition.GetConcurrencyLimiter overloads or similar that took async factories to go with it.

0reactions
cristipufucommented, Jan 19, 2023

I believe that the option to make an async call in cases like this should be added to the framework’s APIs, the workaround is just a workaround

Read more comments on GitHub >

github_iconTop Results From Across the Web

Rate limiting middleware in ASP.NET Core
RateLimiting middleware provides rate limiting middleware. Apps configure rate limiting policies and then attach the policies to endpoints. Apps ...
Read more >
Announcing Rate Limiting for .NET - .NET Blog
Concurrency limiter limits how many concurrent requests can access a resource. If your limit is 10, then 10 requests can access a resource...
Read more >
Rate Limiting
Rate Limiter policies can be specified per route via RouteConfig.RateLimiterPolicy and can be bound from the Routes sections of the config file. As...
Read more >
Microsoft.AspNetCore.RateLimiting Namespace
Metadata that provides endpoint-specific request rate limiting. OnRejectedContext. Holds state needed for the OnRejected callback in the RateLimitingMiddleware.
Read more >
RateLimiterOptions Class
Gets or sets the global PartitionedRateLimiter<TResource> that will be applied on all requests. The global limiter will be executed first, followed by the ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found