top of page

Implement a Rate Limiting Middleware in ASP.NET Core

When building APIs, regardless of the framework and language you are using, you may want to limit the number of requests an API accepts within a window of time. This is done by rate-limiting strategy which helps to limit the network traffic. In this article, I am going to implement a very simple rate-limiting middleware in ASP.NET Core.


Definitions

We will create the following middlewares and classes to enable request throttling in our App.

  • RateLimitMiddlware:

This Middleware aborts an API call if a client exceeds the number of requests the API receives within a time frame in which we considered for the executing API through decorating them by a class (i.e., RateLimitDecorator) that describes a rate-limiting strategy.

  • RateLimitDecorator:

We decorate our APIs with this class to choose a strategy needed for request throttling.


Implementation

First off, create an ASP.Net Core Web API.



Then add a class named RateLimitMiddlware.cs as follows:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Controllers;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

namespace dotnetcore_rate_limiting
{
    public class RateLimitMiddlware    
    {
        private readonly RequestDelegate _next;
        static readonly ConcurrentDictionary<string, DateTime?> 
                                    ApiCallsInMemory = new();
        public RateLimitMiddlware(RequestDelegate next)        
        {
            _next=next;        
        }
        
        public async Task InvokeAsync(HttpContext context)        
        {
            var endpoint = context.GetEndpoint();
            varcontrollerActionDescriptor = endpoint?
             .Metadata.GetMetadata<ControllerActionDescriptor>();
            
            if (controllerActionDescriptor is null)            
            {
                await _next(context);
                return;            
            }
            
            var apiDecorator = (RateLimitDecorator)
            controllerActionDescriptor.MethodInfo                            
                .GetCustomAttributes(true)                            
                .SingleOrDefault(w => w.GetType() == 
                                typeof(RateLimitDecorator));
            
            if (apiDecorator is null)            
            {
                await _next(context);
                return;            
            }
            
            string key = GetCurrentClientKey(apiDecorator, 
                                                    context);
            
            var previousApiCall = GetPreviousApiCallByKey(key);
            if (previousApiCall != null)            
            {
                if (DateTime.Now<previousApiCall.Value.         
                                                AddSeconds(5))                
                {
                    context.Response.StatusCode = 
                            (int)HttpStatusCode.TooManyRequests;
                    return;                
                }            
            }
            
            UpdateApiCallFor(key);
            
            await _next(context);        
        }
        
        /// <summary>
        /// We store the time that a client made a call to the 
        current API
        /// </summary>
        /// <paramname="key"></param>
        
        private void UpdateApiCallFor(string key)        
        {
            ApiCallsInMemory.TryRemove(key, out_);
            ApiCallsInMemory.TryAdd(key, DateTime.Now);        
        }
        
        private DateTime? GetPreviousApiCallByKey(string key)        
        {
            ApiCallsInMemory.TryGetValue(key, outDateTime? 
                                                        value);
            return value;       
        }
        
        /// <summary>
        /// Makes key based on the specified strategy for the 
        current API
        /// </summary>
        /// <paramname="apiDecorator"></param>
        /// <paramname="context"></param>
        /// <returns></returns>
        
        private static string GetCurrentClientKey
        (
            RateLimitDecorator apiDecorator,
             HttpContext context)        
        {
            var keys = new List<string>            
            {
                context.Request.Path            
            };
            
            if (apiDecorator.StrategyType == StrategyTypeEnum.     
                                                    IpAddress)
                keys.Add(GetClientIpAddress(context));
            
            // TODO: Implement other strategies.
            
            return string.Join('_', keys);        
        }
        
        /// <summary>
        /// Returns the client's Ip Address
        /// </summary>
        /// <paramname="context"></param>
        /// <returns></returns>
        private static string GetClientIpAddress(HttpContext 
                                                        context)        
        {
            // Check this out
            // https://stackoverflow.com/questions/28664686/how-
            do-i-get-client-ip-address-in-asp-net-core
            return context.Connection.RemoteIpAddress.ToString();        
        }    
    }
}


Then, create the RateLimitDecorator attribute to be used by your APIs. Currently, It contains a StrategyType property only.

using System;
namespace dotnetcore_rate_limiting
{        
    [AttributeUsage(AttributeTargets.Method)]
    public class RateLimitDecorator : Attribute    
    {
        public StrategyTypeEnum StrategyType { get; set; }    
    }
}

namespace dotnetcore_rate_limiting
{
    public enum StrategyTypeEnum    
    {
        IpAddress,
        PerUser,
        PerApiKey    
    }
}

Now, let’s config our startup class to use the implemented RateLimitMiddleware:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)        
{
    // Other middlewares ...
    app.UseMiddleware<RateLimitMiddlware>();
    
    app.UseEndpoints(endpoints=>            
    {
        endpoints.MapControllers();            
    });        
}

It is time to decorate our API with RateLimitDecorator to limit the number of requests it receives.

[HttpGet]        [RateLimitDecorator(StrategyType=StrategyTypeEnum.IpAddress)]
public IEnumerable<WeatherForecast> Get()        
{
    var rng = new Random();
    return Enumerable.Range(1, 5).Select(index => new 
                                                WeatherForecast            
    {
        Date=DateTime.Now.AddDays(index),
        TemperatureC=rng.Next(-20, 55),
        Summary=Summaries[rng.Next(Summaries.Length)]            
   })            
   .ToArray();        }

Congrats! You limited the Get API to be executable every 5 seconds based on the clients’ Ip Addresses.


If you try to execute the API twice or more within 5 seconds, the middleware comes into play and returns 429 status code, which is Too many requests.


Source : Medium - AhmadReza Payan


The Tech Platform

0 comments

Comments


bottom of page