Skip to content

Instantly share code, notes, and snippets.

@abergs
Last active March 21, 2017 17:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save abergs/9334586 to your computer and use it in GitHub Desktop.
Save abergs/9334586 to your computer and use it in GitHub Desktop.
AllowOneConcurrent
using System;
using System.Net;
using System.Net.Http;
using System.Web;
using System.Web.Caching;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
/// <summary>
/// Decorates any Action that needs to have client requests limited by concurrent requests.
/// </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 AllowOneConcurrentAttribute : ActionFilterAttribute
{
public AllowOneConcurrentAttribute(string name)
{
this.Name = name;
}
/// <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" or name-authroizationtoken
/// </remarks>
public string Name { get; set; }
/// <summary>
/// Timeout should not matter, but might as well be set to clear out the cache.
/// </summary>
private const int TIMEOUT = 120;
/// <summary>
/// A text message that will be sent to the client upon throttling.
/// </summary>
public string Message { get; set; }
private static object _thisLock = new object();
public override void OnActionExecuting(HttpActionContext c)
{
var key = string.Concat("ConcurrentThrottler", Name, "-", GetClientIp(c.Request);
var allowExecute = false;
if (HttpRuntime.Cache[key] == null)
{
lock (_thisLock)
{
if (HttpRuntime.Cache[key] == null)
{
HttpRuntime.Cache.Add(key,
true, // is this the smallest data we can have?
null, // no dependencies
DateTime.Now.AddSeconds(TIMEOUT), // absolute expiration
Cache.NoSlidingExpiration,
CacheItemPriority.Low,
null); // no callback
allowExecute = true;
}
}
}
if (!allowExecute)
{
if (String.IsNullOrEmpty(Message))
Message = "You may only perform one concurrect request for this endpoint";
c.Response = c.Request.CreateErrorResponse(HttpStatusCode.Conflict, Message);
// see 409 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
}
}
public override void OnActionExecuted(HttpActionExecutedContext c)
{
var key = string.Concat("ConcurrentThrottler", Name, "-", GetClientIp(c.Request);
if (HttpRuntime.Cache[key] != null)
{
HttpRuntime.Cache.Remove(key);
}
}
private string GetClientIp(HttpRequestMessage request)
{
if (request.Properties.ContainsKey("MS_HttpContext"))
{
return ((HttpContextWrapper)request.Properties["MS_HttpContext"]).Request.UserHostAddress;
}
else if (request.Properties.ContainsKey(RemoteEndpointMessageProperty.Name))
{
RemoteEndpointMessageProperty prop;
prop = (RemoteEndpointMessageProperty)this.Request.Properties[RemoteEndpointMessageProperty.Name];
return prop.Address;
}
else
{
return null;
}
}
}
@MattHuntington
Copy link

There's a race condition because you're storing the cache key as a class level variable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment