top of page

Implement a Rate Limiting Middleware in ASP.NET Core

Writer's picture: The Tech PlatformThe Tech Platform

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

Recent Posts

See All

Comments


bottom of page