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