An introduction to the new rate-limiting middleware available in ASP.NET Core and how it compares to the AspNetCoreRateLimit package that solves the same task.
Introduction
Since I learned that ASP.NET Core 7 and newer come with built-in rate limiting I have wanted to try it out. I finally had some time to check it out and here's what I've found so far. This introduces rate limiting in ASP.NET Core using both the built-in option and AspNetCoreRateLimit
.
Background
A quick comment before we begin. I've blogged about this subject in the past, so I would recommend you to read through this post: Rate limiting API requests with ASP.NET Core and AspNetCoreRateLimit. In this post, I'll be comparing the built-in rate limiting feature in ASP.NET Core with AspNetCoreRateLimit
. I'll try to summarize the previous post here but for the full experience, consider reading both posts.
Rate-limiting in ASP.NET Core
Rate limiting in a web application is typically about ... well limiting the number of requests processed by the web application. For public APIs, you quickly need some way of limiting how many requests your users can make. There's a range of possibilities as to how to implement this, whether this is based on the client's IP address, an API key/token, or something third. To compare the examples in this post with the previous post about AspNetCoreRateLimit
, I'll implement rate limiting based on an API key.
To test out rate limiting, you will need .NET 7 or newer as well as the newest version of Visual Studio 2022 (currently the preview version is required to run .NET 7 apps).
Create a new application by running the following command:
dotnet new mvc
This creates a new MVC app. Rate limiting is not specific to ASP.NET Core MVC and can be used with minimal API and other types as well. In the new web application, install the Microsoft.AspNetCore.RateLimiting
NuGet package:
dotnet add package Microsoft.AspNetCore.RateLimiting
In the Program.cs file or (Startup.cs if don't like top-level statements) include rate limiting configuration like this:
builder.Services.AddRateLimiter(options =>
{
});
This will tell ASP.NET Core that you want to configure the rate-limiting middleware with default settings. One default parameter that I would like to change up front is the status code that should be returned once the limit is reached. As a default Microsoft.AspNetCore.RateLimiting
will return a status code of 503
. To change this to the (more correct IMO) status code 429
, provide it as part of the options:
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
});
Next, we will need to tell the rate limit middleware when a limit is reached. This is done with a concept called policies. There's a range of different policy types available in the rate-limiting middleware. In this example, I want to limit the number of requests made with the same API key available in the URL like this:
https://localhost:7126/?api_key=mykey
To configure this, include the following policy:
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddPolicy("apikey", httpContext =>
{
if (httpContext.Request.Query.Keys.Contains("api_key"))
{
return RateLimitPartition.GetFixedWindowLimiter(
httpContext.Request.Query["api_key"].ToString(),
fac =>
{
return new FixedWindowRateLimiterOptions
{
Window = TimeSpan.FromHours(1),
PermitLimit = 10,
};
});
}
else
{
return RateLimitPartition.GetNoLimiter("");
}
});
});
Let's go through the policy line-by-line. Policies are added by calling the AddPolicy
method. The first parameter is a policy name that we will need to reference this policy. This name will be used in the following step. Next, we provide a Func
that contains the actual code to run for this policy. In the callback, I check if the current request contains a query parameter named api_key
. In this case, we want rate limiting to apply. If not, rate limiting should not be run which is done by returning the result of GetNoLimiter
.
This rate limit policy is based on the fixed window limit. As already mentioned, there is a range of different policy types available, so make sure to check out the documentation before picking. The fixed window limiter will rate limit requests within a specified time window. In this example, I permit 10 requests per hour using the same API key. In the real world, your API should probably be able to handle more than 10 requests but for this, I have chosen a low number to easily test this in the browser.
The final step missing is to tell ASP.NET Core to use the middleware and where to apply the policy. There are attributes available to apply this to specific actions and/or controllers, but for this example, I'll apply it to everything:
app.UseRateLimiter();
app
.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}")
.RequireRateLimiting("apikey");
By appending RequestRateLimiting
to the controller set up and including the policy name that we set in the previous step, ASP.NET Core will automatically run the policy against all controllers. Remember that only endpoints with an api_key
query parameter will apply here.
AspNetCoreRateLimit
That's a very basic example of how rate limiting can be implemented with the Microsoft.AspNetCore.RateLimiting
package. Let's quickly revisit the AspNetCoreRateLimit
package to see how a similar policy can be implemented with that package.
AspNetCoreRateLimit
is another rate-limiting library that has existed for years. It is maintained by Stefan Prodan and offers a very flexible model for implementing rate limiting in ASP.NET Core.
Start by installing the AspNetCoreRateLimit
NuGet package. If you are coding along, you can install it in the same project as before but having two rate-limiting packages installed isn't recommended for real code:
dotnet add package AspNetCoreRateLimit
A policy in AspNetCoreRateLimit
is implemented as configuration and a client resolver:
public class ElmahIoRateLimitConfiguration : RateLimitConfiguration
{
public ElmahIoRateLimitConfiguration(
IOptions<IpRateLimitOptions> ipOptions,
IOptions<ClientRateLimitOptions> clientOptions)
: base(ipOptions, clientOptions)
{
}
public override void RegisterResolvers()
{
base.RegisterResolvers();
ClientResolvers.Add(new ClientQueryStringResolveContributor());
}
}
public class ClientQueryStringResolveContributor : IClientResolveContributor
{
public Task<string> ResolveClientAsync(HttpContext httpContext)
{
var queryDictionary =
Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(
httpContext.Request.QueryString.ToString());
if (queryDictionary.ContainsKey("api_key")
&& !string.IsNullOrWhiteSpace(queryDictionary["api_key"]))
{
return Task.FromResult(queryDictionary["api_key"].ToString());
}
return Task.FromResult(Guid.NewGuid().ToString());
}
}
The code in this post has been updated to match the newest version of AspNetCoreRateLimit
. For more details on how AspNetCoreRateLimit
works, check out the previous post. The code above corresponds to the policy that we specified with the other package based on a query parameter named api_key
.
These classes can be configured in the Program.cs file:
builder.Services.AddSingleton<IClientPolicyStore, MemoryCacheClientPolicyStore>();
builder.Services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();
builder.Services.AddSingleton<IProcessingStrategy, AsyncKeyLockProcessingStrategy>();
builder.Services.AddSingleton<IRateLimitConfiguration, ElmahIoRateLimitConfiguration>();
The additional singletons tell AspNetCoreRateLimit
how to store the current state. The limit and timespan from the previous example are provided through options:
builder.Services.Configure<ClientRateLimitOptions>(options =>
{
options.GeneralRules = new List<RateLimitRule>
{
new RateLimitRule
{
Endpoint = "*",
Period = "1h",
Limit = 10,
}
};
});
Again, we limit the requests to 10 per hour. The *
value for the Endpoint
property matches how we included the rate-limiting middleware to all controllers in the previous example.
The only missing part is calling the UseClientRateLimiting
method:
app.UseClientRateLimiting();
That's it! A similar rate limit has now been implemented using AspNetCoreRateLimit
. Whether you prefer one package over the other, I will leave it up to you. These examples only show a limited set of capabilities from each package so I would recommend you to try out both and pick the one you prefer.
History
- 28th November, 2022: Initial version