diff --git a/README.md b/README.md index 47e73daa..0126dcd5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,47 @@ -**Rate-limiting pattern** +**Implementation - Brandon Choi** + +I utilized the builder pattern to provide a clear and straightforward way to construct rate limiting rules. Composite rules were implemented to support both logical AND and OR operations, allowing for flexible combinations of rules. Additionally, I employed a simple decorator pattern to seamlessly extend rule evaluation with additional logic. + +Here are usage examples: + +1. Combining rules using AND operation: +``` +var maxRequestsRule = new MaxRequestsPerTimeSpanRule(5, TimeSpan.FromSeconds(10)); +var minTimeRule = new MinimumTimeBetweenRequestsRule(TimeSpan.FromSeconds(2)); + +var builder = new RateLimitRuleBuilder(); +var combinedRule = builder + .Add(maxRequestsRule) + .Add(minTimeRule) + .Build(); +``` + +2. Combining rules using OR operation: +``` +var maxRequestsRule = new MaxRequestsPerTimeSpanRule(1, TimeSpan.FromSeconds(10)); +var minTimeRule = new MinimumTimeBetweenRequestsRule(TimeSpan.FromSeconds(5)); + +var builder = new RateLimitRuleBuilder(); +var combinedRule = builder + .Or(new List { maxRequestsRule, minTimeRule }) + .Build(); +``` + +3. Region specific rules: +``` +var usRule = new MaxRequestsPerTimeSpanRule(5, TimeSpan.FromSeconds(10)); +var euRule = new MinimumTimeBetweenRequestsRule(TimeSpan.FromSeconds(5)); + +var regionBuilder = new RateLimitRuleBuilder(); +var regionCombinedRule = regionBuilder + .AddForRegion(usRule, Region.US) + .AddForRegion(euRule, Region.EU) + .Build(); +``` + +# + +**Rate-limiting pattern** 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. diff --git a/RateLimiter.Tests/RateLimiterTest.cs b/RateLimiter.Tests/RateLimiterTest.cs index 172d44a7..8be9d175 100644 --- a/RateLimiter.Tests/RateLimiterTest.cs +++ b/RateLimiter.Tests/RateLimiterTest.cs @@ -1,13 +1,115 @@ -using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Threading; +using NUnit.Framework; +using RateLimiter.Models; +using RateLimiter.Rules; +using RateLimiter.Interfaces; +using RateLimiter.Builder; namespace RateLimiter.Tests; +/// +/// Collection of tests to demonstrate RateLimiter project usage. +/// Given more time, I would provide more detailed tests of each class and aim for 100% code coverage. +/// [TestFixture] -public class RateLimiterTest +public class RateLimiterTests { [Test] - public void Example() + public void RateLimitRuleBuilder_CombinesTwoPassingRulesWithAnd_ReturnsAllowed() { - Assert.That(true, Is.True); + // Arrange + var rule1 = new MaxRequestsPerTimeSpanRule(5, TimeSpan.FromSeconds(10)); + var rule2 = new MinimumTimeBetweenRequestsRule(TimeSpan.FromSeconds(2)); + + // Act + var combinedRule = new RateLimitRuleBuilder() + .Add(rule1) + .Add(rule2) + .Build(); + + var context = new RateLimitContext("client-123", "api/resource", Region.US); + var result = combinedRule.Evaluate(context); + + // Assert + Assert.IsTrue(result.Allowed); + Assert.IsEmpty(result.RejectedReasons); + } + + [Test] + public void RateLimitRuleBuilder_CombinesTwoRulesWithOr_ReturnsAllowed() + { + // Arrange + var rule1 = new MaxRequestsPerTimeSpanRule(1, TimeSpan.FromSeconds(10)); + var rule2 = new MinimumTimeBetweenRequestsRule(TimeSpan.FromSeconds(5)); + var builder = new RateLimitRuleBuilder(); + + // Act, Assert + var combinedRule = builder + .Or(new List { rule1, rule2 }) + .Build(); + var context = new RateLimitContext("client-123", "api/resource", Region.US); + + var response1 = combinedRule.Evaluate(context); + Assert.IsTrue(response1.Allowed); + + var response2 = combinedRule.Evaluate(context); + Assert.IsFalse(response2.Allowed); + Thread.Sleep(6000); + + var response3 = combinedRule.Evaluate(context); + Assert.IsTrue(response3.Allowed); + } + + [Test] + public void RateLimitRuleBuilder_RegionMatchesAndEvaluatesRule_ReturnsAllowed() + { + // Arrange + var rule = new MaxRequestsPerTimeSpanRule(1, TimeSpan.FromSeconds(10)); + var builder = new RateLimitRuleBuilder(); + var region = Region.US; + + // Act + var combinedRule = builder + .AddForRegion(rule, region) + .Build(); + var context = new RateLimitContext("client-123", "api/resource", region); + + // Assert + var response = combinedRule.Evaluate(context); + Assert.IsTrue(response.Allowed); + } + + [Test] + public void RateLimitRuleBuilder_RegionBasedRules_ReturnsAllowedForUSAndNotAllowedForEU() + { + // Arrange + var usRule = new MaxRequestsPerTimeSpanRule(5, TimeSpan.FromSeconds(10)); + var euRule = new MinimumTimeBetweenRequestsRule(TimeSpan.FromSeconds(5)); + var builder = new RateLimitRuleBuilder(); + + // Act + var combinedRule = builder + .AddForRegion(usRule, Region.US) + .AddForRegion(euRule, Region.EU) + .Build(); + + // US-based context + var usContext = new RateLimitContext("us-client-123", "api/resource", Region.US); + for (int i = 0; i < 5; i++) + { + var response = combinedRule.Evaluate(usContext); + Assert.IsTrue(response.Allowed); + } + + // EU-based context + var euContext = new RateLimitContext("eu-client-456", "api/resource", Region.EU); + var response1 = combinedRule.Evaluate(euContext); + Assert.IsTrue(response1.Allowed); + + var response2 = combinedRule.Evaluate(euContext); + Assert.IsFalse(response2.Allowed); + Assert.That(response2.RejectedReasons, Has.Exactly(1).EqualTo(nameof(MinimumTimeBetweenRequestsRule))); } -} \ No newline at end of file +} diff --git a/RateLimiter.Tests/Rules/MaxRequestsPerTimeSpanRuleTests.cs b/RateLimiter.Tests/Rules/MaxRequestsPerTimeSpanRuleTests.cs new file mode 100644 index 00000000..3bc53a2a --- /dev/null +++ b/RateLimiter.Tests/Rules/MaxRequestsPerTimeSpanRuleTests.cs @@ -0,0 +1,98 @@ +using System; +using System.Threading; +using NUnit.Framework; +using RateLimiter.Models; +using RateLimiter.Rules; + +namespace RateLimiter.Tests.Rules +{ + [TestFixture] + public class MaxRequestsPerTimeSpanRuleTests + { + [Test] + public void Evaluate_LessThanMaxRequests_ReturnsAllowed() + { + // Arrange + var rule = new MaxRequestsPerTimeSpanRule(5, TimeSpan.FromSeconds(10)); + var context = new RateLimitContext("client-123", "api/resource", Region.US); + + // Act and Assert + for (int i = 0; i < 5; i++) + { + var response = rule.Evaluate(context); + Assert.IsTrue(response.Allowed); + } + } + + [Test] + public void MaxRequestsPerTimeSpanRule_MaxRequestsExceeded_ReturnsNotAllowed() + { + // Arrange + var rule = new MaxRequestsPerTimeSpanRule(5, TimeSpan.FromSeconds(10)); + var context = new RateLimitContext("client-123", "api/resource", Region.US); + + // Act + for (int i = 0; i < 5; i++) + { + rule.Evaluate(context); + } + var response = rule.Evaluate(context); + + // Assert + Assert.IsFalse(response.Allowed); + Assert.That(response.RejectedReasons, Has.Exactly(1).EqualTo(nameof(MaxRequestsPerTimeSpanRule))); + } + + [Test] + public void MaxRequestsPerTimeSpan_TimeSpanExceeded_ReturnsAllowed() + { + // Arrange + var rule = new MaxRequestsPerTimeSpanRule(5, TimeSpan.FromSeconds(2)); + var context = new RateLimitContext("client-123", "api/resource", Region.US); + + // Act + for (int i = 0; i < 5; i++) + { + rule.Evaluate(context); + } + Thread.Sleep(2100); + var response = rule.Evaluate(context); + + // Assert + Assert.IsTrue(response.Allowed); + } + + [Test] + public void MinimumTimeBetweenRequestsRule_MinimumTimeNotPassed_ReturnsNotAllowed() + { + // Arrange + var rule = new MinimumTimeBetweenRequestsRule(TimeSpan.FromSeconds(2)); + var context = new RateLimitContext("client-123", "api/resource", Region.US); + + // Act + var response1 = rule.Evaluate(context); + var response2 = rule.Evaluate(context); + + // Assert + Assert.IsTrue(response1.Allowed); + Assert.IsFalse(response2.Allowed); + Assert.That(response2.RejectedReasons, Has.Exactly(1).EqualTo(nameof(MinimumTimeBetweenRequestsRule))); + } + + [Test] + public void MinimumTimeBetweenRequestsRule_MinimumTimePassed_ReturnsAllowed() + { + // Arrange + var rule = new MinimumTimeBetweenRequestsRule(TimeSpan.FromSeconds(2)); + var context = new RateLimitContext("client-123", "api/resource", Region.US); + + // Act + rule.Evaluate(context); + Thread.Sleep(2100); + var response = rule.Evaluate(context); + + // Assert + Assert.IsTrue(response.Allowed); + } + } +} diff --git a/RateLimiter.Tests/Rules/MinimumTimeBetweenRequestsRuleTests.cs b/RateLimiter.Tests/Rules/MinimumTimeBetweenRequestsRuleTests.cs new file mode 100644 index 00000000..a14c493c --- /dev/null +++ b/RateLimiter.Tests/Rules/MinimumTimeBetweenRequestsRuleTests.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; +using NUnit.Framework; +using RateLimiter.Models; +using RateLimiter.Rules; + +namespace RateLimiter.Tests.Rules +{ + [TestFixture] + public class MinimumTimeBetweenRequestsRuleTests + { + [Test] + public void Evaluate_MinimumTimeNotPassed_ReturnsNotAllowed() + { + // Arrange + var rule = new MinimumTimeBetweenRequestsRule(TimeSpan.FromSeconds(2)); + var context = new RateLimitContext("client-123", "api/resource", Region.US); + + // Act + var response1 = rule.Evaluate(context); + var response2 = rule.Evaluate(context); + + // Assert + Assert.IsTrue(response1.Allowed); + Assert.IsFalse(response2.Allowed); + Assert.That(response2.RejectedReasons, Has.Exactly(1).EqualTo(nameof(MinimumTimeBetweenRequestsRule))); + } + + [Test] + public void Evaluate_MinimumTimePassed_ReturnsAllowed() + { + // Arrange + var rule = new MinimumTimeBetweenRequestsRule(TimeSpan.FromSeconds(2)); + var context = new RateLimitContext("client-123", "api/resource", Region.US); + + // Act + rule.Evaluate(context); + Thread.Sleep(2100); + var response = rule.Evaluate(context); + + // Assert + Assert.IsTrue(response.Allowed); + } + } +} diff --git a/RateLimiter/Builder/RateLimitRuleBuilder.cs b/RateLimiter/Builder/RateLimitRuleBuilder.cs new file mode 100644 index 00000000..948fd387 --- /dev/null +++ b/RateLimiter/Builder/RateLimitRuleBuilder.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using RateLimiter.Interfaces; +using RateLimiter.Rules; +using RateLimiter.Models; + +namespace RateLimiter.Builder; + +/// +/// Builder to compile individual and/or groups of rate limit rules. +/// By default, will evaluate multiple rules with logical AND. +/// +public class RateLimitRuleBuilder +{ + private readonly List _rateLimitRules = new List(); + + /// + /// Adds rule to builder. + /// + /// + /// + public RateLimitRuleBuilder Add(IRateLimitRule rule) + { + _rateLimitRules.Add(rule); + return this; + } + + /// + /// Adds a collection of rules that will be evaluated with logical AND. + /// + /// Collection of rate limit rules. + /// + public RateLimitRuleBuilder Add(IEnumerable rateLimitRules) + { + _rateLimitRules.Add(new AndCompositeRule(rateLimitRules)); + return this; + } + + /// + /// Adds a collection of rules that will be evaluated with logical OR. + /// + /// + /// + public RateLimitRuleBuilder Or(IEnumerable rateLimitRules) + { + _rateLimitRules.Add(new OrCompositeRule(rateLimitRules)); + return this; + } + + /// + /// Adds a rule that should only apply for a specified region. + /// + /// + /// + /// The current builder instance for fluent chaining. + public RateLimitRuleBuilder AddForRegion(IRateLimitRule rule, Region region) + { + _rateLimitRules.Add(new RegionBasedRuleDecorator(rule, region)); + return this; + } + + /// + /// Compiles the list of rules into a single rule combined with logical AND. + /// + /// + public IRateLimitRule Build() + { + if (_rateLimitRules.Count == 0) + { + throw new InvalidOperationException("No rate limit rules have been added."); + } + else if (_rateLimitRules.Count == 1) + { + return _rateLimitRules.First(); + } + return new AndCompositeRule(_rateLimitRules); + } +} \ No newline at end of file diff --git a/RateLimiter/Interfaces/IRateLimitRule.cs b/RateLimiter/Interfaces/IRateLimitRule.cs new file mode 100644 index 00000000..f091dcea --- /dev/null +++ b/RateLimiter/Interfaces/IRateLimitRule.cs @@ -0,0 +1,16 @@ +using RateLimiter.Models; + +namespace RateLimiter.Interfaces; + +/// +/// Interface for RateLimitRule. +/// +public interface IRateLimitRule +{ + /// + /// Evaluates rate limiting rule. + /// + /// + /// + RateLimitResponse Evaluate(RateLimitContext context); +} \ No newline at end of file diff --git a/RateLimiter/Models/RateLimitContext.cs b/RateLimiter/Models/RateLimitContext.cs new file mode 100644 index 00000000..b9236169 --- /dev/null +++ b/RateLimiter/Models/RateLimitContext.cs @@ -0,0 +1,37 @@ +using System; + +namespace RateLimiter.Models; + +/// +/// Rate Limit Context. +/// +public class RateLimitContext +{ + /// + /// Client Token. + /// + public string ClientToken { get; } + + /// + /// API Resource. + /// + public string ApiResource { get; } + + /// + /// Region. + /// + public Region Region { get; } + + /// + /// Initializes a new instance of . + /// + /// The client token. + /// The API resource. + /// The region. + public RateLimitContext(string clientToken, string apiResource, Region region) + { + ClientToken = clientToken ?? throw new ArgumentNullException(nameof(clientToken)); + ApiResource = apiResource ?? throw new ArgumentNullException(nameof(apiResource)); + Region = region; + } +} \ No newline at end of file diff --git a/RateLimiter/Models/RateLimitResponse.cs b/RateLimiter/Models/RateLimitResponse.cs new file mode 100644 index 00000000..d8e61228 --- /dev/null +++ b/RateLimiter/Models/RateLimitResponse.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; + +namespace RateLimiter.Models; + +/// +/// Response for rate limit rule evaluations. +/// +public class RateLimitResponse +{ + /// + /// Indicates whether request passes rate limiting rule(s). + /// + public bool Allowed { get; } + + /// + /// List of rule(s) that were rejected. + /// + public IReadOnlyList RejectedReasons { get; } = Array.Empty(); + + /// + /// Initializes a new instance of with default values. + /// + public RateLimitResponse() + { + Allowed = true; + } + + /// + /// Initializes a new instance of . + /// + /// True if the request is allowed; otherwise, false. + /// List of rule(s) that request was rejected. + public RateLimitResponse(bool allowed, IReadOnlyList? rejectedRules) + { + Allowed = allowed; + RejectedReasons = rejectedRules ?? Array.Empty(); + } +} \ No newline at end of file diff --git a/RateLimiter/Models/RateLimitWindow.cs b/RateLimiter/Models/RateLimitWindow.cs new file mode 100644 index 00000000..640ca96a --- /dev/null +++ b/RateLimiter/Models/RateLimitWindow.cs @@ -0,0 +1,36 @@ +using System; + +namespace RateLimiter.Models; + +/// +/// Request start time and number of requests. +/// +public class RateLimitWindow +{ + /// + /// Time at which first request was made. + /// + public DateTime StartTime { get; set; } + + /// + /// End time of window (start time + timeSpan). + /// + public DateTime EndTime { get; set; } + + /// + /// Number of requests made. + /// + public int RequestCount { get; set; } + + /// + /// Initializes an instance of . + /// + /// Start time of the window. + /// Number of requests. + public RateLimitWindow(DateTime startTime, DateTime endTime, int requestCount) + { + StartTime = startTime; + EndTime = endTime; + RequestCount = requestCount; + } +} \ No newline at end of file diff --git a/RateLimiter/Models/Region.cs b/RateLimiter/Models/Region.cs new file mode 100644 index 00000000..cda54de5 --- /dev/null +++ b/RateLimiter/Models/Region.cs @@ -0,0 +1,11 @@ +namespace RateLimiter.Models; + +/// +/// Region. +/// +public enum Region +{ + US, + EU, + Other +} \ No newline at end of file diff --git a/RateLimiter/Rules/AndCompositeRule.cs b/RateLimiter/Rules/AndCompositeRule.cs new file mode 100644 index 00000000..b3d32db2 --- /dev/null +++ b/RateLimiter/Rules/AndCompositeRule.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Linq; +using RateLimiter.Interfaces; +using RateLimiter.Models; + +namespace RateLimiter.Rules; + +/// +/// Rule combining multiple rules and requires all to pass. (logical AND) +/// +public class AndCompositeRule : IRateLimitRule +{ + private readonly List _rateLimitRules; + + /// + /// Initializes an instance of . + /// + /// Collection of + public AndCompositeRule(IEnumerable rateLimitRules) + { + _rateLimitRules = rateLimitRules.ToList(); + } + + /// + /// Evaluates a collection of rules using logical AND. + /// + /// + /// + public RateLimitResponse Evaluate(RateLimitContext context) + { + var rejectedReasons = new List(); + + foreach (var rule in _rateLimitRules) + { + var response = rule.Evaluate(context); + if (!response.Allowed) + { + rejectedReasons.AddRange(response.RejectedReasons); + } + } + + return new RateLimitResponse(!rejectedReasons.Any(), rejectedReasons); + } +} \ No newline at end of file diff --git a/RateLimiter/Rules/MaxRequestsPerTimeSpanRule.cs b/RateLimiter/Rules/MaxRequestsPerTimeSpanRule.cs new file mode 100644 index 00000000..801d200b --- /dev/null +++ b/RateLimiter/Rules/MaxRequestsPerTimeSpanRule.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using RateLimiter.Interfaces; +using RateLimiter.Models; + +namespace RateLimiter.Rules; + +/// +/// Rule utilizing a simple fixed window technique to determine if a client has exceeded the maximum requests per time span. +/// +public class MaxRequestsPerTimeSpanRule : IRateLimitRule +{ + private readonly int _maxRequests; + private readonly TimeSpan _timeSpan; + private readonly ConcurrentDictionary<(string clientToken, string resource), RateLimitWindow> _timeLastCalled; + + /// + /// Initializes an instance of + /// + /// Maximum number of requests. + /// Time span through which a number of requests is accepted. + public MaxRequestsPerTimeSpanRule(int maxRequests, TimeSpan timeSpan) + { + _maxRequests = maxRequests; + _timeSpan = timeSpan; + _timeLastCalled = new ConcurrentDictionary<(string clientToken, string resource), RateLimitWindow>(); + } + + /// + /// Evaluates whether a request is allowed based on number of requests in a time span. + /// + /// + /// + public RateLimitResponse Evaluate(RateLimitContext context) + { + var requestKey = (context.ClientToken, context.ApiResource); + DateTime currentTime = DateTime.UtcNow; + + var window = _timeLastCalled.GetOrAdd(requestKey, _ => new RateLimitWindow(currentTime, currentTime + _timeSpan, 0)); + lock (window) + { + // Previous time window passed, we can restart the window for this request. + if (currentTime - window.StartTime >= _timeSpan) + { + window.StartTime = currentTime; + window.EndTime = currentTime + _timeSpan; + window.RequestCount = 0; + } + + window.RequestCount++; + + if (window.RequestCount > _maxRequests) + { + return new RateLimitResponse(false, new List { nameof(MaxRequestsPerTimeSpanRule) }); + } + + return new RateLimitResponse(true, null); + } + } +} \ No newline at end of file diff --git a/RateLimiter/Rules/MinimumTimeBetweenRequestsRule.cs b/RateLimiter/Rules/MinimumTimeBetweenRequestsRule.cs new file mode 100644 index 00000000..6ca4496a --- /dev/null +++ b/RateLimiter/Rules/MinimumTimeBetweenRequestsRule.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using RateLimiter.Interfaces; +using RateLimiter.Models; + +namespace RateLimiter.Rules; + +/// +/// Rule that defines the minimum time before the next request can be made. +/// +public class MinimumTimeBetweenRequestsRule : IRateLimitRule +{ + private readonly TimeSpan _minimumTimeSpan; + + private readonly ConcurrentDictionary<(string clientToken, string resource), DateTime> _timeLastCalled; + + /// + /// Initializes an instance of + /// + /// Minimum time span required before next request. + public MinimumTimeBetweenRequestsRule(TimeSpan minimumTimeSpan) + { + _minimumTimeSpan = minimumTimeSpan; + _timeLastCalled = new ConcurrentDictionary<(string clientToken, string resource), DateTime>(); + } + + /// + /// Evaluates if the minimum time has passed from the last call made. + /// + /// + /// + public RateLimitResponse Evaluate(RateLimitContext context) + { + var requestKey = (context.ClientToken, context.ApiResource); + var currentTime = DateTime.UtcNow; + + if (_timeLastCalled.TryGetValue(requestKey, out DateTime lastCallTime) + && ((currentTime - lastCallTime) < _minimumTimeSpan)) + { + return new RateLimitResponse(false, new List { nameof(MinimumTimeBetweenRequestsRule) }); + } + + _timeLastCalled[requestKey] = currentTime; + return new RateLimitResponse(true, null); + } +} \ No newline at end of file diff --git a/RateLimiter/Rules/OrCompositeRule.cs b/RateLimiter/Rules/OrCompositeRule.cs new file mode 100644 index 00000000..c9dc7978 --- /dev/null +++ b/RateLimiter/Rules/OrCompositeRule.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.Linq; +using RateLimiter.Interfaces; +using RateLimiter.Models; + +namespace RateLimiter.Rules; + +/// +/// Rule combining multiple rules and requires at least one to pass. (logical OR) +/// +public class OrCompositeRule : IRateLimitRule +{ + private readonly List _rateLimitRules; + + /// + /// Initializes an instance of . + /// + /// Collection of + public OrCompositeRule(IEnumerable rateLimitRules) + { + _rateLimitRules = rateLimitRules.ToList(); + } + + /// + /// Evaluates a collection of rules using logical OR. + /// + /// + /// + public RateLimitResponse Evaluate(RateLimitContext context) + { + var rejectedReasons = new List(); + var allowed = false; + + foreach (var rule in _rateLimitRules) + { + var response = rule.Evaluate(context); + if (response.Allowed) + { + allowed = response.Allowed; + } + else + { + rejectedReasons.AddRange(response.RejectedReasons); + } + } + + if (allowed) + { + return new RateLimitResponse(true, null); + } + + return new RateLimitResponse(false, rejectedReasons); + } +} \ No newline at end of file diff --git a/RateLimiter/Rules/RateLimitRuleDecorator.cs b/RateLimiter/Rules/RateLimitRuleDecorator.cs new file mode 100644 index 00000000..157c7ef8 --- /dev/null +++ b/RateLimiter/Rules/RateLimitRuleDecorator.cs @@ -0,0 +1,28 @@ +using RateLimiter.Interfaces; +using RateLimiter.Models; + +namespace RateLimiter.Rules; + +/// +/// Decorator that wraps and allows adding functionality. +/// +public abstract class RateLimitRuleDecorator : IRateLimitRule +{ + protected IRateLimitRule _rateLimitRule; + + /// + /// Initializes a new instance of the . + /// + /// + protected RateLimitRuleDecorator(IRateLimitRule rateLimitRule) + { + _rateLimitRule = rateLimitRule; + } + + /// + /// Evaluates the decorated rule. + /// + /// . + /// . + public abstract RateLimitResponse Evaluate(RateLimitContext context); +} \ No newline at end of file diff --git a/RateLimiter/Rules/RegionBasedRuleDecorator.cs b/RateLimiter/Rules/RegionBasedRuleDecorator.cs new file mode 100644 index 00000000..1f837e20 --- /dev/null +++ b/RateLimiter/Rules/RegionBasedRuleDecorator.cs @@ -0,0 +1,40 @@ +using RateLimiter.Interfaces; +using RateLimiter.Models; + +namespace RateLimiter.Rules; + +/// +/// Decorator to check if rule applies to the specified region. +/// +public class RegionBasedRuleDecorator : RateLimitRuleDecorator +{ + private readonly Region _region; + + /// + /// Initializes an instance of the . + /// + /// + /// + public RegionBasedRuleDecorator(IRateLimitRule rule, Region region) + : base(rule) + { + _region = region; + } + + /// + /// Checks if context region matches the assigned region, and then evaluates the rule. + /// If context region does not match, then we ignore the rule and return allowed. + /// + /// + /// + /// + public override RateLimitResponse Evaluate(RateLimitContext context) + { + if (context.Region == _region) + { + return _rateLimitRule.Evaluate(context); + } + + return new RateLimitResponse(true, null); + } +} \ No newline at end of file