diff --git a/RateLimiter.DataStore/Entities/RequestLog.cs b/RateLimiter.DataStore/Entities/RequestLog.cs
new file mode 100644
index 00000000..2662bed0
--- /dev/null
+++ b/RateLimiter.DataStore/Entities/RequestLog.cs
@@ -0,0 +1,19 @@
+namespace RateLimiter.DataStore.Entities
+{
+ public class RequestLog
+ {
+ public int RequestLogId { get; set; }
+
+ public string ClientToken { get; set; }
+
+ public string ResourceName { get; set; }
+
+ public string TimeStampString { get; set; } = string.Empty;
+
+ public int AccessCounts { get; set; } = 1;
+
+ public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
+
+ public DateTime? UpdatedTime { get; set; } = null;
+ }
+}
\ No newline at end of file
diff --git a/RateLimiter.DataStore/InMemoryDataContext.cs b/RateLimiter.DataStore/InMemoryDataContext.cs
new file mode 100644
index 00000000..888bcc7f
--- /dev/null
+++ b/RateLimiter.DataStore/InMemoryDataContext.cs
@@ -0,0 +1,25 @@
+using Microsoft.EntityFrameworkCore;
+using RateLimiter.DataStore.Entities;
+using RateLimiter.DataStore.Interfaces;
+
+namespace RateLimiter.DataStore
+{
+ ///
+ /// In-Memory data context implementation
+ ///
+ public class InMemoryDataContext : DbContext, IDataContext
+ {
+ public DbSet RequestLogs { get; set; }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder options)
+ {
+ // In-Memory database used for simplicity
+ options.UseInMemoryDatabase("RateLimiterDb");
+ }
+
+ void IDataContext.SaveChanges()
+ {
+ base.SaveChanges();
+ }
+ }
+}
diff --git a/RateLimiter.DataStore/Interfaces/IDataContext.cs b/RateLimiter.DataStore/Interfaces/IDataContext.cs
new file mode 100644
index 00000000..e3314612
--- /dev/null
+++ b/RateLimiter.DataStore/Interfaces/IDataContext.cs
@@ -0,0 +1,12 @@
+using Microsoft.EntityFrameworkCore;
+using RateLimiter.DataStore.Entities;
+
+namespace RateLimiter.DataStore.Interfaces
+{
+ public interface IDataContext
+ {
+ public DbSet RequestLogs { get; set; }
+
+ void SaveChanges();
+ }
+}
diff --git a/RateLimiter.DataStore/Interfaces/IRateLimitRepository.cs b/RateLimiter.DataStore/Interfaces/IRateLimitRepository.cs
new file mode 100644
index 00000000..039ead40
--- /dev/null
+++ b/RateLimiter.DataStore/Interfaces/IRateLimitRepository.cs
@@ -0,0 +1,23 @@
+using RateLimiter.DataStore.Entities;
+
+namespace RateLimiter.DataStore.Interfaces
+{
+ public interface IRateLimitRepository
+ {
+ Task AddRequestLog(RequestLog requestLog);
+
+ Task DeleteExpiredLogs(int pastHours);
+
+ Task UpdateRequestLogCounts(RequestLog requestLog);
+
+ Task GetLogByTimeStamp(
+ string token,
+ string resourceName,
+ string timeStamp);
+
+ Task> GetLogsWithinTimeSpan(
+ string token,
+ string resourceName,
+ TimeSpan timeSpan);
+ }
+}
diff --git a/RateLimiter.DataStore/RateLimitRepository.cs b/RateLimiter.DataStore/RateLimitRepository.cs
new file mode 100644
index 00000000..5a36fa9f
--- /dev/null
+++ b/RateLimiter.DataStore/RateLimitRepository.cs
@@ -0,0 +1,64 @@
+using RateLimiter.DataStore.Entities;
+using RateLimiter.DataStore.Interfaces;
+
+namespace RateLimiter.DataStore
+{
+ public class RateLimitRepository : IRateLimitRepository
+ {
+ private IDataContext _context;
+
+ public RateLimitRepository(IDataContext dataContext)
+ {
+ _context = dataContext;
+ }
+
+ public async Task AddRequestLog(RequestLog requestLog)
+ {
+ _context.RequestLogs.Add(requestLog);
+ _context.SaveChanges();
+ }
+
+ ///
+ /// Call this to save any changes made in Entity
+ ///
+ public async Task UpdateRequestLogCounts(RequestLog requestLog)
+ {
+ requestLog.AccessCounts += 1;
+ requestLog.UpdatedTime = DateTime.UtcNow;
+ _context.SaveChanges();
+ }
+
+ ///
+ /// Call this to remove older logs
+ ///
+ ///
+ public async Task DeleteExpiredLogs(int pastHours)
+ {
+ var oldLogs = _context.RequestLogs
+ .Where(x => x.CreatedTime < DateTime.Now.AddHours(pastHours));
+
+ _context.RequestLogs.RemoveRange(oldLogs);
+ _context.SaveChanges();
+ }
+
+ public async Task GetLogByTimeStamp(string token, string resourceName, string timeStamp)
+ {
+ return _context.RequestLogs
+ .Where(x =>
+ x.ClientToken == token &&
+ x.ResourceName == resourceName &&
+ x.TimeStampString == timeStamp)
+ .FirstOrDefault();
+ }
+
+ public async Task> GetLogsWithinTimeSpan(string token, string resourceName, TimeSpan timeSpan)
+ {
+ return _context.RequestLogs
+ .Where(x =>
+ x.ClientToken == token &&
+ x.ResourceName == resourceName.Trim() &&
+ x.CreatedTime > DateTime.UtcNow.AddTicks(-timeSpan.Ticks))
+ .ToList();
+ }
+ }
+}
diff --git a/RateLimiter.DataStore/RateLimiter.DataStore.csproj b/RateLimiter.DataStore/RateLimiter.DataStore.csproj
new file mode 100644
index 00000000..74cf24e9
--- /dev/null
+++ b/RateLimiter.DataStore/RateLimiter.DataStore.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/RateLimiter.Services/Enums/RuleType.cs b/RateLimiter.Services/Enums/RuleType.cs
new file mode 100644
index 00000000..de3095fb
--- /dev/null
+++ b/RateLimiter.Services/Enums/RuleType.cs
@@ -0,0 +1,9 @@
+namespace RateLimiter.Services.Enums
+{
+ public enum RuleType
+ {
+ USA = 0,
+ Europe = 1,
+ Mixed = 2
+ }
+}
diff --git a/RateLimiter.Services/EuropeRuleService.cs b/RateLimiter.Services/EuropeRuleService.cs
new file mode 100644
index 00000000..5b24f2dd
--- /dev/null
+++ b/RateLimiter.Services/EuropeRuleService.cs
@@ -0,0 +1,47 @@
+using RateLimiter.DataStore.Entities;
+using RateLimiter.DataStore.Interfaces;
+using RateLimiter.Services.Interfaces;
+using RateLimiter.Services.Rule;
+
+namespace RateLimiter.Services
+{
+ public class EuropeRuleService : IRuleService
+ {
+ private readonly IRateLimitRepository _rateLimitRepository;
+ private readonly RuleOptions _ruleOptions;
+
+ public EuropeRuleService(RuleOptions ruleOptions, IRateLimitRepository rateLimitRepository)
+ {
+ _rateLimitRepository = rateLimitRepository;
+ _ruleOptions = ruleOptions;
+ }
+
+ public async Task DeleteOldRequestLogs(int pastHours)
+ {
+ await _rateLimitRepository.DeleteExpiredLogs(pastHours);
+ }
+
+ public async Task HasRateLimitExceeded(string token, string resourceName)
+ {
+ var currentLogs = await _rateLimitRepository.GetLogsWithinTimeSpan(
+ token,
+ resourceName,
+ _ruleOptions.TimeSpan);
+
+ if (currentLogs.Count > _ruleOptions.MaxCounts ||
+ currentLogs.Sum(x=>x.AccessCounts) > _ruleOptions.MaxCounts)
+ {
+ return true;
+ }
+
+ _ = _rateLimitRepository.AddRequestLog(new RequestLog()
+ {
+ ClientToken = token,
+ ResourceName = resourceName,
+ TimeStampString = DateTime.UtcNow.ToString(_ruleOptions.TimeStampFormat)
+ });
+
+ return false;
+ }
+ }
+}
diff --git a/RateLimiter.Services/Factory/ServiceFactory.cs b/RateLimiter.Services/Factory/ServiceFactory.cs
new file mode 100644
index 00000000..551bc657
--- /dev/null
+++ b/RateLimiter.Services/Factory/ServiceFactory.cs
@@ -0,0 +1,37 @@
+using RateLimiter.DataStore;
+using RateLimiter.Services.Interfaces;
+using RateLimiter.Services.Rule;
+
+namespace RateLimiter.Services.Factory
+{
+ public static class ServiceFactory
+ {
+ public static IRuleService CreateRuleService(RuleOptions ruleOptions)
+ {
+ var reposiotry = new RateLimitRepository(new InMemoryDataContext());
+
+ if (ruleOptions.RuleType == Enums.RuleType.USA)
+ {
+ return new USARuleService(
+ ruleOptions,
+ reposiotry);
+ }
+ else if (ruleOptions.RuleType == Enums.RuleType.Europe)
+ {
+ return new EuropeRuleService(
+ ruleOptions,
+ reposiotry);
+ }
+ else if (ruleOptions.RuleType == Enums.RuleType.Mixed)
+ {
+ return new MixedRuleService(
+ ruleOptions,
+ reposiotry);
+ }
+ else
+ {
+ throw new Exception("Rule not supported.");
+ }
+ }
+ }
+}
diff --git a/RateLimiter.Services/Interfaces/IRuleService.cs b/RateLimiter.Services/Interfaces/IRuleService.cs
new file mode 100644
index 00000000..62397c61
--- /dev/null
+++ b/RateLimiter.Services/Interfaces/IRuleService.cs
@@ -0,0 +1,9 @@
+namespace RateLimiter.Services.Interfaces
+{
+ public interface IRuleService
+ {
+ Task DeleteOldRequestLogs(int pastHours);
+
+ Task HasRateLimitExceeded(string token, string resourceName);
+ }
+}
diff --git a/RateLimiter.Services/MixedRuleService.cs b/RateLimiter.Services/MixedRuleService.cs
new file mode 100644
index 00000000..0aeb163b
--- /dev/null
+++ b/RateLimiter.Services/MixedRuleService.cs
@@ -0,0 +1,66 @@
+using RateLimiter.DataStore.Entities;
+using RateLimiter.DataStore.Interfaces;
+using RateLimiter.Services.Interfaces;
+using RateLimiter.Services.Rule;
+
+namespace RateLimiter.Services
+{
+ public class MixedRuleService : IRuleService
+ {
+ private readonly IRateLimitRepository _rateLimitRepository;
+ private readonly RuleOptions _ruleOptions;
+
+ public MixedRuleService(RuleOptions ruleOptions, IRateLimitRepository rateLimitRepository)
+ {
+ _rateLimitRepository = rateLimitRepository;
+ _ruleOptions = ruleOptions;
+ }
+
+ public async Task DeleteOldRequestLogs(int pastHours)
+ {
+ await _rateLimitRepository.DeleteExpiredLogs(pastHours);
+ }
+
+ public async Task HasRateLimitExceeded(string token, string resourceName)
+ {
+ var timeStampString = DateTime.UtcNow.ToString(_ruleOptions.TimeStampFormat);
+
+ var currentLog = await _rateLimitRepository.GetLogByTimeStamp(
+ token,
+ resourceName,
+ timeStampString);
+
+ if (currentLog is { })
+ {
+ if (currentLog.AccessCounts >= _ruleOptions.MaxCounts)
+ {
+ return true;
+ }
+
+ _ = _rateLimitRepository.UpdateRequestLogCounts(currentLog);
+ }
+ else
+ {
+ var currentLogs = await _rateLimitRepository.GetLogsWithinTimeSpan(
+ token,
+ resourceName,
+ _ruleOptions.TimeSpan);
+
+ if (currentLogs.Count > _ruleOptions.MaxCounts ||
+ currentLogs.Sum(x => x.AccessCounts) > _ruleOptions.MaxCounts)
+ {
+ return true;
+ }
+ }
+
+ _ = _rateLimitRepository.AddRequestLog(new RequestLog()
+ {
+ ClientToken = token,
+ ResourceName = resourceName,
+ TimeStampString = timeStampString
+ });
+
+ return false;
+ }
+ }
+}
diff --git a/RateLimiter.Services/RateLimiter.Services.csproj b/RateLimiter.Services/RateLimiter.Services.csproj
new file mode 100644
index 00000000..29b9f6f9
--- /dev/null
+++ b/RateLimiter.Services/RateLimiter.Services.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/RateLimiter.Services/Rule/RuleOptions.cs b/RateLimiter.Services/Rule/RuleOptions.cs
new file mode 100644
index 00000000..a19adb86
--- /dev/null
+++ b/RateLimiter.Services/Rule/RuleOptions.cs
@@ -0,0 +1,28 @@
+using RateLimiter.Services.Enums;
+
+namespace RateLimiter.Services.Rule
+{
+ public class RuleOptions
+ {
+ ///
+ /// Default to 10
+ ///
+ public int MaxCounts { get; set; } = 10;
+
+ ///
+ /// Default to year month day hour and minutes
+ ///
+ public string TimeStampFormat { get; set; } = "yyyyMMddHHmm";
+
+ ///
+ /// Default to 0
+ ///
+ public TimeSpan TimeSpan { get; set; } = TimeSpan.FromTicks(10);
+
+
+ ///
+ /// Default to USA
+ ///
+ public RuleType RuleType { get; set; } = RuleType.USA;
+ }
+}
diff --git a/RateLimiter.Services/USARuleService.cs b/RateLimiter.Services/USARuleService.cs
new file mode 100644
index 00000000..5f0883c3
--- /dev/null
+++ b/RateLimiter.Services/USARuleService.cs
@@ -0,0 +1,55 @@
+using RateLimiter.DataStore.Entities;
+using RateLimiter.DataStore.Interfaces;
+using RateLimiter.Services.Interfaces;
+using RateLimiter.Services.Rule;
+
+namespace RateLimiter.Services
+{
+ public class USARuleService : IRuleService
+ {
+ private readonly IRateLimitRepository _rateLimitRepository;
+ private readonly RuleOptions _ruleOptions;
+
+ public USARuleService(RuleOptions ruleOptions, IRateLimitRepository rateLimitRepository)
+ {
+ _rateLimitRepository = rateLimitRepository;
+ _ruleOptions = ruleOptions;
+ }
+
+ public async Task DeleteOldRequestLogs(int pastHours)
+ {
+ await _rateLimitRepository.DeleteExpiredLogs(pastHours);
+ }
+
+ public async Task HasRateLimitExceeded(string token, string resourceName)
+ {
+ var timeStampString = DateTime.UtcNow.ToString(_ruleOptions.TimeStampFormat);
+
+ var currentLog = await _rateLimitRepository.GetLogByTimeStamp(
+ token,
+ resourceName,
+ timeStampString);
+
+ if (currentLog is { })
+ {
+ if (currentLog.AccessCounts >= _ruleOptions.MaxCounts)
+ {
+ return true;
+ }
+
+ _ = _rateLimitRepository.UpdateRequestLogCounts(currentLog);
+ }
+ else
+ {
+ _ = _rateLimitRepository.AddRequestLog(new RequestLog()
+ {
+ ClientToken = token,
+ ResourceName = resourceName,
+ TimeStampString = timeStampString,
+ });
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/RateLimiter.Tests/EuropeRateLimiterTest.cs b/RateLimiter.Tests/EuropeRateLimiterTest.cs
new file mode 100644
index 00000000..97956df8
--- /dev/null
+++ b/RateLimiter.Tests/EuropeRateLimiterTest.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Moq;
+using NUnit.Framework;
+using RateLimiter.DataStore;
+using RateLimiter.DataStore.Entities;
+using RateLimiter.Interfaces;
+using RateLimiter.Services.Rule;
+
+namespace RateLimiter.Tests
+{
+ [TestFixture]
+ public class EuropeRateLimiterTest
+ {
+ private readonly RuleOptions _ruleOptions;
+ private IRateLimiter _subject;
+
+ public EuropeRateLimiterTest()
+ {
+ var mockLogger = new Mock>();
+
+ _ruleOptions = new RuleOptions()
+ {
+ MaxCounts = 5,
+ TimeSpan = TimeSpan.FromSeconds(30),
+ RuleType = Services.Enums.RuleType.Europe
+ };
+
+ _subject = new RateLimiter(mockLogger.Object, _ruleOptions);
+ }
+
+ [Test]
+ public async Task EuropeRateLimiterTest_FirstRequest_ShouldAlwaysReturnTrue()
+ {
+ // Arrange
+ var resource = "www.myapi.com/login";
+ var token = "EU_" + Guid.NewGuid();
+
+ // Act
+ var isRequestAllowed = await _subject.IsRequestAllowed(token, resource);
+
+ // Assert
+ Assert.AreEqual(true, isRequestAllowed);
+ }
+
+ [Test]
+ public async Task EuropeRateLimiterTest_MultipleRequestsInTimeSpan_ShouldReturnFalse()
+ {
+ // Arrange
+ var rateLimitRepository = new RateLimitRepository(new InMemoryDataContext());
+ var resource = "www.myapi.com/login";
+ var token = "EU_" + Guid.NewGuid();
+
+ for (var i = 0; i < 10; i++)
+ {
+ await rateLimitRepository.AddRequestLog(new RequestLog()
+ {
+ ClientToken = token,
+ ResourceName = resource
+ });
+ }
+
+ // Act
+ var isRequestAllowed = await _subject.IsRequestAllowed(token, resource);
+
+ // Assert
+ Assert.AreEqual(false, isRequestAllowed);
+ }
+ }
+}
diff --git a/RateLimiter.Tests/MixedRateLimiterTest.cs b/RateLimiter.Tests/MixedRateLimiterTest.cs
new file mode 100644
index 00000000..6924a4d2
--- /dev/null
+++ b/RateLimiter.Tests/MixedRateLimiterTest.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Moq;
+using NUnit.Framework;
+using RateLimiter.DataStore;
+using RateLimiter.DataStore.Entities;
+using RateLimiter.Interfaces;
+using RateLimiter.Services.Rule;
+
+namespace RateLimiter.Tests
+{
+ [TestFixture]
+ public class MixedRateLimiterTest
+ {
+ private readonly RuleOptions _ruleOptions;
+ private IRateLimiter _subject;
+
+ public MixedRateLimiterTest()
+ {
+ var mockLogger = new Mock>();
+
+ _ruleOptions = new RuleOptions()
+ {
+ MaxCounts = 5,
+ RuleType = Services.Enums.RuleType.Mixed,
+ TimeStampFormat = "yyyyMMddHHmm",
+ TimeSpan = TimeSpan.FromSeconds(30)
+ };
+
+ _subject = new RateLimiter(mockLogger.Object, _ruleOptions);
+ }
+
+ [Test]
+ public async Task MixedRateLimiterTest_TestingTimeStamp_ShouldHandleIt()
+ {
+ // Arrange
+ var resource = "www.myapi.com/customer";
+ var token = "USA_" + Guid.NewGuid();
+ var isRequestAllowed = true;
+ var requestCount = 0;
+
+ // Act
+ for (; requestCount < 10 && isRequestAllowed; requestCount++)
+ {
+ //await _subject.IsRequestAllowed(token, resource);
+ isRequestAllowed = await _subject.IsRequestAllowed(token, resource);
+ }
+
+ // Assert
+ Assert.That(isRequestAllowed, Is.False);
+ Assert.AreEqual(requestCount, _ruleOptions.MaxCounts + 1);
+ }
+
+ [Test]
+ public async Task MixedRateLimiterTest_TestingTimeSpan_ShouldHandleIt()
+ {
+ // Arrange
+ var rateLimitRepository = new RateLimitRepository(new InMemoryDataContext());
+ var resource = "www.myapi.com/login";
+ var token = "EU_" + Guid.NewGuid();
+
+ for (var i = 0; i < 10; i++)
+ {
+ await rateLimitRepository.AddRequestLog(new RequestLog()
+ {
+ ClientToken = token,
+ ResourceName = resource
+ });
+ }
+
+ // Act
+ var isRequestAllowed = await _subject.IsRequestAllowed(token, resource);
+
+ // Assert
+ Assert.AreEqual(false, isRequestAllowed);
+ }
+ }
+}
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
index 172d44a7..e8ec194d 100644
--- a/RateLimiter.Tests/RateLimiterTest.cs
+++ b/RateLimiter.Tests/RateLimiterTest.cs
@@ -1,13 +1,59 @@
-using NUnit.Framework;
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Moq;
+using NUnit.Framework;
+using RateLimiter.Interfaces;
+using RateLimiter.Services.Interfaces;
namespace RateLimiter.Tests;
[TestFixture]
public class RateLimiterTest
{
+ Mock> _mockLogger;
+ Mock _mockRuleService;
+ IRateLimiter _subject;
+
+ public RateLimiterTest()
+ {
+ _mockLogger = new Mock>();
+ _mockRuleService = new Mock();
+
+ _subject = new RateLimiter(_mockLogger.Object, _mockRuleService.Object);
+ }
+
[Test]
- public void Example()
+ public async Task RateLimiterTest_MockedServiceResponse_ShouldNotAllowRequest()
{
- Assert.That(true, Is.True);
+ // Arrange
+ var resourceAPI = "www.myapi.com/user";
+ var authToken = "75938394-f733-4b88-9741-961b5f71b815";
+
+ _mockRuleService.Setup(x => x.HasRateLimitExceeded(resourceAPI, authToken))
+ .ReturnsAsync(true);
+
+ // Act
+ var limitExceed = await _subject.IsRequestAllowed(resourceAPI, authToken);
+
+ // Assert
+ Assert.That(limitExceed, Is.False);
}
+
+ [Test]
+ public async Task RateLimiterTest_MockedServiceResponse_ShouldAllowRequest()
+ {
+ // Arrange
+ var resource = "www.myapi.com/customer";
+ var token = $"{Guid.NewGuid()}";
+
+ _mockRuleService.Setup(x => x.HasRateLimitExceeded(resource, token))
+ .ReturnsAsync(false);
+
+ // Act
+ var limitExceed = await _subject.IsRequestAllowed(resource, token);
+
+ // Assert
+ Assert.That(limitExceed, Is.True);
+ }
}
\ No newline at end of file
diff --git a/RateLimiter.Tests/UsaRateLimiterTest.cs b/RateLimiter.Tests/UsaRateLimiterTest.cs
new file mode 100644
index 00000000..181e64e8
--- /dev/null
+++ b/RateLimiter.Tests/UsaRateLimiterTest.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Moq;
+using NUnit.Framework;
+using RateLimiter.Interfaces;
+using RateLimiter.Services.Rule;
+
+namespace RateLimiter.Tests
+{
+ [TestFixture]
+ public class UsaRateLimiterTest
+ {
+ private readonly RuleOptions _ruleOptions;
+ private IRateLimiter _subject;
+
+ public UsaRateLimiterTest()
+ {
+ var mockLogger = new Mock>();
+
+ _ruleOptions = new RuleOptions()
+ {
+ MaxCounts = 5,
+ RuleType = Services.Enums.RuleType.USA,
+ TimeStampFormat = "yyyyMMddHHmm"
+ };
+
+ _subject = new RateLimiter(mockLogger.Object, _ruleOptions);
+ }
+
+ [Test]
+ public async Task UsaRateLimiterTest_FirstRequest_ShouldAlwaysReturnTrue()
+ {
+ // Arrange
+ var resource = "www.myapi.com/user";
+ var token = "USA_" + Guid.NewGuid();
+
+ // Act
+ var isRequestAllowed = await _subject.IsRequestAllowed(token, resource);
+
+ // Assert
+ Assert.That(isRequestAllowed, Is.True);
+ }
+
+ [Test]
+ public async Task UsaRateLimiterTest_WhenMaxCountsExcessed_ShouldReturnFalse()
+ {
+ // Arrange
+ var resource = "www.myapi.com/customer";
+ var token = "USA_" + Guid.NewGuid();
+ var isRequestAllowed = true;
+ var requestCount = 0;
+
+ // Act
+ for (; requestCount < 10 && isRequestAllowed; requestCount++)
+ {
+ //await _subject.IsRequestAllowed(token, resource);
+ isRequestAllowed = await _subject.IsRequestAllowed(token, resource);
+ }
+
+ // Assert
+ Assert.That(isRequestAllowed, Is.False);
+ Assert.AreEqual(requestCount, _ruleOptions.MaxCounts+1);
+ }
+ }
+}
diff --git a/RateLimiter.sln b/RateLimiter.sln
index 626a7bfa..42d22e92 100644
--- a/RateLimiter.sln
+++ b/RateLimiter.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio 15
-VisualStudioVersion = 15.0.26730.15
+# Visual Studio Version 17
+VisualStudioVersion = 17.12.35707.178
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiter", "RateLimiter\RateLimiter.csproj", "{36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}"
EndProject
@@ -12,6 +12,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.md = README.md
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiter.DataStore", "RateLimiter.DataStore\RateLimiter.DataStore.csproj", "{69025A01-8E89-4EF2-A4F1-4FF79D1DA6A8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiter.Services", "RateLimiter.Services\RateLimiter.Services.csproj", "{FB2796E2-6603-4924-8DAC-2A6DB1D93BA5}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -26,6 +30,14 @@ Global
{C4F9249B-010E-46BE-94B8-DD20D82F1E60}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C4F9249B-010E-46BE-94B8-DD20D82F1E60}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C4F9249B-010E-46BE-94B8-DD20D82F1E60}.Release|Any CPU.Build.0 = Release|Any CPU
+ {69025A01-8E89-4EF2-A4F1-4FF79D1DA6A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {69025A01-8E89-4EF2-A4F1-4FF79D1DA6A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {69025A01-8E89-4EF2-A4F1-4FF79D1DA6A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {69025A01-8E89-4EF2-A4F1-4FF79D1DA6A8}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FB2796E2-6603-4924-8DAC-2A6DB1D93BA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FB2796E2-6603-4924-8DAC-2A6DB1D93BA5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FB2796E2-6603-4924-8DAC-2A6DB1D93BA5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FB2796E2-6603-4924-8DAC-2A6DB1D93BA5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/RateLimiter/Interfaces/IRateLimiter.cs b/RateLimiter/Interfaces/IRateLimiter.cs
new file mode 100644
index 00000000..5d1f592a
--- /dev/null
+++ b/RateLimiter/Interfaces/IRateLimiter.cs
@@ -0,0 +1,11 @@
+using System.Threading.Tasks;
+
+namespace RateLimiter.Interfaces
+{
+ public interface IRateLimiter
+ {
+ Task ClearOldRequestLogs(int pastHours);
+
+ Task IsRequestAllowed(string token, string resourceName);
+ }
+}
diff --git a/RateLimiter/RateLimiter.cs b/RateLimiter/RateLimiter.cs
new file mode 100644
index 00000000..a833c196
--- /dev/null
+++ b/RateLimiter/RateLimiter.cs
@@ -0,0 +1,71 @@
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using RateLimiter.Interfaces;
+using RateLimiter.Services.Factory;
+using RateLimiter.Services.Interfaces;
+using RateLimiter.Services.Rule;
+
+namespace RateLimiter
+{
+ ///
+ /// RateLimiter class
+ ///
+ public class RateLimiter : IRateLimiter
+ {
+ private readonly ILogger _logger;
+ private readonly IRuleService _ruleService;
+
+ ///
+ /// Pass rule service instance
+ ///
+ ///
+ ///
+ public RateLimiter(ILogger logger, IRuleService ruleService)
+ {
+ _logger = logger;
+ _ruleService = ruleService;
+ }
+
+ ///
+ /// Pass Rule Options, which will be used by factory to create a rule service
+ ///
+ ///
+ ///
+ public RateLimiter(ILogger logger, RuleOptions ruleOptions)
+ {
+ _logger = logger;
+ _ruleService = ServiceFactory.CreateRuleService(ruleOptions);
+ }
+
+ ///
+ /// Call this periodically to delete older request logs
+ ///
+ ///
+ ///
+ public async Task ClearOldRequestLogs(int pastHours = 1)
+ {
+ await _ruleService.DeleteOldRequestLogs(pastHours);
+ }
+
+ ///
+ /// Check wheather given user token and resource access is allowed
+ ///
+ ///
+ ///
+ ///
+ public async Task IsRequestAllowed(string token, string resourceName)
+ {
+ _logger.LogInformation($"Checking resource rate limit for {token}\r\n" +
+ $"Resource - {resourceName}");
+
+ if (await _ruleService.HasRateLimitExceeded(token, resourceName))
+ {
+ _logger.LogWarning($"Rate limit exceeded for {token}.");
+
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/RateLimiter/RateLimiter.csproj b/RateLimiter/RateLimiter.csproj
index 19962f52..0d514864 100644
--- a/RateLimiter/RateLimiter.csproj
+++ b/RateLimiter/RateLimiter.csproj
@@ -4,4 +4,10 @@
latest
enable
+
+
+
+
+
+
\ No newline at end of file