diff --git a/RateLimiter.Core/Configuration/ConsoleLogger.cs b/RateLimiter.Core/Configuration/ConsoleLogger.cs
new file mode 100644
index 00000000..1d763d46
--- /dev/null
+++ b/RateLimiter.Core/Configuration/ConsoleLogger.cs
@@ -0,0 +1,16 @@
+using System;
+using RateLimiter.Core.Configuration.Contracts;
+
+namespace RateLimiter.Core.Configuration;
+
+public class ConsoleLogger : ILogger
+{
+ public void LogInformation(string message)
+ => Console.WriteLine($"[INFO] {DateTime.UtcNow:O} {message}");
+
+ public void LogWarning(string message)
+ => Console.WriteLine($"[WARN] {DateTime.UtcNow:O} {message}");
+
+ public void LogError(string message, Exception ex = null)
+ => Console.WriteLine($"[ERROR] {DateTime.UtcNow:O} {message} {ex?.ToString()}");
+}
\ No newline at end of file
diff --git a/RateLimiter.Core/Configuration/Contracts/ILogger.cs b/RateLimiter.Core/Configuration/Contracts/ILogger.cs
new file mode 100644
index 00000000..19fc4c6b
--- /dev/null
+++ b/RateLimiter.Core/Configuration/Contracts/ILogger.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace RateLimiter.Core.Configuration.Contracts;
+///
+/// It can be AWS.Logger.NLog or Microsoft.Extensions.Logging
+///
+public interface ILogger
+{
+ void LogInformation(string message);
+ void LogWarning(string message);
+ void LogError(string message, Exception ex = null!);
+}
\ No newline at end of file
diff --git a/RateLimiter.Core/Configuration/Contracts/IRateLimitRule.cs b/RateLimiter.Core/Configuration/Contracts/IRateLimitRule.cs
new file mode 100644
index 00000000..a6c1a823
--- /dev/null
+++ b/RateLimiter.Core/Configuration/Contracts/IRateLimitRule.cs
@@ -0,0 +1,7 @@
+namespace RateLimiter.Core.Configuration.Contracts;
+
+public interface IRateLimitRule
+{
+ bool IsAllowed(string clientToken, string resourceKey);
+ void RecordRequest(string clientToken, string resourceKey);
+}
\ No newline at end of file
diff --git a/RateLimiter.Core/Configuration/Contracts/ISystemTime.cs b/RateLimiter.Core/Configuration/Contracts/ISystemTime.cs
new file mode 100644
index 00000000..4c93998f
--- /dev/null
+++ b/RateLimiter.Core/Configuration/Contracts/ISystemTime.cs
@@ -0,0 +1,8 @@
+using System;
+
+namespace RateLimiter.Core.Configuration.Contracts;
+
+public interface ISystemTime
+{
+ DateTime GetCurrentUtcTime();
+}
\ No newline at end of file
diff --git a/RateLimiter.Core/Configuration/MockSystemTime.cs b/RateLimiter.Core/Configuration/MockSystemTime.cs
new file mode 100644
index 00000000..11688e9c
--- /dev/null
+++ b/RateLimiter.Core/Configuration/MockSystemTime.cs
@@ -0,0 +1,15 @@
+using System;
+using RateLimiter.Core.Configuration.Contracts;
+
+namespace RateLimiter.Core.Configuration;
+
+///
+/// Only for test
+///
+/// Test start time
+public class MockSystemTime(DateTime startTime) : ISystemTime
+{
+ public DateTime CurrentTime { get; set; } = startTime;
+
+ public DateTime GetCurrentUtcTime() => CurrentTime;
+}
\ No newline at end of file
diff --git a/RateLimiter.Core/Configuration/RateLimiter.cs b/RateLimiter.Core/Configuration/RateLimiter.cs
new file mode 100644
index 00000000..56cdba1a
--- /dev/null
+++ b/RateLimiter.Core/Configuration/RateLimiter.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using RateLimiter.Core.Configuration.Contracts;
+
+namespace RateLimiter.Core.Configuration;
+public class RateLimiter(ILogger logger = null!)
+{
+ private readonly Dictionary> _resourceRules = new();
+
+ public void AddRule(string resourceKey, IRateLimitRule rule)
+ {
+ if (string.IsNullOrEmpty(resourceKey))
+ throw new ArgumentNullException(nameof(resourceKey));
+
+ if (rule == null)
+ throw new ArgumentNullException(nameof(rule));
+
+ try
+ {
+ if (!_resourceRules.ContainsKey(resourceKey))
+ _resourceRules[resourceKey] = new List();
+
+ _resourceRules[resourceKey].Add(rule);
+ logger.LogInformation($"Added rule {rule.GetType().Name} for resource {resourceKey}");
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"Error adding rule for resource {resourceKey}", ex);
+ throw;
+ }
+ }
+
+
+ public bool IsRequestAllowed(string clientToken, string resourceKey)
+ {
+ if (string.IsNullOrEmpty(clientToken))
+ throw new ArgumentNullException(nameof(clientToken));
+
+ if (string.IsNullOrEmpty(resourceKey))
+ throw new ArgumentNullException(nameof(resourceKey));
+
+ try
+ {
+ if (!_resourceRules.TryGetValue(resourceKey, out var rules) || !rules.Any())
+ {
+ logger.LogWarning($"No rules configured for resource {resourceKey}");
+ return true;
+ }
+
+ foreach (var rule in rules.Where(rule => !rule.IsAllowed(clientToken, resourceKey)))
+ {
+ logger.LogWarning($"Request blocked by {rule.GetType().Name} " +
+ $"for {clientToken} on {resourceKey}");
+ return false;
+ }
+
+ foreach (var rule in rules)
+ {
+ try
+ {
+ rule.RecordRequest(clientToken, resourceKey);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"Error recording request in {rule.GetType().Name}", ex);
+ }
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ logger.LogError($"Error processing request for {clientToken} on {resourceKey}", ex);
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/RateLimiter.Core/Configuration/SystemSystemTime.cs b/RateLimiter.Core/Configuration/SystemSystemTime.cs
new file mode 100644
index 00000000..55112688
--- /dev/null
+++ b/RateLimiter.Core/Configuration/SystemSystemTime.cs
@@ -0,0 +1,11 @@
+using System;
+using RateLimiter.Core.Configuration.Contracts;
+
+namespace RateLimiter.Core.Configuration;
+///
+/// Real time usage
+///
+public class SystemSystemTime : ISystemTime
+{
+ public DateTime GetCurrentUtcTime() => DateTime.UtcNow;
+}
\ No newline at end of file
diff --git a/RateLimiter.Core/Exceptions/FixedWindowRuleException.cs b/RateLimiter.Core/Exceptions/FixedWindowRuleException.cs
new file mode 100644
index 00000000..f92f1ba9
--- /dev/null
+++ b/RateLimiter.Core/Exceptions/FixedWindowRuleException.cs
@@ -0,0 +1,8 @@
+using System;
+
+namespace RateLimiter.Core.Exceptions;
+
+public class FixedWindowRuleException : Exception
+{
+
+}
\ No newline at end of file
diff --git a/RateLimiter.Core/Exceptions/RegionBasedRuleException.cs b/RateLimiter.Core/Exceptions/RegionBasedRuleException.cs
new file mode 100644
index 00000000..91ab1268
--- /dev/null
+++ b/RateLimiter.Core/Exceptions/RegionBasedRuleException.cs
@@ -0,0 +1,8 @@
+using System;
+
+namespace RateLimiter.Core.Exceptions;
+
+public class RegionBasedRuleException : Exception
+{
+
+}
\ No newline at end of file
diff --git a/RateLimiter.Core/Exceptions/TimeSinceLastCallRuleException.cs b/RateLimiter.Core/Exceptions/TimeSinceLastCallRuleException.cs
new file mode 100644
index 00000000..4248a5bd
--- /dev/null
+++ b/RateLimiter.Core/Exceptions/TimeSinceLastCallRuleException.cs
@@ -0,0 +1,7 @@
+using System;
+
+namespace RateLimiter.Core.Exceptions;
+
+public class TimeSinceLastCallRuleException : Exception
+{
+}
\ No newline at end of file
diff --git a/RateLimiter.Core/PolicyProviders/DatabasePolicyProvider.cs b/RateLimiter.Core/PolicyProviders/DatabasePolicyProvider.cs
new file mode 100644
index 00000000..94a410e0
--- /dev/null
+++ b/RateLimiter.Core/PolicyProviders/DatabasePolicyProvider.cs
@@ -0,0 +1,8 @@
+namespace RateLimiter.Core.PolicyProviders;
+///
+/// TODO can be used for Get Policy from DB
+///
+public class DatabasePolicyProvider
+{
+
+}
\ No newline at end of file
diff --git a/RateLimiter.Core/PolicyProviders/JsonPolicyProvider.cs b/RateLimiter.Core/PolicyProviders/JsonPolicyProvider.cs
new file mode 100644
index 00000000..76f4b8af
--- /dev/null
+++ b/RateLimiter.Core/PolicyProviders/JsonPolicyProvider.cs
@@ -0,0 +1,8 @@
+namespace RateLimiter.Core.PolicyProviders;
+///
+/// TODO can be used for Get Policy from app_settings or local file
+///
+public class JsonPolicyProvider
+{
+
+}
\ No newline at end of file
diff --git a/RateLimiter/RateLimiter.csproj b/RateLimiter.Core/RateLimiter.Core.csproj
similarity index 77%
rename from RateLimiter/RateLimiter.csproj
rename to RateLimiter.Core/RateLimiter.Core.csproj
index 19962f52..b4648de7 100644
--- a/RateLimiter/RateLimiter.csproj
+++ b/RateLimiter.Core/RateLimiter.Core.csproj
@@ -1,6 +1,6 @@
- net6.0
+ net9.0
latest
enable
diff --git a/RateLimiter.Core/Rules/Combine/RegionBasedRule.cs b/RateLimiter.Core/Rules/Combine/RegionBasedRule.cs
new file mode 100644
index 00000000..297d11d0
--- /dev/null
+++ b/RateLimiter.Core/Rules/Combine/RegionBasedRule.cs
@@ -0,0 +1,75 @@
+using System;
+using RateLimiter.Core.Configuration;
+using RateLimiter.Core.Configuration.Contracts;
+
+namespace RateLimiter.Core.Rules.Combine;
+
+public class RegionBasedRule(
+ IRateLimitRule usRule,
+ IRateLimitRule euRule,
+ ILogger logger = null!) : IRateLimitRule
+{
+ private readonly IRateLimitRule _usRule = usRule ?? throw new ArgumentNullException(nameof(usRule));
+ private readonly IRateLimitRule _euRule = euRule ?? throw new ArgumentNullException(nameof(euRule));
+
+ private readonly ILogger _logger = logger ?? new ConsoleLogger();
+
+ public bool IsAllowed(string clientToken, string resourceKey)
+ {
+ try
+ {
+ var region = GetRegionFromToken(clientToken);
+ _logger.LogInformation($"Processing {region} region request for {clientToken}");
+
+ return region switch
+ {
+ "US" => _usRule.IsAllowed(clientToken, resourceKey),
+ "EU" => _euRule.IsAllowed(clientToken, resourceKey),
+ _ => HandleUnknownRegion(clientToken)
+ };
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"Error in RegionBasedRule.IsAllowed for {clientToken}", ex);
+ return false;
+ }
+ }
+
+ private bool HandleUnknownRegion(string clientToken)
+ {
+ _logger.LogWarning($"Unknown region for token {clientToken}");
+ return false;
+ }
+
+ private static string GetRegionFromToken(string token)
+ {
+ try
+ {
+ var parts = token.Split('-');
+ if (parts.Length < 1 || string.IsNullOrEmpty(parts[0]))
+ throw new FormatException("Invalid token format");
+
+ return parts[0].ToUpper();
+ }
+ catch
+ {
+ throw new FormatException($"Invalid token format: {token}");
+ }
+ }
+
+ public void RecordRequest(string clientToken, string resourceKey)
+ {
+ var region = GetRegionFromToken(clientToken);
+ switch (region)
+ {
+ case "US":
+ _usRule.RecordRequest(clientToken, resourceKey);
+ break;
+ case "EU":
+ _euRule.RecordRequest(clientToken, resourceKey);
+ break;
+ default:
+ throw new NotSupportedException($"Region {region} not supported.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/RateLimiter.Core/Rules/FixedWindowRule.cs b/RateLimiter.Core/Rules/FixedWindowRule.cs
new file mode 100644
index 00000000..520b672e
--- /dev/null
+++ b/RateLimiter.Core/Rules/FixedWindowRule.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using RateLimiter.Core.Configuration;
+using RateLimiter.Core.Configuration.Contracts;
+
+namespace RateLimiter.Core.Rules;
+public class FixedWindowRule : IRateLimitRule
+{
+ private readonly int _maxRequests;
+ private readonly TimeSpan _windowDuration;
+ private readonly ISystemTime _systemTime;
+ private readonly ILogger _logger;
+
+ private readonly Dictionary> _clientResourceData = new();
+
+ public FixedWindowRule(int maxRequests, TimeSpan windowDuration, ISystemTime systemTime = null, ILogger logger = null)
+ {
+ if (maxRequests <= 0)
+ throw new ArgumentException("Max requests must be positive", nameof(maxRequests));
+ _maxRequests = maxRequests;
+ _windowDuration = windowDuration;
+ _logger = logger;
+ _systemTime = systemTime;
+ }
+
+ public bool IsAllowed(string clientToken, string resourceKey)
+ {
+ if (string.IsNullOrEmpty(clientToken))
+ throw new ArgumentNullException(nameof(clientToken));
+
+ if (string.IsNullOrEmpty(resourceKey))
+ throw new ArgumentNullException(nameof(resourceKey));
+
+ try
+ {
+ lock (_clientResourceData)
+ {
+ var currentTime = _systemTime.GetCurrentUtcTime();
+ var data = GetOrInitializeData(clientToken, resourceKey, currentTime);
+
+ _logger.LogInformation($"Client {clientToken} resource {resourceKey}: " +
+ $"{data.Count}/{_maxRequests} requests in current window");
+
+ return data.Count < _maxRequests;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"Error in FixedWindowRule.IsAllowed for {clientToken}", ex);
+ return false;
+ }
+ }
+
+ public void RecordRequest(string clientToken, string resourceKey)
+ {
+ lock (_clientResourceData)
+ {
+ var currentTime = _systemTime.GetCurrentUtcTime();
+ var data = GetOrInitializeData(clientToken, resourceKey, currentTime);
+ data.Count++;
+ UpdateData(clientToken, resourceKey, data.Count, data.WindowStart);
+ }
+ }
+
+ private (int Count, DateTime WindowStart) GetOrInitializeData(string clientToken, string resourceKey, DateTime currentTime)
+ {
+ if (!_clientResourceData.TryGetValue(clientToken, out var resourceData))
+ {
+ resourceData = new Dictionary();
+ _clientResourceData[clientToken] = resourceData;
+ }
+
+ if (!resourceData.TryGetValue(resourceKey, out var data))
+ {
+ data = (0, currentTime);
+ resourceData[resourceKey] = data;
+ }
+
+ if (currentTime - data.WindowStart > _windowDuration)
+ {
+ data = (0, currentTime);
+ resourceData[resourceKey] = data;
+ }
+
+ return data;
+ }
+
+ private void UpdateData(string clientToken, string resourceKey, int count, DateTime windowStart)
+ {
+ _clientResourceData[clientToken][resourceKey] = (count, windowStart);
+ }
+}
\ No newline at end of file
diff --git a/RateLimiter.Core/Rules/TimeSinceLastCallRule.cs b/RateLimiter.Core/Rules/TimeSinceLastCallRule.cs
new file mode 100644
index 00000000..d4776936
--- /dev/null
+++ b/RateLimiter.Core/Rules/TimeSinceLastCallRule.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using RateLimiter.Core.Configuration.Contracts;
+
+namespace RateLimiter.Core.Rules;
+
+public class TimeSinceLastCallRule : IRateLimitRule
+{
+ private readonly TimeSpan _requiredDelay;
+ private readonly ISystemTime _systemTime;
+ private readonly Dictionary> _lastRequestTimes = new();
+ private readonly ILogger _logger;
+ public TimeSinceLastCallRule(TimeSpan requiredDelay, ISystemTime systemTime = null!, ILogger logger = null!)
+ {
+ if (requiredDelay.Ticks < 0)
+ throw new ArgumentException("Delay cannot be negative", nameof(requiredDelay));
+ _requiredDelay = requiredDelay;
+ _systemTime = systemTime;
+ _logger = logger;
+ }
+
+ public bool IsAllowed(string clientToken, string resourceKey)
+ {
+ try
+ {
+ lock (_lastRequestTimes)
+ {
+ var currentTime = _systemTime.GetCurrentUtcTime();
+
+ if (!_lastRequestTimes.TryGetValue(clientToken, out var resourceTimes) ||
+ !resourceTimes.TryGetValue(resourceKey, out var lastTime))
+ {
+ return true;
+ }
+
+ var timeSinceLastCall = currentTime - lastTime;
+ _logger.LogInformation($"Client {clientToken} last call: " +
+ $"{timeSinceLastCall.TotalSeconds}s ago (required {_requiredDelay.TotalSeconds}s)");
+
+ return timeSinceLastCall >= _requiredDelay;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError($"Error in TimeSinceLastCallRule.IsAllowed for {clientToken}", ex);
+ return false;
+ }
+ }
+
+ public void RecordRequest(string clientToken, string resourceKey)
+ {
+ lock (_lastRequestTimes)
+ {
+ if (!_lastRequestTimes.TryGetValue(clientToken, out var resourceTimes))
+ {
+ resourceTimes = new Dictionary();
+ _lastRequestTimes[clientToken] = resourceTimes;
+ }
+
+ resourceTimes[resourceKey] = _systemTime.GetCurrentUtcTime();
+ }
+ }
+}
\ No newline at end of file
diff --git a/RateLimiter.Core/StorageProviders/IDistributedStorage.cs b/RateLimiter.Core/StorageProviders/IDistributedStorage.cs
new file mode 100644
index 00000000..1673dcc3
--- /dev/null
+++ b/RateLimiter.Core/StorageProviders/IDistributedStorage.cs
@@ -0,0 +1,6 @@
+namespace RateLimiter.Core.StorageProviders;
+
+public class DistributedStorage
+{
+
+}
\ No newline at end of file
diff --git a/RateLimiter.Core/StorageProviders/InMemoryStorage.cs b/RateLimiter.Core/StorageProviders/InMemoryStorage.cs
new file mode 100644
index 00000000..b9a60a1f
--- /dev/null
+++ b/RateLimiter.Core/StorageProviders/InMemoryStorage.cs
@@ -0,0 +1,6 @@
+namespace RateLimiter.Core.StorageProviders;
+
+public class InMemoryStorage
+{
+
+}
\ No newline at end of file
diff --git a/RateLimiter.Core/StorageProviders/RedisStorage.cs b/RateLimiter.Core/StorageProviders/RedisStorage.cs
new file mode 100644
index 00000000..58a31ba3
--- /dev/null
+++ b/RateLimiter.Core/StorageProviders/RedisStorage.cs
@@ -0,0 +1,6 @@
+namespace RateLimiter.Core.StorageProviders;
+
+public class RedisStorage
+{
+
+}
\ No newline at end of file
diff --git a/RateLimiter.Tests/RateLimiter.Tests.csproj b/RateLimiter.Tests/RateLimiter.Tests.csproj
index 5cbfc4e8..ad92a93b 100644
--- a/RateLimiter.Tests/RateLimiter.Tests.csproj
+++ b/RateLimiter.Tests/RateLimiter.Tests.csproj
@@ -1,11 +1,11 @@
- net6.0
+ net9.0
latest
enable
-
+
diff --git a/RateLimiter.Tests/RateLimiterTest.cs b/RateLimiter.Tests/RateLimiterTest.cs
index 172d44a7..6290e9dd 100644
--- a/RateLimiter.Tests/RateLimiterTest.cs
+++ b/RateLimiter.Tests/RateLimiterTest.cs
@@ -1,13 +1,96 @@
-using NUnit.Framework;
+using System;
+using NUnit.Framework;
+using RateLimiter.Core.Configuration;
+using RateLimiter.Core.Rules;
+using RateLimiter.Core.Rules.Combine;
namespace RateLimiter.Tests;
[TestFixture]
public class RateLimiterTest
{
- [Test]
- public void Example()
- {
- Assert.That(true, Is.True);
- }
+ [Test]
+ public void FixedWindowRule_AllowsMaxRequestsPerWindow()
+ {
+ var startTime = new DateTime(2025, 1, 28, 1, 1, 1, DateTimeKind.Utc);
+ var logger = new ConsoleLogger();
+ var clock = new MockSystemTime(startTime);
+ var rule = new FixedWindowRule(2, TimeSpan.FromMinutes(1), clock, logger);
+ var clientToken = "client1";
+ var resourceKey = "resource1";
+
+ Assert.IsTrue(rule.IsAllowed(clientToken, resourceKey));
+ rule.RecordRequest(clientToken, resourceKey);
+
+ Assert.IsTrue(rule.IsAllowed(clientToken, resourceKey));
+ rule.RecordRequest(clientToken, resourceKey);
+
+ Assert.IsFalse(rule.IsAllowed(clientToken, resourceKey));
+
+ clock.CurrentTime = startTime.AddMinutes(1);
+ Assert.IsFalse(rule.IsAllowed(clientToken, resourceKey));
+ }
+
+ [Test]
+ public void TimeSinceLastCallRule_AllowsRequestAfterDelay()
+ {
+ var logger = new ConsoleLogger();
+ var startTime = new DateTime(2025, 1, 28, 2, 2, 2, DateTimeKind.Utc);
+ var clock = new MockSystemTime(startTime);
+ var rule = new TimeSinceLastCallRule(TimeSpan.FromSeconds(10), clock, logger);
+ var clientToken = "client1";
+ var resourceKey = "resource1";
+
+ Assert.IsTrue(rule.IsAllowed(clientToken, resourceKey));
+ rule.RecordRequest(clientToken, resourceKey);
+
+ clock.CurrentTime = startTime.AddSeconds(5);
+ Assert.IsFalse(rule.IsAllowed(clientToken, resourceKey));
+
+ clock.CurrentTime = startTime.AddSeconds(10);
+ Assert.IsTrue(rule.IsAllowed(clientToken, resourceKey));
+ }
+
+ [Test]
+ public void RateLimiter_CombinedRules_BothMustAllow()
+ {
+ var startTime = new DateTime(2025, 1, 28, 3, 3, 3, DateTimeKind.Utc);
+ var clock = new MockSystemTime(startTime);
+ var logger = new ConsoleLogger();
+ var rateLimiter = new Core.Configuration.RateLimiter(logger);
+ rateLimiter.AddRule("resource1", new FixedWindowRule(2, TimeSpan.FromMinutes(1), clock, logger));
+ rateLimiter.AddRule("resource1", new TimeSinceLastCallRule(TimeSpan.FromSeconds(10), clock, logger));
+
+ var clientToken = "client1";
+ Assert.IsTrue(rateLimiter.IsRequestAllowed(clientToken, "resource1"));
+ clock.CurrentTime = startTime.AddSeconds(5);
+ Assert.IsFalse(rateLimiter.IsRequestAllowed(clientToken, "resource1"));
+
+ clock.CurrentTime = startTime.AddSeconds(10);
+ Assert.IsTrue(rateLimiter.IsRequestAllowed(clientToken, "resource1"));
+ }
+
+ [Test]
+ public void RegionBasedRule_UsesDifferentRulesBasedOnTokenRegion()
+ {
+ var logger = new ConsoleLogger();
+ var startTime = new DateTime(2025, 1, 28, 4, 4, 4, DateTimeKind.Utc);
+ var clock = new MockSystemTime(startTime);
+ var usRule = new FixedWindowRule(2, TimeSpan.FromMinutes(1), clock, logger);
+ var euRule = new TimeSinceLastCallRule(TimeSpan.FromSeconds(10), clock, logger);
+ var regionRule = new RegionBasedRule(usRule, euRule);
+
+ var rateLimiter = new Core.Configuration.RateLimiter(logger);
+ rateLimiter.AddRule("resource1", regionRule);
+
+ var usToken = "US-123";
+ var euToken = "EU-321";
+
+ Assert.IsTrue(rateLimiter.IsRequestAllowed(usToken, "resource1"));
+ Assert.IsTrue(rateLimiter.IsRequestAllowed(usToken, "resource1"));
+ Assert.IsFalse(rateLimiter.IsRequestAllowed(usToken, "resource1"));
+
+ Assert.IsTrue(rateLimiter.IsRequestAllowed(euToken, "resource1"));
+ Assert.IsFalse(rateLimiter.IsRequestAllowed(euToken, "resource1"));
+ }
}
\ No newline at end of file
diff --git a/RateLimiter.sln b/RateLimiter.sln
index 626a7bfa..1caab340 100644
--- a/RateLimiter.sln
+++ b/RateLimiter.sln
@@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.15
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiter", "RateLimiter\RateLimiter.csproj", "{36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiter.Core", "RateLimiter.Core\RateLimiter.Core.csproj", "{36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiter.Tests", "RateLimiter.Tests\RateLimiter.Tests.csproj", "{C4F9249B-010E-46BE-94B8-DD20D82F1E60}"
EndProject