Implementing Rate Limiting in ASP.NET 6 Core and C#

August 10th, 2022

Throttling and rate limiting restrict API access, but they serve different purposes. While rate limiting can limit API usage to ensure stability, throttling prevents users from accessing your APIs over a certain period.

This article talks about rate limiting and how it can be implemented in ASP.NET 6. It shows how we can store appropriate log messages in a PostgreSQL database using Devart for PostgreSql once the rate limit exceeds.

Pre-requisites

You’ll need the following tools to deal with code examples:

  • Visual Studio 2022 Community Edition
  • dotConnect for PostgreSQL

You can download PostgreSQL from here: https://www.postgresql.org/download/

What is Rate Limiting? Why do we need rate limiting?

When making web apps, you might want to limit how often users can make requests to a specific endpoint. To avoid denial-of-service attacks, you might want to limit the number of requests originating from an IP address within a given time frame. Enter Rate limiting.

Rate limiting is a network traffic management strategy. It limits the number of times you may execute a specified action within a defined time frame, such as accessing a specific resource, logging into an account, etc. Rate-limiting typically keeps track of the IP addresses and the duration between requests. The IP address assists in determining the origin of a specific request.

The idea of rate limiting is to limit how often a resource can be accessed. Rate limiting lets you protect a resource so that your app doesn’t get too busy and traffic stays at a safe level. A rate-limiting system can monitor the duration between each request and the overall number of requests in a given timeframe. If a single IP address makes too many requests in a short period, the rate-limiting solution will refuse the requests for a certain amount of time.

With rate limiting, you can limit how many requests a client can send to an endpoint. Previously, implementing rate limiting in ASP.NET required a lot of boilerplate code. The current versions of ASP.NET make it easy to set up and configure rate limiting.

Create a new ASP.NET 6 Core Web API Project

In this section, we’ll learn how to create a new ASP.NET 6 Core Web API project in Visual Studio 2022.

Now, follow the steps outlined below:

1. Open Visual Studio 2022.

2. Click Create a new project.

3. Select ASP.NET Core Web API and click Next.

4. Specify the project name and location to store that project in your system. Optionally, checkmark the Place solution and project in the same directory checkbox.

5. Click Next.

6. In the Additional information window, select .NET 6.0 (Long-term support) as the project version.

7. Disable the Configure for HTTPS and Enable Docker Support options (uncheck them).

8. Since we’ll not be using authentication in this example, select the Authentication type as None.

9. Since we won’t use Open API in this example, deselect the Enable OpenAPI support checkbox.

10. Since we’ll not be using minimal APIs in this example, ensure that the Use controllers (uncheck to use minimal APIs) are checked.

11. Leave the Do not use top-level statements checkbox unchecked.

12. Click Create to finish the process.

We’ll use this project in this article.

Install NuGet Packages

Before you get started implementing rate limiting, you should install the dotConnect for PostgreSQL and AspNetCoreRateLimit packages in your project. You can install them either from the NuGet Package Manager tool inside Visual Studio or, from the NuGet Package Manager console using the following commands:

PM> Install-Package Devart.Data.PostgreSql
PM> Install-Package AspNetCoreRateLimit

Implement Rate Limiting in ASP.NET 6

In this section, we’ll implement rate limiting in ASP.NET 6. For the sake of simplicity, we’ll take advantage of the default generated controller named WeatherForecastController here. Now, create a new class named CustomClientRateLimitMiddleware that extends the ClientRateMiddleware class of the AspNetCoreRateLimit library and write the following code there:

using AspNetCoreRateLimit;
using Microsoft.Extensions.Options;
using System.Text.Json;
namespace RateLimitingDemo
{
    public class MyCustomClientRateLimitMiddleware : ClientRateLimitMiddleware
    {
        public MyCustomClientRateLimitMiddleware(RequestDelegate next,
            IProcessingStrategy processingStrategy,
            IOptions<ClientRateLimitOptions> options,
            IClientPolicyStore policyStore,
            IRateLimitConfiguration config,
            ILogger<ClientRateLimitMiddleware> logger) :
            base(next, processingStrategy, options, policyStore, config, logger)

        {

        }

