In a scenario where the Web API being accessed requires authorization, the suggestions that utilize ActionFilterAttribute will not actually limit a client contacting an API that has not been authorized. The client can keep calling the api without any throttling.
The WebApiThrottling project uses a DelegatingHandler to overcome this. The following is an example DelegatingHandler that basically does the same thing as the other answers that use an ActionFilterAttribute. The added benefit is that it will work for authorized and unauthorized clients.
public enum TimeUnit
{
Minute = 60,
Hour = 3600,
Day = 86400
}
public class ThrottleHandler : DelegatingHandler
{
private class Error
{
public string Message;
}
private TimeUnit _timeUnit;
private int _count;
public ThrottleHandler(TimeUnit unit, int count)
{
_timeUnit = unit;
_count = count;
}
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
var seconds = Convert.ToInt32(TimeUnit);
var key = string.Join(
"-",
seconds,
request.Method,
request.RequestUri.AbsolutePath,
GetClientIpAddress(request)
);
// increment the cache value
var cnt = 1;
if (HttpRuntime.Cache[key] != null)
{
cnt = (int)HttpRuntime.Cache[key] + 1;
}
HttpRuntime.Cache.Insert(
key,
cnt,
null,
DateTime.UtcNow.AddSeconds(seconds),
Cache.NoSlidingExpiration,
CacheItemPriority.Low,
null
);
if (cnt > _count)
{
// break out of execution
var response = request.CreateResponse((HttpStatusCode)429, new Error() { Message = "API call quota exceeded! {Count} calls per {TimeUnit} allowed." });
return Task.FromResult(response);
}
return base.SendAsync(request, cancellationToken);
}
private string GetClientIpAddress(HttpRequestMessage request)
{
if (request.Properties.ContainsKey("MS_HttpContext"))
{
return ((HttpContextWrapper)request.Properties["MS_HttpContext"]).Request.UserHostAddress;
}
if (request.Properties.ContainsKey(RemoteEndpointMessageProperty.Name))
{
RemoteEndpointMessageProperty prop = (RemoteEndpointMessageProperty)request.Properties[RemoteEndpointMessageProperty.Name];
return prop.Address;
}
if (HttpContext.Current != null)
{
return HttpContext.Current.Request.UserHostAddress;
}
return String.Empty;
}
}
this.Request
to justrequest
in theGetClientIp
method, right? Otherwise I get 'cannot resolve symbol'. This is a huge help, thank you very much. – Woodchuck