diff --git a/README.md b/README.md index 47e73daa..2b8db9ae 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,148 @@ -**Rate-limiting pattern** +Here's an extended version of the documentation with usage examples for all the rules (FixedWindow, Timespan, And, and Or): -Rate limiting involves restricting the number of requests that a client can make. -A client is identified with an access token, which is used for every request to a resource. -To prevent abuse of the server, APIs enforce rate-limiting techniques. -The rate-limiting application can decide whether to allow the request based on the client. -The client makes an API call to a particular resource; the server checks whether the request for this client is within the limit. -If the request is within the limit, then the request goes through. -Otherwise, the API call is restricted. +--- -Some examples of request-limiting rules (you could imagine any others) -* X requests per timespan; -* a certain timespan has passed since the last call; -* For US-based tokens, we use X requests per timespan; for EU-based tokens, a certain timespan has passed since the last call. +# Rate Limiter Library Documentation -The goal is to design a class(-es) that manages each API resource's rate limits by a set of provided *configurable and extendable* rules. For example, for one resource, you could configure the limiter to use Rule A; for another one - Rule B; for a third one - both A + B, etc. Any combination of rules should be possible; keep this fact in mind when designing the classes. +## Overview -We're more interested in the design itself than in some intelligent and tricky rate-limiting algorithm. There is no need to use a database (in-memory storage is fine) or any web framework. Do not waste time on preparing complex environment, reusable class library covered by a set of tests is more than enough. +The `RateLimiter` library provides a flexible and extensible way to control the rate of requests for specific identifiers, such as IP addresses or tokens. The library supports several rules for rate limiting, including fixed windows, timespan-based limits, and composite rules (AND, OR). This allows for the application of complex rate-limiting strategies to prevent abuse and ensure fairness. -There is a Test Project set up for you to use. However, you are welcome to create your own test project and use whatever test runner you like. +The library consists of several components: +1. **Identifiers** – Represent the entities whose rate of requests is being limited (e.g., IP address, token). +2. **Rules** – Define the conditions under which requests are allowed or denied, including `FixedWindow`, `Timespan`, and logical combinations like `And` and `Or`. +3. **History** – Keeps track of requests and allows checking whether a request exceeds the limits within a specific timeframe. -You are welcome to ask any questions regarding the requirements—treat us as product owners, analysts, or whoever knows the business. -If you have any questions or concerns, please submit them as a [GitHub issue](https://github.com/crexi-dev/rate-limiter/issues). +--- -You should [fork](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) the project and [create a pull request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) named as `FirstName LastName` once you are finished. +## Key Components -Good luck! +### 1. **Identifiers** + +An identifier is an entity whose rate is being limited. For example, you can limit the number of requests per IP address or token. + + +--- + +### 2. **Rules** + +Rules define the conditions for limiting requests. + +#### Fixed Window Rule (`FixedWindow`) + +This rule limits the number of requests that can be made within a fixed window of time (e.g., 10 requests per 60 seconds). + +##### Usage Example: Fixed Window Rule + +```csharp +var history = new InMemoryFixedWindowHistory(); +var fixedWindowRule = new FixedWindow(history, maxCount: 5, window: 10); // Max 5 requests in 10 seconds + +var ipAddress = new IpAddress("192.168.1.1"); + +for (int i = 0; i < 6; i++) +{ + bool isAllowed = fixedWindowRule.Check(ipAddress); + Console.WriteLine(isAllowed ? "Request allowed" : "Request denied"); + + // Simulate a delay between requests + Task.Delay(1000).Wait(); // 1 second delay +} +``` + +--- + +#### Timespan Rule (`Timespan`) + +This rule checks the time passed since the last request for an identifier. It allows a request only if a specified timespan has passed since the last request. + + +##### Usage Example: Timespan Rule + +```csharp +var history = new InMemoryTimespanHistory(); +var timespanRule = new Timespan(history, timespan: 5); // Allow one request every 5 seconds + +var token = new Token("abcdef12345"); + +for (int i = 0; i < 6; i++) +{ + bool isAllowed = timespanRule.Check(token); + Console.WriteLine(isAllowed ? "Request allowed" : "Request denied"); + + // Simulate a delay between requests + Task.Delay(2000).Wait(); // 2 second delay +} +``` + +--- + +#### Logical Rules (`And`, `Or`) + +You can combine multiple rules using `And` (all rules must pass) or `Or` (any rule must pass). + +##### `And` Rule + +This rule checks if all the given rules pass. + + +##### Usage Example: `And` Rule + +```csharp +var fixedWindowHistory = new InMemoryFixedWindowHistory(); +var timespanHistory = new InMemoryTimespanHistory(); + +var fixedWindowRule = new FixedWindow(fixedWindowHistory, maxCount: 3, window: 10); +var timespanRule = new Timespan(timespanHistory, timespan: 5); + +var andRule = new And(new IRule[] { fixedWindowRule, timespanRule }); + +var ipAddress = new IpAddress("192.168.1.1"); + +for (int i = 0; i < 5; i++) +{ + bool isAllowed = andRule.Check(ipAddress); + Console.WriteLine(isAllowed ? "Request allowed" : "Request denied"); + + // Simulate a delay between requests + Task.Delay(1000).Wait(); // 1 second delay +} +``` + +--- + +##### `Or` Rule + +This rule checks if any of the given rules pass. + + +##### Usage Example: `Or` Rule + +```csharp +var fixedWindowHistory = new InMemoryFixedWindowHistory(); +var timespanHistory = new InMemoryTimespanHistory(); + +var fixedWindowRule = new FixedWindow(fixedWindowHistory, maxCount: 2, window: 5); // Max 2 requests in 5 seconds +var timespanRule = new Timespan(timespanHistory, timespan: 10); // Allow one request every 10 seconds + +var orRule = new Or(new IRule[] { fixedWindowRule, timespanRule }); + +var ipAddress = new IpAddress("192.168.1.1"); + +for (int i = 0; i < 5; i++) +{ + bool isAllowed = orRule.Check(ipAddress); + Console.WriteLine(isAllowed ? "Request allowed" : "Request denied"); + + // Simulate a delay between requests + Task.Delay(1000).Wait(); // 1 second delay +} +``` + +--- + +## Conclusion + +The `RateLimiter` library is a powerful tool for controlling request rates based on different conditions. By combining various rules (such as `FixedWindow`, `Timespan`, and logical combinations like `And` and `Or`), you can build sophisticated rate-limiting strategies tailored to your application’s needs. + +This flexibility makes it easy to protect your application from abuse while ensuring fair access for legitimate users. \ No newline at end of file diff --git a/RateLimiter.Tests/AndTest.cs b/RateLimiter.Tests/AndTest.cs new file mode 100644 index 00000000..87846ffb --- /dev/null +++ b/RateLimiter.Tests/AndTest.cs @@ -0,0 +1,102 @@ +using Moq; +using NUnit.Framework; +using RateLimiter.Interfaces; +using RateLimiter.Rules; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RateLimiter.Tests +{ + [TestFixture] + public class AndTest + { + private Mock _rule1Mock; + private Mock _rule2Mock; + private And _andRule; + + [SetUp] + public void SetUp() + { + _rule1Mock = new Mock(); + _rule2Mock = new Mock(); + } + + [Test] + public void Check_AllRulesReturnTrue_ReturnsTrue() + { + // Arrange + var identifier = Mock.Of(); + + _rule1Mock.Setup(r => r.Check(identifier)).Returns(true); + _rule2Mock.Setup(r => r.Check(identifier)).Returns(true); + + _andRule = new And(new[] { _rule1Mock.Object, _rule2Mock.Object }); + + // Act + var result = _andRule.Check(identifier); + + // Assert + Assert.IsTrue(result); + _rule1Mock.Verify(r => r.Check(identifier), Times.Once); + _rule2Mock.Verify(r => r.Check(identifier), Times.Once); + } + + [Test] + public void Check_OneRuleReturnsFalse_ReturnsFalse() + { + // Arrange + var identifier = Mock.Of(); + + _rule1Mock.Setup(r => r.Check(identifier)).Returns(true); + _rule2Mock.Setup(r => r.Check(identifier)).Returns(false); + + _andRule = new And(new[] { _rule1Mock.Object, _rule2Mock.Object }); + + // Act + var result = _andRule.Check(identifier); + + // Assert + Assert.IsFalse(result); + _rule1Mock.Verify(r => r.Check(identifier), Times.Once); + _rule2Mock.Verify(r => r.Check(identifier), Times.Once); + } + + [Test] + public void Check_AllRulesReturnFalse_ReturnsFalse() + { + // Arrange + var identifier = Mock.Of(); + + _rule1Mock.Setup(r => r.Check(identifier)).Returns(false); + _rule2Mock.Setup(r => r.Check(identifier)).Returns(false); + + _andRule = new And(new[] { _rule1Mock.Object, _rule2Mock.Object }); + + // Act + var result = _andRule.Check(identifier); + + // Assert + Assert.IsFalse(result); + _rule1Mock.Verify(r => r.Check(identifier), Times.Once); + _rule2Mock.Verify(r => r.Check(identifier), Times.Never); // Short-circuit: stops at the first false + } + + [Test] + public void Check_NoRules_ReturnsTrue() + { + // Arrange + var identifier = Mock.Of(); + + _andRule = new And(Array.Empty()); + + // Act + var result = _andRule.Check(identifier); + + // Assert + Assert.IsTrue(result); + } + } +} diff --git a/RateLimiter.Tests/FixedWindowTest.cs b/RateLimiter.Tests/FixedWindowTest.cs new file mode 100644 index 00000000..31a14f4f --- /dev/null +++ b/RateLimiter.Tests/FixedWindowTest.cs @@ -0,0 +1,86 @@ +using NUnit.Framework; +using System; +using Moq; +using NUnit.Framework; +using RateLimiter.Rules; +using RateLimiter.Interfaces; + + +namespace RateLimiter.Tests +{ + [TestFixture] + public class FixedWindowTest + { + private Mock _fixedWindowHistoryMock; + private FixedWindow _fixedWindow; + private const uint MaxCount = 5; + private const uint Window = 10; // 10 seconds + + [SetUp] + public void SetUp() + { + _fixedWindowHistoryMock = new Mock(); + _fixedWindow = new FixedWindow(_fixedWindowHistoryMock.Object, MaxCount, Window); + } + + [Test] + public void Check_RequestWithinLimit_ReturnsTrue() + { + // Arrange + var identifier = Mock.Of(); + var now = DateTime.Now; + + _fixedWindowHistoryMock + .Setup(h => h.GetRequestCount(identifier, It.Is(d => d <= DateTime.Now), + It.Is(d => d >= DateTime.Now.AddSeconds(-Window)))) + .Returns(4); // Simulate 4 requests within the window + + // Act + var result = _fixedWindow.Check(identifier); + + // Assert + Assert.IsTrue(result); + _fixedWindowHistoryMock.Verify(h => h.Record(identifier, It.Is(dt => Math.Abs((dt - now).TotalMilliseconds) < 100)), Times.Once); + } + + [Test] + public void Check_RequestExceedsLimit_ReturnsFalse() + { + // Arrange + var identifier = Mock.Of(); + var now = DateTime.Now; + + _fixedWindowHistoryMock + .Setup(h => h.GetRequestCount(identifier, It.Is(d => d <= DateTime.Now), + It.Is(d => d >= DateTime.Now.AddSeconds(-Window)))) + .Returns(6); // Simulate 6 requests within the window + + // Act + var result = _fixedWindow.Check(identifier); + + // Assert + Assert.IsFalse(result); + _fixedWindowHistoryMock.Verify(h => h.Record(It.IsAny(), It.IsAny()), Times.Never); + } + + [Test] + public void Check_RequestAtLimit_ReturnsTrue() + { + // Arrange + var identifier = Mock.Of(); + var now = DateTime.Now; + + _fixedWindowHistoryMock + .Setup(h => h.GetRequestCount(identifier, It.Is(d => d <= DateTime.Now), + It.Is(d => d >= DateTime.Now.AddSeconds(-Window)))) + .Returns(5); // Simulate 5 requests, which is at the limit + + // Act + var result = _fixedWindow.Check(identifier); + + // Assert + Assert.IsTrue(result); + _fixedWindowHistoryMock.Verify(h => h.Record(identifier, It.Is(dt => Math.Abs((dt - now).TotalMilliseconds) < 100)), Times.Once); + } + } +} diff --git a/RateLimiter.Tests/OrTest.cs b/RateLimiter.Tests/OrTest.cs new file mode 100644 index 00000000..c490567d --- /dev/null +++ b/RateLimiter.Tests/OrTest.cs @@ -0,0 +1,102 @@ +using Moq; +using NUnit.Framework; +using RateLimiter.Interfaces; +using RateLimiter.Rules; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RateLimiter.Tests +{ + [TestFixture] + public class OrTest + { + private Mock _rule1Mock; + private Mock _rule2Mock; + private Or _orRule; + + [SetUp] + public void SetUp() + { + _rule1Mock = new Mock(); + _rule2Mock = new Mock(); + } + + [Test] + public void Check_AllRulesReturnFalse_ReturnsFalse() + { + // Arrange + var identifier = Mock.Of(); + + _rule1Mock.Setup(r => r.Check(identifier)).Returns(false); + _rule2Mock.Setup(r => r.Check(identifier)).Returns(false); + + _orRule = new Or(new[] { _rule1Mock.Object, _rule2Mock.Object }); + + // Act + var result = _orRule.Check(identifier); + + // Assert + Assert.IsFalse(result); + _rule1Mock.Verify(r => r.Check(identifier), Times.Once); + _rule2Mock.Verify(r => r.Check(identifier), Times.Once); + } + + [Test] + public void Check_OneRuleReturnsTrue_ReturnsTrue() + { + // Arrange + var identifier = Mock.Of(); + + _rule1Mock.Setup(r => r.Check(identifier)).Returns(false); + _rule2Mock.Setup(r => r.Check(identifier)).Returns(true); + + _orRule = new Or(new[] { _rule1Mock.Object, _rule2Mock.Object }); + + // Act + var result = _orRule.Check(identifier); + + // Assert + Assert.IsTrue(result); + _rule1Mock.Verify(r => r.Check(identifier), Times.Once); + _rule2Mock.Verify(r => r.Check(identifier), Times.Once); + } + + [Test] + public void Check_FirstRuleReturnsTrue_ReturnsTrueAndShortCircuits() + { + // Arrange + var identifier = Mock.Of(); + + _rule1Mock.Setup(r => r.Check(identifier)).Returns(true); + _rule2Mock.Setup(r => r.Check(identifier)).Returns(false); + + _orRule = new Or(new[] { _rule1Mock.Object, _rule2Mock.Object }); + + // Act + var result = _orRule.Check(identifier); + + // Assert + Assert.IsTrue(result); + _rule1Mock.Verify(r => r.Check(identifier), Times.Once); + _rule2Mock.Verify(r => r.Check(identifier), Times.Never); // Short-circuits + } + + [Test] + public void Check_NoRules_ReturnsFalse() + { + // Arrange + var identifier = Mock.Of(); + + _orRule = new Or(Array.Empty()); + + // Act + var result = _orRule.Check(identifier); + + // Assert + Assert.IsFalse(result); + } + } +} diff --git a/RateLimiter.Tests/RateLimiter.Tests.csproj b/RateLimiter.Tests/RateLimiter.Tests.csproj index 5cbfc4e8..ef10b84d 100644 --- a/RateLimiter.Tests/RateLimiter.Tests.csproj +++ b/RateLimiter.Tests/RateLimiter.Tests.csproj @@ -9,6 +9,7 @@ + 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.Tests/TimespanTest.cs b/RateLimiter.Tests/TimespanTest.cs new file mode 100644 index 00000000..acd8ba04 --- /dev/null +++ b/RateLimiter.Tests/TimespanTest.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Moq; +using NUnit.Framework; +using RateLimiter.Interfaces; +using RateLimiter.Rules; + +namespace RateLimiter.Tests +{ + [TestFixture] + public class TimespanTest + { + private Mock _timespanHistoryMock; + private Timespan _timespan; + private const uint TimespanValue = 10; // 10 seconds + + [SetUp] + public void SetUp() + { + _timespanHistoryMock = new Mock(); + _timespan = new Timespan(_timespanHistoryMock.Object, TimespanValue); + } + + [Test] + public void Check_LastRequestOutsideTimespan_ReturnsTrue() + { + // Arrange + var identifier = Mock.Of(); + var now = DateTime.Now; + var lastRequestTime = now.AddSeconds(-20); // Last request was 20 seconds ago + + _timespanHistoryMock + .Setup(h => h.GetLastRequestDate(identifier)) + .Returns(lastRequestTime); + + // Act + var result = _timespan.Check(identifier); + + // Assert + Assert.IsTrue(result); + //_timespanHistoryMock.Verify(h => h.Record(identifier, It.Is(dt => dt == now)), Times.Once); + } + + [Test] + public void Check_LastRequestWithinTimespan_ReturnsFalse() + { + // Arrange + var identifier = Mock.Of(); + var now = DateTime.Now; + var lastRequestTime = now.AddSeconds(-5); // Last request was 5 seconds ago + + _timespanHistoryMock + .Setup(h => h.GetLastRequestDate(identifier)) + .Returns(lastRequestTime); + + // Act + var result = _timespan.Check(identifier); + + // Assert + Assert.IsFalse(result); + //_timespanHistoryMock.Verify(h => h.Record(It.IsAny(), It.IsAny()), Times.Never); + } + + [Test] + public void Check_NoPreviousRequests_ReturnsTrue() + { + // Arrange + var identifier = Mock.Of(); + var now = DateTime.Now; + + _timespanHistoryMock + .Setup(h => h.GetLastRequestDate(identifier)) + .Returns(DateTime.MinValue); // Simulate no previous request + + // Act + var result = _timespan.Check(identifier); + + // Assert + Assert.IsTrue(result); + //_timespanHistoryMock.Verify(h => h.Record(identifier, It.Is(dt => dt == now)), Times.Once); + } + } +} diff --git a/RateLimiter/Identifiers/IpAddress.cs b/RateLimiter/Identifiers/IpAddress.cs new file mode 100644 index 00000000..98a8cde7 --- /dev/null +++ b/RateLimiter/Identifiers/IpAddress.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using RateLimiter.Interfaces; + +namespace RateLimiter.Identifiers +{ + public class IpAddress : IIdentifier + { + private string IpAddressValue { get; set; } + + public IpAddress(string ipAddress) + { + IpAddressValue = ipAddress; + } + + public string ToString() + { + return IpAddressValue; + } + } +} diff --git a/RateLimiter/Identifiers/Token.cs b/RateLimiter/Identifiers/Token.cs new file mode 100644 index 00000000..0ef84ce6 --- /dev/null +++ b/RateLimiter/Identifiers/Token.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using RateLimiter.Interfaces; + +namespace RateLimiter.Identifiers +{ + public class Token : IIdentifier + { + private string TokenKey { get; set; } + + public Token(string token) + { + TokenKey = token; + } + + public string ToString() + { + return TokenKey; + } + } +} diff --git a/RateLimiter/Interfaces/IFixedWindowHistory.cs b/RateLimiter/Interfaces/IFixedWindowHistory.cs new file mode 100644 index 00000000..7463d3a7 --- /dev/null +++ b/RateLimiter/Interfaces/IFixedWindowHistory.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RateLimiter.Interfaces +{ + public interface IFixedWindowHistory : IHistory + { + int GetRequestCount(IIdentifier identifier, DateTime start, DateTime end); + + } +} diff --git a/RateLimiter/Interfaces/IHistory.cs b/RateLimiter/Interfaces/IHistory.cs new file mode 100644 index 00000000..edb661f3 --- /dev/null +++ b/RateLimiter/Interfaces/IHistory.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RateLimiter.Interfaces +{ + public interface IHistory + { + void Record(IIdentifier identifier, DateTime now); + } +} diff --git a/RateLimiter/Interfaces/IIdentifier.cs b/RateLimiter/Interfaces/IIdentifier.cs new file mode 100644 index 00000000..f165fc87 --- /dev/null +++ b/RateLimiter/Interfaces/IIdentifier.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RateLimiter.Interfaces +{ + public interface IIdentifier + { + string ToString(); + } +} diff --git a/RateLimiter/Interfaces/IRule.cs b/RateLimiter/Interfaces/IRule.cs new file mode 100644 index 00000000..71c2a7a0 --- /dev/null +++ b/RateLimiter/Interfaces/IRule.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RateLimiter.Interfaces +{ + public interface IRule + { + bool Check(IIdentifier identifier); + } +} diff --git a/RateLimiter/Interfaces/ITimespanHistory.cs b/RateLimiter/Interfaces/ITimespanHistory.cs new file mode 100644 index 00000000..cf651e68 --- /dev/null +++ b/RateLimiter/Interfaces/ITimespanHistory.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RateLimiter.Interfaces +{ + public interface ITimespanHistory : IHistory + { + DateTime GetLastRequestDate(IIdentifier identifier); + } +} diff --git a/RateLimiter/Rules/And.cs b/RateLimiter/Rules/And.cs new file mode 100644 index 00000000..38b0d714 --- /dev/null +++ b/RateLimiter/Rules/And.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using RateLimiter.Interfaces; + +namespace RateLimiter.Rules +{ + public class And : IRule + { + private readonly IRule[] Rules; + + public And(IRule[] rules) + { + Rules = rules; + } + + public bool Check(IIdentifier identifier) + { + foreach (var rule in Rules) + { + if(!rule.Check(identifier)) + { + return false; + } + } + return true; + } + } +} diff --git a/RateLimiter/Rules/FixedWindow.cs b/RateLimiter/Rules/FixedWindow.cs new file mode 100644 index 00000000..8a522d11 --- /dev/null +++ b/RateLimiter/Rules/FixedWindow.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using RateLimiter.Interfaces; + +namespace RateLimiter.Rules +{ + public class FixedWindow : IRule + { + private readonly IFixedWindowHistory FixedWindowHistory; + private readonly uint MaxCount; + private readonly uint Window; + + public FixedWindow(IFixedWindowHistory fixedWindowHistory, uint maxCount, uint window) + { + FixedWindowHistory = fixedWindowHistory; + MaxCount = maxCount; + Window = window; + } + + public bool Check(IIdentifier identifier) + { + var now = DateTime.Now; + var history = FixedWindowHistory.GetRequestCount(identifier, now.AddSeconds(-Window), now); + var isAllowed = history <= MaxCount; + + if (isAllowed) + { + FixedWindowHistory.Record(identifier, now); + } + + return isAllowed; + } + } +} diff --git a/RateLimiter/Rules/Or.cs b/RateLimiter/Rules/Or.cs new file mode 100644 index 00000000..8ecd2504 --- /dev/null +++ b/RateLimiter/Rules/Or.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using RateLimiter.Interfaces; + +namespace RateLimiter.Rules +{ + public class Or : IRule + { + private readonly IRule[] Rules; + + public Or(IRule[] rules) + { + Rules = rules; + } + + public bool Check(IIdentifier identifier) + { + foreach (var rule in Rules) + { + if(rule.Check(identifier)) + { + return true; + } + } + return false; + } + } +} diff --git a/RateLimiter/Rules/Timespan.cs b/RateLimiter/Rules/Timespan.cs new file mode 100644 index 00000000..fc5474b2 --- /dev/null +++ b/RateLimiter/Rules/Timespan.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using RateLimiter.Interfaces; + +namespace RateLimiter.Rules +{ + public class Timespan : IRule + { + private readonly ITimespanHistory TimespanHistory; + private readonly uint TimespanValue; + + public Timespan(ITimespanHistory timespanHistory, uint timespan) + { + TimespanHistory = timespanHistory; + TimespanValue = timespan; + } + + public bool Check(IIdentifier identifier) + { + var now = DateTime.Now; + var history = TimespanHistory.GetLastRequestDate(identifier); + var isAllowed = history.AddSeconds(TimespanValue) <= now; + + if (isAllowed) + { + TimespanHistory.Record(identifier, now); + } + + return isAllowed; + } + + } +}