        public override Task ReturnQuotaExceededResponse
       (HttpContext httpContext, RateLimitRule rule, string retryAfter)
       {
            string? path = httpContext?.Request?.Path.Value;
            var result = JsonSerializer.Serialize("API calls quota exceeded!”);
            httpContext.Response.Headers["Retry-After"] = retryAfter;
            httpContext.Response.StatusCode = 429;
            httpContext.Response.ContentType = "application/json";

            WriteQuotaExceededResponseMetadata(path, retryAfter);
            return httpContext.Response.WriteAsync(result);

        }
        private void WriteQuotaExceededResponseMetadata
        (string requestPath, string retryAfter, int statusCode = 429)
        {
            //Code to write data to the database
        }
    }
}

In the next section, we’ll store this metadata in a database table.

Persist Quota Exceeded Response Metadata to the Database

We’ll now implement the WriteQuotaExceededResponseMetadata method to store the quota exceeded response information in a PostgreSQL database table. The following code listing shows how you can insert this information into a PostgreSQL database table.

private void WriteQuotaExceededResponseMetadata(string requestPath, string retryAfter, int statusCode = 429)
{
    try
    {
          using (PgSqlConnection pgSqlConnection =
            new PgSqlConnection("User Id = postgres; Password = sa123#;" +
            "host=localhost;database=postgres;"))
          {
              using (PgSqlCommand cmd = new PgSqlCommand())
              {
                  cmd.CommandText = "INSERT INTO public.demo " +
                      "(id, requestpath, retryafter, statuscode)
                  VALUES(@id, @requestPath, @retryAfter, @statusCode)";

                  cmd.Connection = pgSqlConnection;
                  cmd.Parameters.AddWithValue("id", Guid.NewGuid().ToString());
                  cmd.Parameters.AddWithValue("requestpath", requestPath);
                  cmd.Parameters.AddWithValue("retryafter", retryAfter);
                  cmd.Parameters.AddWithValue("statuscode", statusCode);

                  if (pgSqlConnection.State != System.Data.ConnectionState.Open)
                      pgSqlConnection.Open();
                  cmd.ExecuteNonQuery();

              }
          }
    }
    catch
    {
          throw;
    }
}

The complete source code is given below for your reference:

using AspNetCoreRateLimit;
using Devart.Data.PostgreSql;
using Microsoft.Extensions.Options;
using System.Text.Json;
namespace RateLimitingDemo

{
    public class MyCustomClientRateLimitMiddleware : ClientRateLimitMiddleware
    {
       public MyCustomClientRateLimitMiddleware(RequestDelegate next,
       IProcessingStrategy processingStrategy,
       IOptions<ClientRateLimitOptions> options, IClientPolicyStore policyStore,
       IRateLimitConfiguration config, ILogger<ClientRateLimitMiddleware> logger) :
            base(next, processingStrategy, options, policyStore, config, logger)

        {

        }
        public override Task ReturnQuotaExceededResponse
       (HttpContext httpContext, RateLimitRule rule, string retryAfter)
       {
            string? requestPath = httpContext?.Request?.Path.Value;
            var result = JsonSerializer.Serialize("API calls quota exceeded!");
            httpContext.Response.Headers["Retry-After"] = retryAfter;
            httpContext.Response.StatusCode = 429;
            httpContext.Response.ContentType = "application/json";

            WriteQuotaExceededResponseMetadata(requestPath, retryAfter);
            return httpContext.Response.WriteAsync(result);
       }

