diff --git a/RateLimiter.Tests/RateLimitRuleTests.cs b/RateLimiter.Tests/RateLimitRuleTests.cs new file mode 100644 index 00000000..8d0de69c --- /dev/null +++ b/RateLimiter.Tests/RateLimitRuleTests.cs @@ -0,0 +1,54 @@ + +using NUnit.Framework; +using RateLimiter.Rules; +using System; +using System.Collections.Generic; + +namespace RateLimiter.Tests +{ + [TestFixture] + public class RateLimitRuleTests + { + [Test] + public void Should_AllowRequestsWithinLimit() + { + var rule = new XRequestsPerTimespanRule(3, TimeSpan.FromSeconds(10)); + var clientId = "test-client"; + var resource = "test-resource"; + + + Assert.That(rule.IsRequestAllowed(clientId, resource), Is.True, "The request should be allowed."); + Assert.That(rule.IsRequestAllowed(clientId, resource), Is.True, "The request should be allowed."); + Assert.That(rule.IsRequestAllowed(clientId, resource), Is.True, "The request should be allowed."); + Assert.That(rule.IsRequestAllowed(clientId, resource), Is.False, "The request should not be allowed."); + + } + + [Test] + public void ClientRateLimit_AllowsWithinLimit() + { + var rule = new ClientRateLimitRule(5, TimeSpan.FromMinutes(1)); + var clientId = "client1"; + + for (int i = 0; i < 5; i++) + { + Assert.That(rule.IsRequestAllowed(clientId, "resource1", "127.0.0.1"), Is.True, "Request should be allowed within the limit."); + } + + Assert.That(rule.IsRequestAllowed(clientId, "resource1", "127.0.0.1"), Is.False, "Request should be denied after exceeding the limit."); + } + + [Test] + public void ResourceRateLimit_AllowsWithinLimit() + { + var resourceLimits = new Dictionary { { "resource1", 2 } }; + var rule = new ResourceRateLimitRule(resourceLimits); + + Assert.That(rule.IsRequestAllowed("client1", "resource1", "127.0.0.1"), Is.True); + Assert.That(rule.IsRequestAllowed("client1", "resource1", "127.0.0.1"), Is.True); + Assert.That(rule.IsRequestAllowed("client1", "resource1", "127.0.0.1"), Is.False); + } + + + } +} diff --git a/RateLimiter.Tests/RateLimiter.Tests.csproj b/RateLimiter.Tests/RateLimiter.Tests.csproj index 5cbfc4e8..34c80d6c 100644 --- a/RateLimiter.Tests/RateLimiter.Tests.csproj +++ b/RateLimiter.Tests/RateLimiter.Tests.csproj @@ -8,8 +8,8 @@ - - - + + + \ No newline at end of file diff --git a/RateLimiter.Tests/RateLimiterTest.cs b/RateLimiter.Tests/RateLimiterTest.cs deleted file mode 100644 index 172d44a7..00000000 --- a/RateLimiter.Tests/RateLimiterTest.cs +++ /dev/null @@ -1,13 +0,0 @@ -using NUnit.Framework; - -namespace RateLimiter.Tests; - -[TestFixture] -public class RateLimiterTest -{ - [Test] - public void Example() - { - Assert.That(true, Is.True); - } -} \ No newline at end of file diff --git a/RateLimiter/Interfaces/IRateLimitRule.cs b/RateLimiter/Interfaces/IRateLimitRule.cs new file mode 100644 index 00000000..ffa2f529 --- /dev/null +++ b/RateLimiter/Interfaces/IRateLimitRule.cs @@ -0,0 +1,9 @@ + +namespace RateLimiter.Interfaces +{ + public interface IRateLimitRule + { + bool IsRequestAllowed(string clientId, string resource); + bool IsRequestAllowed(string clientId, string resource, string ip); + } +} diff --git a/RateLimiter/Models/RequestLog.cs b/RateLimiter/Models/RequestLog.cs new file mode 100644 index 00000000..3c010b85 --- /dev/null +++ b/RateLimiter/Models/RequestLog.cs @@ -0,0 +1,12 @@ + +using System; + +namespace RateLimiter.Models +{ + public class RequestLog + { + public string ClientId { get; set; } + public string Resource { get; set; } + public DateTime Timestamp { get; set; } + } +} diff --git a/RateLimiter/Program.cs b/RateLimiter/Program.cs new file mode 100644 index 00000000..e9d44235 --- /dev/null +++ b/RateLimiter/Program.cs @@ -0,0 +1,27 @@ + +using RateLimiter.Rules; +using System; +using System.Threading; + +namespace RateLimiter +{ + public class Program + { + public static void Main(string[] args) + { + var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromSeconds(10)); + var clientId = "client1"; + var resource = "api/resource"; + + Console.WriteLine("Rate Limiter Console App"); + Console.WriteLine("Testing rate limiter with 7 requests..."); + + for (int i = 0; i < 7; i++) + { + var allowed = rule.IsRequestAllowed(clientId, resource); + Console.WriteLine($"Request {i + 1}: {(allowed ? "Allowed" : "Blocked")}"); + Thread.Sleep(1000); // Simulate 1-second delay between requests + } + } + } +} diff --git a/RateLimiter/RateLimiter.csproj b/RateLimiter/RateLimiter.csproj index 19962f52..4da26359 100644 --- a/RateLimiter/RateLimiter.csproj +++ b/RateLimiter/RateLimiter.csproj @@ -1,7 +1,22 @@  - - net6.0 - latest - enable - - \ No newline at end of file + + net6.0 + latest + enable + Exe + RateLimiter.Program + + + + + + + + + + + + + + + diff --git a/RateLimiter/Rules/ClientRateLimitRule.cs b/RateLimiter/Rules/ClientRateLimitRule.cs new file mode 100644 index 00000000..3b4d13e1 --- /dev/null +++ b/RateLimiter/Rules/ClientRateLimitRule.cs @@ -0,0 +1,47 @@ +using RateLimiter.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RateLimiter.Rules +{ + public class ClientRateLimitRule : IRateLimitRule + { + private readonly int _requestLimit; + private readonly TimeSpan _timeWindow; + private readonly Dictionary> _clientRequests = new(); + + public ClientRateLimitRule(int requestLimit, TimeSpan timeWindow) + { + _requestLimit = requestLimit; + _timeWindow = timeWindow; + } + + public bool IsRequestAllowed(string clientId, string resource, string ip) + { + if (!_clientRequests.ContainsKey(clientId)) + _clientRequests[clientId] = new Queue(); + + var requests = _clientRequests[clientId]; + var now = DateTime.UtcNow; + + while (requests.Count > 0 && requests.Peek() <= now - _timeWindow) + requests.Dequeue(); + + if (requests.Count < _requestLimit) + { + requests.Enqueue(now); + return true; + } + + return false; + } + + public bool IsRequestAllowed(string clientId, string resource) + { + return this.IsRequestAllowed(clientId, resource, string.Empty); + } + } +} diff --git a/RateLimiter/Rules/ResourceRateLimitRule.cs b/RateLimiter/Rules/ResourceRateLimitRule.cs new file mode 100644 index 00000000..9b016458 --- /dev/null +++ b/RateLimiter/Rules/ResourceRateLimitRule.cs @@ -0,0 +1,48 @@ +using RateLimiter.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RateLimiter.Rules +{ + public class ResourceRateLimitRule : IRateLimitRule + { + private readonly Dictionary _resourceLimits; + private readonly Dictionary> _resourceRequests = new(); + + public ResourceRateLimitRule(Dictionary resourceLimits) + { + _resourceLimits = resourceLimits; + } + + public bool IsRequestAllowed(string clientId, string resource, string ip) + { + if (!_resourceLimits.ContainsKey(resource)) + return true; + + if (!_resourceRequests.ContainsKey(resource)) + _resourceRequests[resource] = new Queue(); + + var requests = _resourceRequests[resource]; + var now = DateTime.UtcNow; + + while (requests.Count > 0 && requests.Peek() <= now - TimeSpan.FromMinutes(1)) + requests.Dequeue(); + + if (requests.Count < _resourceLimits[resource]) + { + requests.Enqueue(now); + return true; + } + + return false; + } + + public bool IsRequestAllowed(string clientId, string resource) + { + return this.IsRequestAllowed(clientId, resource, string.Empty); + } + } +} diff --git a/RateLimiter/Rules/XRequestsPerTimespanRule.cs b/RateLimiter/Rules/XRequestsPerTimespanRule.cs new file mode 100644 index 00000000..e22caf23 --- /dev/null +++ b/RateLimiter/Rules/XRequestsPerTimespanRule.cs @@ -0,0 +1,51 @@ + +using RateLimiter.Interfaces; +using RateLimiter.Models; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace RateLimiter.Rules +{ + public class XRequestsPerTimespanRule : IRateLimitRule + { + private readonly int _maxRequests; + private readonly TimeSpan _timespan; + private readonly ConcurrentDictionary> _requestLogs; + + public XRequestsPerTimespanRule(int maxRequests, TimeSpan timespan) + { + _maxRequests = maxRequests; + _timespan = timespan; + _requestLogs = new ConcurrentDictionary>(); + } + + public bool IsRequestAllowed(string clientId, string resource) + { + var key = $"{clientId}:{resource}"; + var now = DateTime.UtcNow; + + _requestLogs.TryAdd(key, new List()); + var requestLog = _requestLogs[key]; + + lock (requestLog) + { + requestLog.RemoveAll(timestamp => (now - timestamp) > _timespan); + + if (requestLog.Count < _maxRequests) + { + requestLog.Add(now); + return true; + } + + return false; + } + } + + public bool IsRequestAllowed(string clientId, string resource, string ip) + { + + return this.IsRequestAllowed(clientId, resource, string.Empty); + } + } +} diff --git a/RateLimiter/Utilities/DefaultTimeProvider.cs b/RateLimiter/Utilities/DefaultTimeProvider.cs new file mode 100644 index 00000000..3cc4f3e7 --- /dev/null +++ b/RateLimiter/Utilities/DefaultTimeProvider.cs @@ -0,0 +1,15 @@ + +using System; + +namespace RateLimiter.Utilities +{ + public interface ITimeProvider + { + DateTime GetCurrentTime(); + } + + public class DefaultTimeProvider : ITimeProvider + { + public DateTime GetCurrentTime() => DateTime.UtcNow; + } +}