From ac5e3724b2def71150aa267e358fbfb35470759a Mon Sep 17 00:00:00 2001 From: VassilSanych Date: Thu, 30 Jan 2025 20:14:28 +0400 Subject: [PATCH 1/3] Solved --- RateLimiter.Tests/RateLimiterTest.cs | 177 +++++++++++++++++- .../TimespanSinceLastCallRuleTests.cs | 71 +++++++ .../XRequestsPerTimespanRuleTests.cs | 105 +++++++++++ RateLimiter.sln.DotSettings.user | 4 + RateLimiter/BaseRule.cs | 61 ++++++ RateLimiter/RateLimiter.cs | 111 +++++++++++ RateLimiter/RequestLogEntry.cs | 17 ++ RateLimiter/TimespanSinceLastCallRule.cs | 40 ++++ RateLimiter/XRequestsPerTimespanRule.cs | 39 ++++ 9 files changed, 615 insertions(+), 10 deletions(-) create mode 100644 RateLimiter.Tests/TimespanSinceLastCallRuleTests.cs create mode 100644 RateLimiter.Tests/XRequestsPerTimespanRuleTests.cs create mode 100644 RateLimiter.sln.DotSettings.user create mode 100644 RateLimiter/BaseRule.cs create mode 100644 RateLimiter/RateLimiter.cs create mode 100644 RateLimiter/RequestLogEntry.cs create mode 100644 RateLimiter/TimespanSinceLastCallRule.cs create mode 100644 RateLimiter/XRequestsPerTimespanRule.cs diff --git a/RateLimiter.Tests/RateLimiterTest.cs b/RateLimiter.Tests/RateLimiterTest.cs index 172d44a7..932be927 100644 --- a/RateLimiter.Tests/RateLimiterTest.cs +++ b/RateLimiter.Tests/RateLimiterTest.cs @@ -1,13 +1,170 @@ -using NUnit.Framework; - +using System.Collections.Generic; +using System.Linq; +using System; + +using NUnit.Framework; +using System.Collections.Concurrent; namespace RateLimiter.Tests; [TestFixture] -public class RateLimiterTest -{ - [Test] - public void Example() - { - Assert.That(true, Is.True); - } -} \ No newline at end of file +public class RateLimiterTests +{ + private RateLimiter _rateLimiter; + + [SetUp] + public void Setup() + { + _rateLimiter = new RateLimiter(); + } + + + + [Test] + public void IsRequestAllowed_ShouldReturnTrue_WhenAllRulesAllow() + { + // Arrange + var rule = new MockRateLimitingRule { IsAllowed = true }; + _rateLimiter.AddGlobalRule(rule); + + // Act + var result = _rateLimiter.IsRequestAllowed("resource1", "client1", null); + + // Assert + Assert.IsTrue(result); + } + + [Test] + public void IsRequestAllowed_ShouldReturnFalse_WhenAnyRuleDisallows() + { + // Arrange + var rule = new MockRateLimitingRule { IsAllowed = false }; + _rateLimiter.AddGlobalRule(rule); + + // Act + var result = _rateLimiter.IsRequestAllowed("resource1", "client1", null); + + // Assert + Assert.IsFalse(result); + } + + [Test] + public void GetRequestLog_ShouldReturnLogEntries() + { + // Arrange + var rule = new MockRateLimitingRule { IsAllowed = true }; + _rateLimiter.AddGlobalRule(rule); + _rateLimiter.IsRequestAllowed("resource1", "client1", null); + + // Act + var log = _rateLimiter.GetRequestLog(); + + // Assert + Assert.AreEqual(1, log.Count()); + } + } + + public class MockRateLimitingRule : BaseRule + { + public bool IsAllowed { get; set; } + public ConcurrentQueue CommonLog { get; set; } + + public override bool IsRequestAllowed(string clientId, Dictionary? factors) + { + return IsAllowed; + } + } + +//[TestFixture] +//public class RateLimiterTest +//{ +// [Test] +// public void Example() +// { +// Assert.That(true, Is.True); +// } + + +// private RateLimiter _rateLimiter; + +// [SetUp] +// public void SetUp() +// { +// _rateLimiter = new RateLimiter(); +// } + +// [Test] +// public void XRequestsPerTimespanRule_AllowsRequestsWithinLimit() +// { +// var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromMinutes(1)); +// _rateLimiter.AddGlobalRule(rule); + +// var factors = new Dictionary(); + +// for (int i = 0; i < 5; i++) +// { +// Assert.IsTrue(_rateLimiter.IsRequestAllowed("resource1", "client1", factors)); +// } + +// Assert.IsFalse(_rateLimiter.IsRequestAllowed("resource1", "client1", factors)); +// } + +// [Test] +// public void TimespanSinceLastCallRule_AllowsRequestAfterTimespan() +// { +// var rule = new TimespanSinceLastCallRule(TimeSpan.FromSeconds(1)); +// _rateLimiter.AddGlobalRule(rule); + +// var factors = new Dictionary(); + +// Assert.IsTrue(_rateLimiter.IsRequestAllowed("resource1", "client1", factors)); +// Assert.IsFalse(_rateLimiter.IsRequestAllowed("resource1", "client1", factors)); + +// System.Threading.Thread.Sleep(1000); + +// Assert.IsTrue(_rateLimiter.IsRequestAllowed("resource1", "client1", factors)); +// } + +// [Test] +// public void RegionBasedRule_AllowsRequestsBasedOnRegion() +// { +// var rule = new RegionBasedRule(5, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(5)); +// _rateLimiter.AddGlobalRule(rule); + +// var usFactors = new Dictionary { { "region", "US" } }; +// var euFactors = new Dictionary { { "region", "EU" } }; + +// for (int i = 0; i < 5; i++) +// { +// Assert.IsTrue(_rateLimiter.IsRequestAllowed("resource1", "client1", usFactors)); +// } + +// Assert.IsFalse(_rateLimiter.IsRequestAllowed("resource1", "client1", usFactors)); + +// Assert.IsTrue(_rateLimiter.IsRequestAllowed("resource1", "client2", euFactors)); +// Assert.IsFalse(_rateLimiter.IsRequestAllowed("resource1", "client2", euFactors)); + +// System.Threading.Thread.Sleep(5000); + +// Assert.IsTrue(_rateLimiter.IsRequestAllowed("resource1", "client2", euFactors)); +// } + +// [Test] +// public void RateLimiter_LogsRequests() +// { +// var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromMinutes(1)); +// _rateLimiter.AddGlobalRule(rule); + +// var factors = new Dictionary(); + +// for (int i = 0; i < 5; i++) +// { +// _rateLimiter.IsRequestAllowed("resource1", "client1", factors); +// } + +// var log = _rateLimiter.GetRequestLog(); + +// Assert.AreEqual(5, log.Count()); +// Assert.IsTrue(log.All(entry => entry.ClientId == "client1" && entry.Resource == "resource1")); +// } +//} + diff --git a/RateLimiter.Tests/TimespanSinceLastCallRuleTests.cs b/RateLimiter.Tests/TimespanSinceLastCallRuleTests.cs new file mode 100644 index 00000000..43e6cfd1 --- /dev/null +++ b/RateLimiter.Tests/TimespanSinceLastCallRuleTests.cs @@ -0,0 +1,71 @@ +using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources; + +using NUnit.Framework; + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace RateLimiter.Tests +{ + [TestFixture] + public class TimespanSinceLastCallRuleTests + { + private TimespanSinceLastCallRule _rule; + private Dictionary? _factors; + private ConcurrentQueue _log; + + [SetUp] + public void Setup() + { + _rule = new(TimeSpan.FromSeconds(0.1)); + _factors = new() { {"k", "v" } }; + _log = new(); + _rule.CommonLog = _log; + } + + [Test] + public void IsRequestAllowed_FirstRequest_ReturnsTrue() + { + var result = _rule.IsRequestAllowed("client1", _factors); + Assert.IsTrue(result); + } + + [Test] + public void IsRequestAllowed_RequestWithinTimespan_ReturnsFalse() + { + var isAllowed = _rule.IsRequestAllowed("client1", _factors); // First request + System.Threading.Thread.Sleep(10); // Wait for 0.01 seconds + + var entry = new RequestLogEntry + { + ClientId = "client1", + Resource = "resource", + Timestamp = DateTime.UtcNow, + IsAllowed = isAllowed, + Factors = new(_factors) + }; + + _log.Enqueue(entry); + + + + var result = _rule.IsRequestAllowed("client1", _factors); + Assert.IsFalse(result); + } + + [Test] + public void IsRequestAllowed_RequestAfterTimespan_ReturnsTrue() + { + + + + + _rule.IsRequestAllowed("client1", _factors); // First request + System.Threading.Thread.Sleep(110); // Wait for 0.11 seconds + + var result = _rule.IsRequestAllowed("client1", _factors); + Assert.IsTrue(result); + } + } +} \ No newline at end of file diff --git a/RateLimiter.Tests/XRequestsPerTimespanRuleTests.cs b/RateLimiter.Tests/XRequestsPerTimespanRuleTests.cs new file mode 100644 index 00000000..1522a341 --- /dev/null +++ b/RateLimiter.Tests/XRequestsPerTimespanRuleTests.cs @@ -0,0 +1,105 @@ +using NUnit.Framework; + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace RateLimiter.Tests +{ + [TestFixture] + public class XRequestsPerTimespanRuleTests + { + //private TimespanSinceLastCallRule _rule; + private Dictionary? _factors; + private ConcurrentQueue _log; + + [SetUp] + public void Setup() + { + //_rule = new(TimeSpan.FromSeconds(0.1)); + _factors = new() { { "k", "v" } }; + _log = new(); + //_rule.CommonLog = _log; + } + + + [Test] + public void IsRequestAllowed_FirstRequest_ReturnsTrue() + { + var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromSeconds(0.1)); + rule.CommonLog = _log; + var result = rule.IsRequestAllowed("client1", null); + Assert.IsTrue(result); + } + + [Test] + public void IsRequestAllowed_WithinLimit_ReturnsTrue() + { + var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromSeconds(0.1)); + rule.CommonLog = _log; + for (int i = 0; i < 4; i++) + { + var isAllowed = rule.IsRequestAllowed("client1", _factors); + var entry = new RequestLogEntry + { + ClientId = "client1", + Resource = "resource", + Timestamp = DateTime.UtcNow, + IsAllowed = isAllowed, + Factors = new(_factors) + }; + + _log.Enqueue(entry); + } + var result = rule.IsRequestAllowed("client1", null); + Assert.IsTrue(result); + } + + [Test] + public void IsRequestAllowed_ExceedsLimit_ReturnsFalse() + { + var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromSeconds(0.1)); + rule.CommonLog = _log; + for (int i = 0; i < 5; i++) + { + var isAllowed = rule.IsRequestAllowed("client1", _factors); + var entry = new RequestLogEntry + { + ClientId = "client1", + Resource = "resource", + Timestamp = DateTime.UtcNow, + IsAllowed = isAllowed, + Factors = new(_factors) + }; + + _log.Enqueue(entry); + } + var result = rule.IsRequestAllowed("client1", null); + Assert.IsFalse(result); + } + + [Test] + public void IsRequestAllowed_AfterTimespan_ReturnsTrue() + { + var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromSeconds(0.1)); + rule.CommonLog = _log; + for (int i = 0; i < 5; i++) + { + var isAllowed = rule.IsRequestAllowed("client1", _factors); + var entry = new RequestLogEntry + { + ClientId = "client1", + Resource = "resource", + Timestamp = DateTime.UtcNow, + IsAllowed = isAllowed, + Factors = new(_factors) + }; + + _log.Enqueue(entry); + } + System.Threading.Thread.Sleep(TimeSpan.FromSeconds(0.1)); + var result = rule.IsRequestAllowed("client1", null); + Assert.IsTrue(result); + } + } +} \ No newline at end of file diff --git a/RateLimiter.sln.DotSettings.user b/RateLimiter.sln.DotSettings.user new file mode 100644 index 00000000..d3459605 --- /dev/null +++ b/RateLimiter.sln.DotSettings.user @@ -0,0 +1,4 @@ + + <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &lt;RateLimiter.Tests&gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <Project Location="C:\Source\TulacoRateLimiter\RateLimiter.Tests" Presentation="&lt;RateLimiter.Tests&gt;" /> +</SessionState> \ No newline at end of file diff --git a/RateLimiter/BaseRule.cs b/RateLimiter/BaseRule.cs new file mode 100644 index 00000000..37d917ef --- /dev/null +++ b/RateLimiter/BaseRule.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; + +namespace RateLimiter; + + +/// +/// Interface for rate limiting rules +/// +public interface IRateLimitingRule +{ + bool IsRequestAllowed(string clientId, Dictionary? factors = null); + Dictionary? Factors { get; set; } + IEnumerable CommonLog { get; set; } +} + + +/// +/// Base class for rate limiting rules +/// +public abstract class BaseRule : IRateLimitingRule +{ + /// + /// Factors that can be used to determine if the rule is applicable + /// + public Dictionary? Factors { get; set; } + + + /// + /// Common log of requests + /// + public IEnumerable CommonLog { get; set; } + + /// + /// Check if the request is allowed + /// + /// + /// + /// + public virtual bool IsRequestAllowed(string clientId, Dictionary? factors) + { + // If factors are not set or are not used, the rule is not applicable + if (Factors != null && factors?.ContainsAllElements(Factors) != true) + return true; + return false; + } +} + + +/// +/// Extension methods for dictionaries +/// +public static class DictionaryComparer +{ + public static bool ContainsAllElements( + this Dictionary mainDict, + Dictionary subDict) + { + return subDict.All(kv => mainDict.ContainsKey(kv.Key) && EqualityComparer.Default.Equals(mainDict[kv.Key], kv.Value)); + } +} \ No newline at end of file diff --git a/RateLimiter/RateLimiter.cs b/RateLimiter/RateLimiter.cs new file mode 100644 index 00000000..7d6378ed --- /dev/null +++ b/RateLimiter/RateLimiter.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace RateLimiter; + + +/// +/// Class to limit the number of requests +/// +public class RateLimiter +{ + private readonly Dictionary> _resourceRules = new(); + private readonly List _globalRules = []; + private readonly ConcurrentQueue _requestLog = []; //todo: use cache + + /// + /// Time to keep the log entries + /// + public TimeSpan LogRetentionTime { get; set; } = TimeSpan.FromDays(1); //todo: make it configurable + + + /// + /// Add a rule for a specific resource + /// + /// + /// + public void AddRule(string resource, IRateLimitingRule rule) + { + if (!_resourceRules.ContainsKey(resource)) + _resourceRules[resource] = []; + + rule.CommonLog = _requestLog; + _resourceRules[resource].Add(rule); + } + + + /// + /// Add a global rule + /// + /// + public void AddGlobalRule(IRateLimitingRule rule) + { + _globalRules.Add(rule); + } + + + /// + /// Remove old entries from the log + /// + private void RemoveOldEntries() + { + var now = DateTime.UtcNow; + while (_requestLog.TryPeek(out var first)) + { + if (now - first.Timestamp <= LogRetentionTime) + break; + _requestLog.TryDequeue(out _); + } + } + + + /// + /// Check if a request is allowed + /// + /// + /// + /// + /// + public bool IsRequestAllowed(string resource, string clientId, Dictionary? factors) + { + var rulesToCheck = new List(_globalRules); + + if (_resourceRules.TryGetValue(resource, out var resourceRule)) + rulesToCheck.AddRange(resourceRule); + + RemoveOldEntries(); + + var isAllowed = rulesToCheck.All(rule => rule.IsRequestAllowed(clientId, factors)); + + var entry = new RequestLogEntry + { + ClientId = clientId, + Resource = resource, + Timestamp = DateTime.UtcNow, + IsAllowed = isAllowed, + }; + if (factors != null) + entry.Factors = new Dictionary(factors); + _requestLog.Enqueue(entry); + + return isAllowed; + } + + + /// + /// Get the request log + /// + /// + public IEnumerable? GetRequestLog() + { + return _requestLog?.ToArray(); + } + + +} + + + + diff --git a/RateLimiter/RequestLogEntry.cs b/RateLimiter/RequestLogEntry.cs new file mode 100644 index 00000000..1955b8ed --- /dev/null +++ b/RateLimiter/RequestLogEntry.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace RateLimiter +{ + /// + /// Class to limit the number of requests per timespan + /// + public class RequestLogEntry + { + public string ClientId { get; set; } + public string Resource { get; set; } + public DateTime Timestamp { get; set; } + public bool IsAllowed { get; set; } + public Dictionary? Factors { get; set; } + } +} diff --git a/RateLimiter/TimespanSinceLastCallRule.cs b/RateLimiter/TimespanSinceLastCallRule.cs new file mode 100644 index 00000000..8c156564 --- /dev/null +++ b/RateLimiter/TimespanSinceLastCallRule.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System; +using System.Linq; + +namespace RateLimiter; + +/// +/// Rule to limit the certain timespan has passed since the last call +/// +public class TimespanSinceLastCallRule(TimeSpan requiredTimespan) : BaseRule +{ + public override bool IsRequestAllowed(string clientId, Dictionary? factors) + { + if (base.IsRequestAllowed(clientId, factors)) + return true; + + var now = DateTime.UtcNow; + var lastDeniedRequest = CommonLog?.LastOrDefault(entry => + entry.ClientId == clientId + && entry.IsAllowed == false + && (Factors == null + || entry.Factors?.ContainsAllElements(Factors) == true)); + + // If the timespan from the last denied request has not passed yet, allow the request + if (lastDeniedRequest != null && now - lastDeniedRequest.Timestamp <= requiredTimespan) + return true; + + var lastRequest = CommonLog?.LastOrDefault(entry => + entry.ClientId == clientId + && (Factors == null + || entry.Factors?.ContainsAllElements(Factors) == true)); + + return lastRequest == null || now - lastRequest.Timestamp >= requiredTimespan; + } + +} + + + + diff --git a/RateLimiter/XRequestsPerTimespanRule.cs b/RateLimiter/XRequestsPerTimespanRule.cs new file mode 100644 index 00000000..9a7cac16 --- /dev/null +++ b/RateLimiter/XRequestsPerTimespanRule.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace RateLimiter; + +/// +/// This rule limits the number of requests a client can make within a specified timespan. +/// It checks the number of requests made by a client within the given timespan and denies further requests if the limit is exceeded. +/// +public class XRequestsPerTimespanRule(int maxRequests, TimeSpan timespan) : BaseRule +{ + public override bool IsRequestAllowed(string clientId, Dictionary? factors) + { + if (base.IsRequestAllowed(clientId, factors)) + return true; + + var now = DateTime.UtcNow; + var lastDeniedRequest = CommonLog?.LastOrDefault(entry => + entry.ClientId == clientId + && entry.IsAllowed == false + && (Factors == null + || entry.Factors?.ContainsAllElements(Factors) == true)); + + //first time of the log entry that should pass the timespan + var lastTime = now - timespan; + //chose the latest denied time as the start time for the count if it passes the timespan + if (lastDeniedRequest != null && lastDeniedRequest.Timestamp > lastTime) + lastTime = lastDeniedRequest.Timestamp; + + var requestsCount = CommonLog?.Count(entry => entry.ClientId == clientId && entry.Timestamp > lastTime); + + return requestsCount < maxRequests; + } + + +} + + From 04a156d68ba1d5699b586b25515a4cbd33218fd8 Mon Sep 17 00:00:00 2001 From: VassilSanych Date: Thu, 30 Jan 2025 20:21:39 +0400 Subject: [PATCH 2/3] Cleaned --- RateLimiter.Tests/RateLimiterTest.cs | 94 ---------------------------- 1 file changed, 94 deletions(-) diff --git a/RateLimiter.Tests/RateLimiterTest.cs b/RateLimiter.Tests/RateLimiterTest.cs index 932be927..ff11b021 100644 --- a/RateLimiter.Tests/RateLimiterTest.cs +++ b/RateLimiter.Tests/RateLimiterTest.cs @@ -17,8 +17,6 @@ public void Setup() _rateLimiter = new RateLimiter(); } - - [Test] public void IsRequestAllowed_ShouldReturnTrue_WhenAllRulesAllow() { @@ -74,97 +72,5 @@ public override bool IsRequestAllowed(string clientId, Dictionary(); - -// for (int i = 0; i < 5; i++) -// { -// Assert.IsTrue(_rateLimiter.IsRequestAllowed("resource1", "client1", factors)); -// } - -// Assert.IsFalse(_rateLimiter.IsRequestAllowed("resource1", "client1", factors)); -// } - -// [Test] -// public void TimespanSinceLastCallRule_AllowsRequestAfterTimespan() -// { -// var rule = new TimespanSinceLastCallRule(TimeSpan.FromSeconds(1)); -// _rateLimiter.AddGlobalRule(rule); - -// var factors = new Dictionary(); - -// Assert.IsTrue(_rateLimiter.IsRequestAllowed("resource1", "client1", factors)); -// Assert.IsFalse(_rateLimiter.IsRequestAllowed("resource1", "client1", factors)); - -// System.Threading.Thread.Sleep(1000); - -// Assert.IsTrue(_rateLimiter.IsRequestAllowed("resource1", "client1", factors)); -// } - -// [Test] -// public void RegionBasedRule_AllowsRequestsBasedOnRegion() -// { -// var rule = new RegionBasedRule(5, TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(5)); -// _rateLimiter.AddGlobalRule(rule); - -// var usFactors = new Dictionary { { "region", "US" } }; -// var euFactors = new Dictionary { { "region", "EU" } }; - -// for (int i = 0; i < 5; i++) -// { -// Assert.IsTrue(_rateLimiter.IsRequestAllowed("resource1", "client1", usFactors)); -// } - -// Assert.IsFalse(_rateLimiter.IsRequestAllowed("resource1", "client1", usFactors)); - -// Assert.IsTrue(_rateLimiter.IsRequestAllowed("resource1", "client2", euFactors)); -// Assert.IsFalse(_rateLimiter.IsRequestAllowed("resource1", "client2", euFactors)); - -// System.Threading.Thread.Sleep(5000); - -// Assert.IsTrue(_rateLimiter.IsRequestAllowed("resource1", "client2", euFactors)); -// } - -// [Test] -// public void RateLimiter_LogsRequests() -// { -// var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromMinutes(1)); -// _rateLimiter.AddGlobalRule(rule); - -// var factors = new Dictionary(); - -// for (int i = 0; i < 5; i++) -// { -// _rateLimiter.IsRequestAllowed("resource1", "client1", factors); -// } - -// var log = _rateLimiter.GetRequestLog(); -// Assert.AreEqual(5, log.Count()); -// Assert.IsTrue(log.All(entry => entry.ClientId == "client1" && entry.Resource == "resource1")); -// } -//} From c7b458f361597f7bc095331dd989c41d5a5787ce Mon Sep 17 00:00:00 2001 From: VassilSanych Date: Sun, 9 Mar 2025 19:26:09 +0400 Subject: [PATCH 3/3] Enhance RateLimiter with logging mechanism Incorporated a logging mechanism using `ConcurrentQueue` in the `RateLimiter` and its associated rules. Updated `MockRateLimitingRule`, `TimespanSinceLastCallRule`, and `XRequestsPerTimespanRule` to accept a log parameter in their constructors for better request tracking. Modified the `BaseRule` class to initialize its `CommonLog` property with the provided log. Updated test cases in `RateLimiterTest.cs` to align with the new logging structure. --- RateLimiter.Tests/RateLimiterTest.cs | 25 ++++++++++--------- .../TimespanSinceLastCallRuleTests.cs | 11 +++----- .../XRequestsPerTimespanRuleTests.cs | 14 ++++------- RateLimiter/BaseRule.cs | 11 ++++---- RateLimiter/RateLimiter.cs | 6 +---- RateLimiter/TimespanSinceLastCallRule.cs | 6 ++--- RateLimiter/XRequestsPerTimespanRule.cs | 4 +-- 7 files changed, 33 insertions(+), 44 deletions(-) diff --git a/RateLimiter.Tests/RateLimiterTest.cs b/RateLimiter.Tests/RateLimiterTest.cs index ff11b021..29b50065 100644 --- a/RateLimiter.Tests/RateLimiterTest.cs +++ b/RateLimiter.Tests/RateLimiterTest.cs @@ -1,27 +1,29 @@ using System.Collections.Generic; using System.Linq; -using System; - using NUnit.Framework; using System.Collections.Concurrent; -namespace RateLimiter.Tests; - -[TestFixture] +namespace RateLimiter.Tests; + +[TestFixture] public class RateLimiterTests { private RateLimiter _rateLimiter; + private Dictionary? _factors; + private ConcurrentQueue _log; - [SetUp] + [SetUp] public void Setup() { _rateLimiter = new RateLimiter(); - } + _factors = new() { { "k", "v" } }; + _log = new(); + } [Test] public void IsRequestAllowed_ShouldReturnTrue_WhenAllRulesAllow() { // Arrange - var rule = new MockRateLimitingRule { IsAllowed = true }; + var rule = new MockRateLimitingRule(_log) { IsAllowed = true }; _rateLimiter.AddGlobalRule(rule); // Act @@ -35,7 +37,7 @@ public void IsRequestAllowed_ShouldReturnTrue_WhenAllRulesAllow() public void IsRequestAllowed_ShouldReturnFalse_WhenAnyRuleDisallows() { // Arrange - var rule = new MockRateLimitingRule { IsAllowed = false }; + var rule = new MockRateLimitingRule(_log) { IsAllowed = false }; _rateLimiter.AddGlobalRule(rule); // Act @@ -49,7 +51,7 @@ public void IsRequestAllowed_ShouldReturnFalse_WhenAnyRuleDisallows() public void GetRequestLog_ShouldReturnLogEntries() { // Arrange - var rule = new MockRateLimitingRule { IsAllowed = true }; + var rule = new MockRateLimitingRule(_log) { IsAllowed = true }; _rateLimiter.AddGlobalRule(rule); _rateLimiter.IsRequestAllowed("resource1", "client1", null); @@ -61,10 +63,9 @@ public void GetRequestLog_ShouldReturnLogEntries() } } - public class MockRateLimitingRule : BaseRule + public class MockRateLimitingRule(IEnumerable log) : BaseRule(log) { public bool IsAllowed { get; set; } - public ConcurrentQueue CommonLog { get; set; } public override bool IsRequestAllowed(string clientId, Dictionary? factors) { diff --git a/RateLimiter.Tests/TimespanSinceLastCallRuleTests.cs b/RateLimiter.Tests/TimespanSinceLastCallRuleTests.cs index 43e6cfd1..568dfa62 100644 --- a/RateLimiter.Tests/TimespanSinceLastCallRuleTests.cs +++ b/RateLimiter.Tests/TimespanSinceLastCallRuleTests.cs @@ -1,6 +1,4 @@ -using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Resources; - -using NUnit.Framework; +using NUnit.Framework; using System; using System.Collections.Concurrent; @@ -12,16 +10,15 @@ namespace RateLimiter.Tests public class TimespanSinceLastCallRuleTests { private TimespanSinceLastCallRule _rule; - private Dictionary? _factors; + private Dictionary _factors; private ConcurrentQueue _log; [SetUp] public void Setup() { - _rule = new(TimeSpan.FromSeconds(0.1)); - _factors = new() { {"k", "v" } }; _log = new(); - _rule.CommonLog = _log; + _rule = new(TimeSpan.FromSeconds(0.1), _log); + _factors = new() { {"k", "v" } }; } [Test] diff --git a/RateLimiter.Tests/XRequestsPerTimespanRuleTests.cs b/RateLimiter.Tests/XRequestsPerTimespanRuleTests.cs index 1522a341..a02ab0b5 100644 --- a/RateLimiter.Tests/XRequestsPerTimespanRuleTests.cs +++ b/RateLimiter.Tests/XRequestsPerTimespanRuleTests.cs @@ -10,7 +10,7 @@ namespace RateLimiter.Tests public class XRequestsPerTimespanRuleTests { //private TimespanSinceLastCallRule _rule; - private Dictionary? _factors; + private Dictionary _factors; private ConcurrentQueue _log; [SetUp] @@ -26,8 +26,7 @@ public void Setup() [Test] public void IsRequestAllowed_FirstRequest_ReturnsTrue() { - var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromSeconds(0.1)); - rule.CommonLog = _log; + var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromSeconds(0.1), _log); var result = rule.IsRequestAllowed("client1", null); Assert.IsTrue(result); } @@ -35,8 +34,7 @@ public void IsRequestAllowed_FirstRequest_ReturnsTrue() [Test] public void IsRequestAllowed_WithinLimit_ReturnsTrue() { - var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromSeconds(0.1)); - rule.CommonLog = _log; + var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromSeconds(0.1), _log); for (int i = 0; i < 4; i++) { var isAllowed = rule.IsRequestAllowed("client1", _factors); @@ -58,8 +56,7 @@ public void IsRequestAllowed_WithinLimit_ReturnsTrue() [Test] public void IsRequestAllowed_ExceedsLimit_ReturnsFalse() { - var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromSeconds(0.1)); - rule.CommonLog = _log; + var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromSeconds(0.1), _log); for (int i = 0; i < 5; i++) { var isAllowed = rule.IsRequestAllowed("client1", _factors); @@ -81,8 +78,7 @@ public void IsRequestAllowed_ExceedsLimit_ReturnsFalse() [Test] public void IsRequestAllowed_AfterTimespan_ReturnsTrue() { - var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromSeconds(0.1)); - rule.CommonLog = _log; + var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromSeconds(0.1), _log); for (int i = 0; i < 5; i++) { var isAllowed = rule.IsRequestAllowed("client1", _factors); diff --git a/RateLimiter/BaseRule.cs b/RateLimiter/BaseRule.cs index 37d917ef..c346eaad 100644 --- a/RateLimiter/BaseRule.cs +++ b/RateLimiter/BaseRule.cs @@ -18,7 +18,7 @@ public interface IRateLimitingRule /// /// Base class for rate limiting rules /// -public abstract class BaseRule : IRateLimitingRule +public abstract class BaseRule(IEnumerable log) : IRateLimitingRule { /// /// Factors that can be used to determine if the rule is applicable @@ -29,7 +29,7 @@ public abstract class BaseRule : IRateLimitingRule /// /// Common log of requests /// - public IEnumerable CommonLog { get; set; } + public IEnumerable CommonLog { get; set; } = log; /// /// Check if the request is allowed @@ -40,9 +40,8 @@ public abstract class BaseRule : IRateLimitingRule public virtual bool IsRequestAllowed(string clientId, Dictionary? factors) { // If factors are not set or are not used, the rule is not applicable - if (Factors != null && factors?.ContainsAllElements(Factors) != true) - return true; - return false; + return Factors != null + && factors?.ContainsAllElements(Factors) != true; } } @@ -54,7 +53,7 @@ public static class DictionaryComparer { public static bool ContainsAllElements( this Dictionary mainDict, - Dictionary subDict) + Dictionary subDict) where TKey : notnull { return subDict.All(kv => mainDict.ContainsKey(kv.Key) && EqualityComparer.Default.Equals(mainDict[kv.Key], kv.Value)); } diff --git a/RateLimiter/RateLimiter.cs b/RateLimiter/RateLimiter.cs index 7d6378ed..ea8115f0 100644 --- a/RateLimiter/RateLimiter.cs +++ b/RateLimiter/RateLimiter.cs @@ -31,7 +31,6 @@ public void AddRule(string resource, IRateLimitingRule rule) if (!_resourceRules.ContainsKey(resource)) _resourceRules[resource] = []; - rule.CommonLog = _requestLog; _resourceRules[resource].Add(rule); } @@ -98,10 +97,7 @@ public bool IsRequestAllowed(string resource, string clientId, Dictionary /// - public IEnumerable? GetRequestLog() - { - return _requestLog?.ToArray(); - } + public IEnumerable GetRequestLog() => _requestLog.ToArray(); } diff --git a/RateLimiter/TimespanSinceLastCallRule.cs b/RateLimiter/TimespanSinceLastCallRule.cs index 8c156564..2fd72eaa 100644 --- a/RateLimiter/TimespanSinceLastCallRule.cs +++ b/RateLimiter/TimespanSinceLastCallRule.cs @@ -7,7 +7,7 @@ namespace RateLimiter; /// /// Rule to limit the certain timespan has passed since the last call /// -public class TimespanSinceLastCallRule(TimeSpan requiredTimespan) : BaseRule +public class TimespanSinceLastCallRule(TimeSpan requiredTimespan, IEnumerable log) : BaseRule(log) { public override bool IsRequestAllowed(string clientId, Dictionary? factors) { @@ -15,7 +15,7 @@ public override bool IsRequestAllowed(string clientId, Dictionary + var lastDeniedRequest = CommonLog.LastOrDefault(entry => entry.ClientId == clientId && entry.IsAllowed == false && (Factors == null @@ -25,7 +25,7 @@ public override bool IsRequestAllowed(string clientId, Dictionary + var lastRequest = CommonLog.LastOrDefault(entry => entry.ClientId == clientId && (Factors == null || entry.Factors?.ContainsAllElements(Factors) == true)); diff --git a/RateLimiter/XRequestsPerTimespanRule.cs b/RateLimiter/XRequestsPerTimespanRule.cs index 9a7cac16..7976b99e 100644 --- a/RateLimiter/XRequestsPerTimespanRule.cs +++ b/RateLimiter/XRequestsPerTimespanRule.cs @@ -8,7 +8,7 @@ namespace RateLimiter; /// This rule limits the number of requests a client can make within a specified timespan. /// It checks the number of requests made by a client within the given timespan and denies further requests if the limit is exceeded. /// -public class XRequestsPerTimespanRule(int maxRequests, TimeSpan timespan) : BaseRule +public class XRequestsPerTimespanRule(int maxRequests, TimeSpan timespan, IEnumerable log) : BaseRule(log) { public override bool IsRequestAllowed(string clientId, Dictionary? factors) { @@ -16,7 +16,7 @@ public override bool IsRequestAllowed(string clientId, Dictionary + var lastDeniedRequest = CommonLog.LastOrDefault(entry => entry.ClientId == clientId && entry.IsAllowed == false && (Factors == null