How to Limit API Request Count in .NET Core

How to Limit API Request Count in .NET Core

I've previously introduced wheels like AspNetCoreRateLimit, but today I won't discuss that; let's talk about the underlying principles.

Last updated 7/8/2022 6:54 AM
黑哥聊dotNet
4 min read
Category
ASP.NET Core
Tags
.NET C# ASP.NET Core

I have previously introduced tools like AspNetCoreRateLimit to everyone, so I won’t go over that today. Instead, let’s talk about the underlying principles. Feedback from experts is welcome!

Consider some common API request interfaces we often see:

Take the seven-day weather forecast API for major foreign cities as an example: non-VIP users can only use it 2000 times, while VIP users can make up to 10,000 requests per day. To access this API, you must register an account to obtain an appid and secret key.

Based on this requirement, let’s design a rate-limited weather API.

Step 1

Verify whether the login account exists. If it does not exist, throw an error indicating that the account does not exist.

[HttpPost("GetWeather")]
public IActionResult ApiLimit(WeatherInfor weatherInfor)
{
  if (!_userService.IsValid(weatherInfor.Appid, weatherInfor.Appsecret))
  {
    throw new Exception("Incorrect account or password");
  }
}

Step 2

Determine whether the account is a VIP user. If it is a VIP, there is no limit on total calls, only a daily limit. For a daily limit that resets the next day, using a cache is reasonable. We set the cache to expire at 23:59:59 each day, which is an absolute expiration time.

The specific business logic is as follows: Since the user's appid is unique, we can use it as the key and the call count as the value. If the cache does not exist, we add it; if it exists, we retrieve the call count. If the count exceeds 2000, we inform the caller that the quota is exhausted. Otherwise, we get the cached call count and increment it by one.

Cache class

public class MemoryCacheHelper
{

    public static MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());

    /// <summary>
    /// Check whether the cache item exists
    /// </summary>
    /// <param name="key">Cache key</param>
    /// <returns></returns>
    public static bool Exists(string key)
    {
        if (key == null)
        {
            return false;
        }
        return _cache.TryGetValue(key, out _);
    }

    /// <summary>
    /// Get cached data
    /// </summary>
    /// <param name="key">Cache key</param>
    /// <returns></returns>
    public static object Get(string key)
    {
        if (key == null)
        {
            throw new ArgumentNullException(nameof(key));
        }
        if (!Exists(key))
            throw new ArgumentNullException(nameof(key));


        return _cache.Get(key);
    }

    /// <summary>
    /// Add cache
    /// </summary>
    /// <param name="key">Cache key</param>
    /// <param name="value">Cache value</param>
    /// <param name="expiresSliding">Sliding expiration time (if there is an operation within the expiration time, the expiration time is extended from the current time)</param>
    /// <param name="expiressAbsoulte">Absolute expiration time</param>
    /// <returns></returns>
    public static bool AddMemoryCache(string key, object value)
    {
        if (key == null)
        {
            throw new ArgumentNullException(nameof(key));
        }
        if (value == null)
        {
            throw new ArgumentNullException(nameof(value));
        }
        DateTime time = Convert.ToDateTime(DateTime.Now.AddDays(1).ToString("D").ToString()).AddSeconds(-1);
        _cache.Set(key, value, time);
        return Exists(key);
    }
}

Business logic

public bool IsLimit(string appId)
{
    if (MemoryCacheHelper.Exists(appId))
    {
        int value = int.Parse(MemoryCacheHelper.Get(appId).ToString());
        if (value > 2000)
            return false;
        else
            value = value + 1;

        MemoryCacheHelper.AddMemoryCache(appId, value);
    }
    else
    {
        MemoryCacheHelper.AddMemoryCache(appId, 1);
    }
    return true;
}

Finally

Query the weather API and return the data.

[HttpPost("GetWeather")]
public IActionResult ApiLimit(WeatherInfor weatherInfor)
{
    if (!_userService.IsValid(weatherInfor.Appid, weatherInfor.Appsecret))
    {
        throw new Exception("Incorrect account or password");
    }
    bool isLimit = false;
    if (_userService.IsVIP(weatherInfor.Appid))
    {
        isLimit= _sqlServices.IsLimit(weatherInfor.Appid);
    }
    else
    {
        isLimit = _memoryCacheServices.IsLimit(weatherInfor.Appid);
    }
    if (isLimit)
    {
        // Query the weather API and return data
    }
    else
    {
        throw new Exception("Call quota exhausted");
    }
    return Ok("");
}

If you like my article, please follow and give me a like. I hope the .NET ecosystem gets better and better!

Keep Exploring

Related Reading

More Articles
Same category / Same tag 6/22/2022

ASP.NET Core WebAPI Localization (Single Resource File)

Microsoft's default approach is one class corresponding to multiple resource files, which is cumbersome to use. This article introduces the use of a single resource file, where all classes in the entire project correspond to one set of multilingual resource files.

Continue Reading