I\'m trying to implement request throttling via the following:
Best way to implement request throttling in ASP.NET MVC?
I\'ve pulled that code into my solut
I am using ThrottleAttribute
to limit the calling rate of my short-message sending API, but I found it not working sometimes. API may been called many times until the throttle logic works, finally I am using System.Web.Caching.MemoryCache
instead of HttpRuntime.Cache
and the problem seems to solved.
if (MemoryCache.Default[key] == null)
{
MemoryCache.Default.Set(key, true, DateTime.Now.AddSeconds(Seconds));
allowExecute = true;
}
My 2 cents is add some extra info for 'key' about the request info on parameters, so that different paramter request is allowed from the same IP.
key = Name + clientIP + actionContext.ActionArguments.Values.ToString()
Also, my little concern about the 'clientIP', is it possible that two different user use the same ISP has the same 'clientIP'? If yes, then one client my be throttled wrongly.
You seem to be confusing action filters for an ASP.NET MVC controller and action filters for an ASP.NET Web API controller. Those are 2 completely different classes:
It appears that what you have shown is a Web API controller action (one that is declared inside a controller deriving from ApiController
). So if you want to apply custom filters to it, they must derive from System.Web.Http.Filters.ActionFilterAttribute
.
So let's go ahead and adapt the code for Web API:
public class ThrottleAttribute : ActionFilterAttribute
{
/// <summary>
/// A unique name for this Throttle.
/// </summary>
/// <remarks>
/// We'll be inserting a Cache record based on this name and client IP, e.g. "Name-192.168.0.1"
/// </remarks>
public string Name { get; set; }
/// <summary>
/// The number of seconds clients must wait before executing this decorated route again.
/// </summary>
public int Seconds { get; set; }
/// <summary>
/// A text message that will be sent to the client upon throttling. You can include the token {n} to
/// show this.Seconds in the message, e.g. "Wait {n} seconds before trying again".
/// </summary>
public string Message { get; set; }
public override void OnActionExecuting(HttpActionContext actionContext)
{
var key = string.Concat(Name, "-", GetClientIp(actionContext.Request));
var allowExecute = false;
if (HttpRuntime.Cache[key] == null)
{
HttpRuntime.Cache.Add(key,
true, // is this the smallest data we can have?
null, // no dependencies
DateTime.Now.AddSeconds(Seconds), // absolute expiration
Cache.NoSlidingExpiration,
CacheItemPriority.Low,
null); // no callback
allowExecute = true;
}
if (!allowExecute)
{
if (string.IsNullOrEmpty(Message))
{
Message = "You may only perform this action every {n} seconds.";
}
actionContext.Response = actionContext.Request.CreateResponse(
HttpStatusCode.Conflict,
Message.Replace("{n}", Seconds.ToString())
);
}
}
}
where the GetClientIp
method comes from this post.
Now you can use this attribute on your Web API controller action.
The proposed solution is not accurate. There are at least 5 reasons for it.
There are many more issues and hidden obstacles to solve while implementing the throttling. There are free open source options available. I recommend to look at https://throttlewebapi.codeplex.com/, for example.
Double check the using
statements in your action filter. As you're using an API controller, ensure that you are referencing the ActionFilterAttribute in System.Web.Http.Filters
and not the one in System.Web.Mvc
.
using System.Web.Http.Filters;
It is very easily solved in .NET Core. In this case, I used IMemoryCache, which is 'in-memory per service'. However, if you want it based on Redis e.g. just change the interface to IDistributedCache… (make sure you configure Redis of course)
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Net;
namespace My.ActionFilters
{
/// <summary>
/// Decorates any MVC route that needs to have client requests limited by time.
/// </summary>
/// <remarks>
/// Uses the current System.Web.Caching.Cache to store each client request to the decorated route.
/// </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ThrottleFilterAttribute : ActionFilterAttribute
{
public ThrottleFilterAttribute()
{
}
/// <summary>
/// A unique name for this Throttle.
/// </summary>
/// <remarks>
/// We'll be inserting a Cache record based on this name and client IP, e.g. "Name-192.168.0.1"
/// </remarks>
public string Name { get; set; }
/// <summary>
/// The number of seconds clients must wait before executing this decorated route again.
/// </summary>
public int Seconds { get; set; }
/// <summary>
/// A text message that will be sent to the client upon throttling. You can include the token {n} to
/// show this.Seconds in the message, e.g. "Wait {n} seconds before trying again".
/// </summary>
public string Message { get; set; }
public override void OnActionExecuting(ActionExecutingContext c)
{
var memCache = (IMemoryCache)c.HttpContext.RequestServices.GetService(typeof(IMemoryCache));
var testProxy = c.HttpContext.Request.Headers.ContainsKey("X-Forwarded-For");
var key = 0;
if (testProxy)
{
var ipAddress = IPAddress.TryParse(c.HttpContext.Request.Headers["X-Forwarded-For"], out IPAddress realClient);
if (ipAddress)
{
key = realClient.GetHashCode();
}
}
if (key != 0)
{
key = c.HttpContext.Connection.RemoteIpAddress.GetHashCode();
}
memCache.TryGetValue(key, out bool forbidExecute);
memCache.Set(key, true, new MemoryCacheEntryOptions() { SlidingExpiration = TimeSpan.FromMilliseconds(Milliseconds) });
if (forbidExecute)
{
if (String.IsNullOrEmpty(Message))
Message = $"You may only perform this action every {Milliseconds}ms.";
c.Result = new ContentResult { Content = Message, ContentType = "text/plain" };
// see 409 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
c.HttpContext.Response.StatusCode = StatusCodes.Status409Conflict;
}
}
}
}