Created
December 8, 2025 19:38
-
-
Save jpolvora/1dd827cc2529c71b5c02d4f305d794de to your computer and use it in GitHub Desktop.
CustomRateLimiterAttribute for ASP.NET 4 MVC 5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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