I have a system where a user may do a request 3 times within a given timeframe. How do I program that in C#?
I know I could just give User 3 properties (DateTime1
, DateTime2
, DateTime3
), and check on those, but that feels wrong. Is there a smarter way to do it? I was also thinking about using a stack or queue, but can't figure out how.
It would be nice if both the timeframe and request-limit could be changed ad-hoc (so no individual properties).
Window is 60 minutes:
User 1 have used up the timeframe limit, and is denied access.
User 2 have not used up the limit, so is accepted.
The timeframe is, as mentioned, rolling.
You could define your own RollingTimeWindow
class which records the latest successful requests and only performs the request if the timing allows it.
You can test it using this code. Press ENTER to try to perform a request, it will only get executed if the time window allows it.
static void Main(string[] args) {
//Only allow 3 requests in a time window of 10 seconds.
var r = new RollingTimeWindow(3, TimeSpan.FromSeconds(10), () => Console.WriteLine("Executed."));
while (true) {
Console.ReadLine(); //wait for press on Enter
r.PerformRequest();
}
}
This is a simple code to get you started to handle your rolling time window:
public class RollingTimeWindow {
private Queue<DateTime> _latestRequests = new Queue<DateTime>();
private readonly int _maxNumberOfRequests;
private readonly TimeSpan _windowSize;
private Action _action;
/// <summary>
/// Creates a new RollingTimeWindow performing requests if allowed by the time frame.
/// </summary>
/// <param name="maximumNumberOfRequests">Number of requests allowed in the window size</param>
/// <param name="windowSize">Rolling window size</param>
/// <param name="requestAction">Action to invoke when the request must be performed</param>
public RollingTimeWindow(int maximumNumberOfRequests, TimeSpan windowSize, Action requestAction = default) {
_action = requestAction;
_maxNumberOfRequests = maximumNumberOfRequests;
if (_maxNumberOfRequests < 0) {
throw new ArgumentException(nameof(maximumNumberOfRequests));
}
_windowSize = windowSize;
}
//Returns true if the request would be allowed, else false (NOT threadsafe)
public bool IsRequestAllowed() {
CleanQueue(DateTime.Now);
return _latestRequests.Count < _maxNumberOfRequests;
}
//Returns true if the request was performed, else false.
public bool PerformRequest() {
var now = DateTime.Now;
CleanQueue(now);
if (_latestRequests.Count < _maxNumberOfRequests) {
_latestRequests.Enqueue(now);
_action(); //perform the actual request
return true;
} else {
return false; //request not allowed
}
}
private void CleanQueue(DateTime now) {
//Cleans all requests older than the window size
while (_latestRequests.Count > 0 && _latestRequests.Peek() < now - _windowSize) {
_latestRequests.Dequeue();
}
}
}
Note that this is not thread-safe, and that it could also be implemented with Timer
objects to automatically dequeue older values.