Last active
March 21, 2017 17:01
-
-
Save abergs/9334586 to your computer and use it in GitHub Desktop.
AllowOneConcurrent
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.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; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
There's a race condition because you're storing the cache key as a class level variable.