       private void WriteQuotaExceededResponseMetadata
       (string requestPath, string retryAfter, int statusCode = 429)
       {
           try
           {
               using (PgSqlConnection pgSqlConnection =
               new PgSqlConnection("User Id = postgres; Password = sa123#;" +
               "host=localhost;database=postgres;"))
               {
                   using (PgSqlCommand cmd = new PgSqlCommand())
                   {
                       cmd.CommandText = "INSERT INTO public.demo " +
                           "(id, requestpath, retryafter, statuscode)
                       VALUES(@id, @requestPath, @retryAfter, @statusCode)";

                       cmd.Connection = pgSqlConnection;
                       cmd.Parameters.AddWithValue("id", Guid.NewGuid().ToString());
                       cmd.Parameters.AddWithValue("requestpath", requestPath);
                       cmd.Parameters.AddWithValue("retryafter", retryAfter);
                       cmd.Parameters.AddWithValue("statuscode", statusCode);

                       if (pgSqlConnection.State != System.Data.ConnectionState.Open)
                           pgSqlConnection.Open();
                       cmd.ExecuteNonQuery();
                   }
               }
           }
           catch
           {
               throw;
           }
       }
    }
}

You should add this middleware to the request processing pipeline using the following code in the Program.cs file:

app.UseMiddleware<MyCustomClientRateLimitMiddleware>();

Configure Rate Limiting in the Program.cs file

You should add IMemoryCache services to the pipeline using the following code:

builder.Services.AddMemoryCache();

You should also add an instance of type IRateLimitConfiguration to the services container. The IRateLimitConfiguration contains a declaration of methods such as the RegisterResolvers method used to configure the rate limit for your application.

builder.Services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();

To configure the middleware, you can write the following code in the Program.cs file:

builder.Services.Configure<ClientRateLimitOptions>(options =>
{
    options.EnableEndpointRateLimiting = true;
    options.StackBlockedRequests = false;
    options.HttpStatusCode = 429;
    options.GeneralRules = new List<RateLimitRule>
        {
            new RateLimitRule
            {
                Endpoint = "*",
                Period = "10s",
                Limit = 2
            }
        };
});

Note how the rate limit rule has been added to the GeneralRules list. The status code has been set as 429 which implies that when the rate limit rule is violated, this status code will be returned in the response.

If you’re to rate limit a specific endpoint, you can use the following code snippet:

options.GeneralRules = new List<RateLimitRule>
{
    new RateLimitRule
    {
         Endpoint = "GET:/author/GetAuthors",
         Period = "10s",
         Limit = 2,
    }
}

The complete source code of the Program.cs file is given below for your reference:

using AspNetCoreRateLimit;
using RateLimitingDemo;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddMemoryCache();

builder.Services.AddSingleton<IIpPolicyStore, MemoryCacheIpPolicyStore>();
builder.Services.AddSingleton<IRateLimitCounterStore, MemoryCacheRateLimitCounterStore>();
builder.Services.AddSingleton<IProcessingStrategy, AsyncKeyLockProcessingStrategy>();
builder.Services.AddSingleton<IRateLimitConfiguration, RateLimitConfiguration>();

builder.Services.AddInMemoryRateLimiting();

builder.Services.Configure<ClientRateLimitOptions>(options =>
{
    options.EnableEndpointRateLimiting = true;
    options.StackBlockedRequests = false;
    options.HttpStatusCode = 429;
    options.ClientIdHeader = "Client-Id";
    options.GeneralRules = new List<RateLimitRule>
        {
            new RateLimitRule
            {
                Endpoint = "*",
                Period = "10s",
                Limit = 2
            }

        };
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseMiddleware<MyCustomClientRateLimitMiddleware>();

app.UseAuthorization();
app.MapControllers();

app.Run();

You can add multiple rules as well. The following code snippet shows how this can be achieved:

services.Configure<ClientRateLimitOptions>(options =>
{
    options.GeneralRules = new List<RateLimitRule>
    {
        new RateLimitRule
        {
            Endpoint = "*",
            Period = "1m",
            Limit = 10,
        },
        new RateLimitRule
        {
            Endpoint = "*",
            Period = "5s",
            Limit = 1,
        }
    };
});

The preceding code shows how you can add two rules – one that limits calls to 10 in a minute and another that limits calls to a maximum of 1 in 5 seconds. You can also specify rate limit configuration metadata in a configuration file such as the appsettings.json file as shown in the code snippet given below:

"ClientRateLimiting": {
  "EnableEndpointRateLimiting": false,
  "StackBlockedRequests": false,
  "ClientIdHeader": "X-ClientId",
  "HttpStatusCode": 429,
  "EndpointWhitelist": [ "get:/api/login", "*:/api/register" ],
  "ClientWhitelist": [ "joydip", "admin" ],
  "GeneralRules": [
    {
      "Endpoint": "*",
      "Period": "5s",
      "Limit": 2
    },
    {
      "Endpoint": "*",
      "Period": "1m",
      "Limit": 10
    }
  ]
}

Execute the application

Launch the API application from Visual Studio – on our computer, the URL is http://localhost:5098/.

Now launch Postman to test the API as shown in Figure 1 below:

Figure 1. Testing the API using Postman

Execute the endpoint multiple times within a short time span. Now, note the HTTP Status Code and the message in the response.

Summary

Rate limiting is one of the simplest and most effective methods of controlling traffic to your APIs. In this article, we’ve implemented client rate limiting using the AspNetCoreRateLimit package in ASP.NET 6. Note that you can also rate limit your APIs based on the client Id, IP Address, white list clients or IP Addresses, etc.

Comments are closed.