Skip to content

Instantly share code, notes, and snippets.

@jpolvora
Created December 8, 2025 19:38
Show Gist options
  • Select an option

  • Save jpolvora/1dd827cc2529c71b5c02d4f305d794de to your computer and use it in GitHub Desktop.

Select an option

Save jpolvora/1dd827cc2529c71b5c02d4f305d794de to your computer and use it in GitHub Desktop.
CustomRateLimiterAttribute for ASP.NET 4 MVC 5
using System;
using System.Net;
using System.Runtime.Caching;
using System.Web;
using System.Web.Mvc;
public class CustomRateLimitAttribute : ActionFilterAttribute
{
// These properties are set when applying the attribute:
// [CustomRateLimit(Limit = 5, WindowInMinutes = 1)]
public int Limit { get; set; }
public int WindowInMinutes { get; set; }
// Use a single static cache instance across the application
private static readonly MemoryCache _cache = MemoryCache.Default;
private static readonly object _lockObject = new object();
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var request = filterContext.HttpContext.Request;
// Use the function to get the actual client IP (handling Azure proxies)
var ipAddress = GetClientIpAddress(request);
var actionName = filterContext.ActionDescriptor.ActionName;
var controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName;
// Create a unique cache key based on user IP, controller, and action
var key = string.Format("{0}-{1}-{2}", ipAddress, controllerName, actionName);
// Protect the cache operations with a lock to ensure thread safety
lock (_lockObject)
{
// Try to get the current request count from the cache
var count = _cache.Get(key) as int? ?? 0;
if (count >= Limit)
{
// Limit exceeded. Set the HTTP status code to 429 "Too Many Requests"
filterContext.Result = new HttpStatusCodeResult(429, "Too Many Requests. Please try again later.");
// Optionally, add a "Retry-After" header if needed
return;
}
// Increment the counter and store it back in the cache
// If it's the first request (count == 0), set an absolute expiration
if (count == 0)
{
_cache.Add(key, 1, DateTimeOffset.UtcNow.AddMinutes(WindowInMinutes));
}
else
{
// Update the existing entry with the new count
// The original expiration time remains the same
_cache.Set(key, count + 1, _cache[key] as DateTimeOffset? ?? DateTimeOffset.UtcNow.AddMinutes(WindowInMinutes));
}
}
base.OnActionExecuting(filterContext);
}
/// <summary>
/// Helper method to get the actual client IP address, checking X-Forwarded-For for Azure/proxied requests.
/// </summary>
private static string GetClientIpAddress(HttpRequestBase request)
{
string ipAddress = request.ServerVariables["HTTP_X_FORWARDED_FOR"];
if (!string.IsNullOrEmpty(ipAddress))
{
// It might contain a list of IPs, the first one is the original client IP
string[] addresses = ipAddress.Split(',');
if (addresses.Length != 0)
{
ipAddress = addresses[0].Trim();
}
}
// Fallback to the standard UserHostAddress if X-Forwarded-For is empty or invalid
if (string.IsNullOrEmpty(ipAddress) || IsPrivateIpAddress(ipAddress))
{
ipAddress = request.UserHostAddress;
}
return ipAddress;
}
/// <summary>
/// Helper method to check if an IP address is a private IP range (optional but good practice).
/// </summary>
private static bool IsPrivateIpAddress(string ipAddress)
{
// Simple check for common private IP ranges (e.g., 10.0.0.0/8, 192.168.0.0/16)
if (string.IsNullOrEmpty(ipAddress)) return false;
if (ipAddress.StartsWith("10.")) return true;
if (ipAddress.StartsWith("192.168.")) return true;
// Add other checks if necessary for your network setup
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment