diff --git a/RateLimiter.Tests/RateLimiterTest.cs b/RateLimiter.Tests/RateLimiterTest.cs index 172d44a7..81f10a12 100644 --- a/RateLimiter.Tests/RateLimiterTest.cs +++ b/RateLimiter.Tests/RateLimiterTest.cs @@ -1,13 +1,211 @@ -using NUnit.Framework; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; +using NUnit.Framework; +using RateLimiter.Domain.Models; +using System; +using System.Threading.Tasks; namespace RateLimiter.Tests; [TestFixture] public class RateLimiterTest { - [Test] - public void Example() - { - Assert.That(true, Is.True); - } + [Test] + public async Task RequestsTimespanPerLastRequestRuleFailTest() + { + var configurations = new Domain.Models.Configurations + { + RequestsPerTimespan = 5, + RequestTimespan = 5, + TimespanSinceLastCall = 5 + }; + + var id = Guid.NewGuid(); + var request = new Domain.Models.RateLimiterRequest + { + Country = Domain.Enumerations.Contries.US, + Id = id, + RequestDate = DateTime.UtcNow + }; + + var updateDate = DateTime.UtcNow.AddSeconds(-3); + var stats = new Domain.Models.RateLimiterStats + { + Id = id, + LastRequestDateTime = updateDate, + NumberOfRequestsInTimespan = 0 + }; + + var result = new Domain.Models.RulesResult + { + updatedRateLimiterStats = stats + }; + + var rule = new Domain.Rules.RequestsTimespanPerLastRequestRule(); + var results = await rule.ExecuteRule(request, configurations, stats); + + Assert.That(results.Message == "Timespan has not expired."); + Assert.That(results.Status == false); + Assert.That(results.updatedRateLimiterStats.LastRequestDateTime == updateDate); + Assert.That(results.updatedRateLimiterStats.NumberOfRequestsInTimespan == 0); + } + + [Test] + public async Task RequestsTimespanPerLastRequestRuleOverTimespanPassTest() + { + var configurations = new Domain.Models.Configurations + { + RequestsPerTimespan = 5, + RequestTimespan = 5, + TimespanSinceLastCall = 5 + }; + + var id = Guid.NewGuid(); + var request = new Domain.Models.RateLimiterRequest + { + Country = Domain.Enumerations.Contries.US, + Id = id, + RequestDate = DateTime.UtcNow + }; + + var updateDate = DateTime.UtcNow.AddSeconds(-6); + var stats = new Domain.Models.RateLimiterStats + { + Id = id, + LastRequestDateTime = updateDate, + NumberOfRequestsInTimespan = 0 + }; + + var result = new Domain.Models.RulesResult + { + updatedRateLimiterStats = stats + }; + + var rule = new Domain.Rules.RequestsTimespanPerLastRequestRule(); + var results = await rule.ExecuteRule(request, configurations, stats); + + Assert.That(results.Message == ""); + Assert.That(results.Status == true); + Assert.That(results.updatedRateLimiterStats.LastRequestDateTime == request.RequestDate); + Assert.That(results.updatedRateLimiterStats.NumberOfRequestsInTimespan == 0); + } + + [Test] + public async Task RequestsPerTimespanRulePassTest() + { + var configurations = new Domain.Models.Configurations + { + RequestsPerTimespan = 5, + RequestTimespan = 5, + TimespanSinceLastCall = 5 + }; + + var id = Guid.NewGuid(); + var request = new Domain.Models.RateLimiterRequest + { + Country = Domain.Enumerations.Contries.US, + Id = id, + RequestDate = DateTime.UtcNow + }; + + var updateDate = DateTime.UtcNow.AddSeconds(-3); + var stats = new Domain.Models.RateLimiterStats + { + Id = id, + LastRequestDateTime = updateDate, + NumberOfRequestsInTimespan = 3 + }; + + var result = new Domain.Models.RulesResult + { + updatedRateLimiterStats = stats + }; + + var rule = new Domain.Rules.RequestsPerTimespanRule(); + var results = await rule.ExecuteRule(request, configurations, stats); + + Assert.That(results.Message == ""); + Assert.That(results.Status == true); + Assert.That(results.updatedRateLimiterStats.LastRequestDateTime == updateDate); + Assert.That(results.updatedRateLimiterStats.NumberOfRequestsInTimespan == 4); + } + + [Test] + public async Task RequestsPerTimespanRuleResetTest() + { + var configurations = new Domain.Models.Configurations + { + RequestsPerTimespan = 5, + RequestTimespan = 5, + TimespanSinceLastCall = 5 + }; + + var id = Guid.NewGuid(); + var request = new Domain.Models.RateLimiterRequest + { + Country = Domain.Enumerations.Contries.US, + Id = id, + RequestDate = DateTime.UtcNow + }; + + var updateDate = DateTime.UtcNow.AddSeconds(-6); + var stats = new Domain.Models.RateLimiterStats + { + Id = id, + LastRequestDateTime = updateDate, + NumberOfRequestsInTimespan = 3 + }; + + var result = new Domain.Models.RulesResult + { + updatedRateLimiterStats = stats + }; + + var rule = new Domain.Rules.RequestsPerTimespanRule(); + var results = await rule.ExecuteRule(request, configurations, stats); + + Assert.That(results.Message == ""); + Assert.That(results.Status == true); + Assert.That(results.updatedRateLimiterStats.LastRequestDateTime == request.RequestDate); + Assert.That(results.updatedRateLimiterStats.NumberOfRequestsInTimespan == 1); + } + + [Test] + public async Task RequestsPerTimespanRuleFailTest() + { + var configurations = new Domain.Models.Configurations + { + RequestsPerTimespan = 5, + RequestTimespan = 5, + TimespanSinceLastCall = 5 + }; + + var id = Guid.NewGuid(); + var request = new Domain.Models.RateLimiterRequest + { + Country = Domain.Enumerations.Contries.US, + Id = id, + RequestDate = DateTime.UtcNow + }; + + var updateDate = DateTime.UtcNow.AddSeconds(-3); + var stats = new Domain.Models.RateLimiterStats + { + Id = id, + LastRequestDateTime = updateDate, + NumberOfRequestsInTimespan = 6 + }; + + var result = new Domain.Models.RulesResult + { + updatedRateLimiterStats = stats + }; + + var rule = new Domain.Rules.RequestsPerTimespanRule(); + var results = await rule.ExecuteRule(request, configurations, stats); + + Assert.That(results.Message == "To many requests for timespan."); + Assert.That(results.Status == false); + Assert.That(results.updatedRateLimiterStats.LastRequestDateTime == updateDate); + Assert.That(results.updatedRateLimiterStats.NumberOfRequestsInTimespan == 6); + } } \ No newline at end of file diff --git a/RateLimiter.sln b/RateLimiter.sln index 626a7bfa..4d1f9a53 100644 --- a/RateLimiter.sln +++ b/RateLimiter.sln @@ -1,17 +1,19 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26730.15 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.35122.118 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiter", "RateLimiter\RateLimiter.csproj", "{36F4BDC6-D3DA-403A-8DB7-0C79F94B938F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RateLimiter", "RateLimiter\RateLimiter.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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RateLimiter.Tests", "RateLimiter.Tests\RateLimiter.Tests.csproj", "{C4F9249B-010E-46BE-94B8-DD20D82F1E60}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9B206889-9841-4B5E-B79B-D5B2610CCCFF}" ProjectSection(SolutionItems) = preProject README.md = README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiterAPI", "RateLimiterAPI\RateLimiterAPI.csproj", "{89B67487-006D-4805-B26A-52ABD0C081A0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -26,6 +28,10 @@ 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 + {89B67487-006D-4805-B26A-52ABD0C081A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89B67487-006D-4805-B26A-52ABD0C081A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89B67487-006D-4805-B26A-52ABD0C081A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89B67487-006D-4805-B26A-52ABD0C081A0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/RateLimiter/Domain/Enumerations/Contries.cs b/RateLimiter/Domain/Enumerations/Contries.cs new file mode 100644 index 00000000..f8bac80c --- /dev/null +++ b/RateLimiter/Domain/Enumerations/Contries.cs @@ -0,0 +1,8 @@ +namespace RateLimiter.Domain.Enumerations +{ + public enum Contries + { + US, + EU + } +} diff --git a/RateLimiter/Domain/Interfaces/IRequestReqository.cs b/RateLimiter/Domain/Interfaces/IRequestReqository.cs new file mode 100644 index 00000000..23ceda4c --- /dev/null +++ b/RateLimiter/Domain/Interfaces/IRequestReqository.cs @@ -0,0 +1,11 @@ +using RateLimiter.Domain.Models; +using System; + +namespace RateLimiter.Domain.Interfaces +{ + public interface IRequestReqository + { + RateLimiterStats GetRateLimiter(Guid id); + void SaveRateLimiter(RateLimiterStats rateLimiterStats); + } +} diff --git a/RateLimiter/Domain/Interfaces/IRule.cs b/RateLimiter/Domain/Interfaces/IRule.cs new file mode 100644 index 00000000..7adb7d60 --- /dev/null +++ b/RateLimiter/Domain/Interfaces/IRule.cs @@ -0,0 +1,10 @@ +using RateLimiter.Domain.Models; +using System.Threading.Tasks; + +namespace RateLimiter.Domain.Interfaces +{ + public interface IRule + { + Task ExecuteRule(RateLimiterRequest request, Configurations configurations, RateLimiterStats rateLimiterStats); + } +} diff --git a/RateLimiter/Domain/Interfaces/IRuleRunner.cs b/RateLimiter/Domain/Interfaces/IRuleRunner.cs new file mode 100644 index 00000000..559eae40 --- /dev/null +++ b/RateLimiter/Domain/Interfaces/IRuleRunner.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace RateLimiter.Domain.Interfaces +{ + public interface IRuleRunner + { + Task RunRules(Models.RateLimiterRequest request, Models.Configurations configurations); + } +} diff --git a/RateLimiter/Domain/Models/Configurations.cs b/RateLimiter/Domain/Models/Configurations.cs new file mode 100644 index 00000000..43740633 --- /dev/null +++ b/RateLimiter/Domain/Models/Configurations.cs @@ -0,0 +1,9 @@ +namespace RateLimiter.Domain.Models +{ + public class Configurations + { + public int RequestsPerTimespan { get; set; } + public int RequestTimespan { get; set; } + public int TimespanSinceLastCall { get; set; } + } +} diff --git a/RateLimiter/Domain/Models/RateLimiterRequest.cs b/RateLimiter/Domain/Models/RateLimiterRequest.cs new file mode 100644 index 00000000..b4d4ffe2 --- /dev/null +++ b/RateLimiter/Domain/Models/RateLimiterRequest.cs @@ -0,0 +1,11 @@ +using System; + +namespace RateLimiter.Domain.Models +{ + public class RateLimiterRequest + { + public Guid Id { get; set; } + public Enumerations.Contries Country {get; set;} + public DateTime RequestDate { get; set;} + } +} diff --git a/RateLimiter/Domain/Models/RateLimiterStats.cs b/RateLimiter/Domain/Models/RateLimiterStats.cs new file mode 100644 index 00000000..6c5f3361 --- /dev/null +++ b/RateLimiter/Domain/Models/RateLimiterStats.cs @@ -0,0 +1,12 @@ + +using System; + +namespace RateLimiter.Domain.Models +{ + public class RateLimiterStats + { + public Guid Id { get; set; } + public int NumberOfRequestsInTimespan { get; set; } + public DateTime LastRequestDateTime { get; set; } + } +} diff --git a/RateLimiter/Domain/Models/RulesResult.cs b/RateLimiter/Domain/Models/RulesResult.cs new file mode 100644 index 00000000..71eeaa47 --- /dev/null +++ b/RateLimiter/Domain/Models/RulesResult.cs @@ -0,0 +1,10 @@ +namespace RateLimiter.Domain.Models +{ + public class RulesResult + { + public string Message { get; set; } + public bool Status { get; set; } + public int RetryAfter { get; set; } + public RateLimiterStats updatedRateLimiterStats { get; set; } + } +} diff --git a/RateLimiter/Domain/RuleRunner.cs b/RateLimiter/Domain/RuleRunner.cs new file mode 100644 index 00000000..73309685 --- /dev/null +++ b/RateLimiter/Domain/RuleRunner.cs @@ -0,0 +1,51 @@ +using RateLimiter.Domain.Models; +using RateLimiter.Domain.Rules; +using System.Threading.Tasks; + +namespace RateLimiter.Domain +{ + public class RuleRunner : Interfaces.IRuleRunner + { + private readonly Interfaces.IRequestReqository _requestRepository; + public RuleRunner(Interfaces.IRequestReqository requestReqository) + { + _requestRepository = requestReqository; + } + public async Task RunRules(Models.RateLimiterRequest request, Models.Configurations configurations) + { + RequestsPerTimespanRule requestsPerTimespanRule = new RequestsPerTimespanRule(); + RequestsTimespanPerLastRequestRule requestsTimespanPerLastRequestRule = new RequestsTimespanPerLastRequestRule(); + + RateLimiterStats rateLimiterStats = _requestRepository.GetRateLimiter(request.Id); + if (rateLimiterStats == null) { + rateLimiterStats = new RateLimiterStats + { + LastRequestDateTime = new System.DateTime() + }; + } + + if (request.Country == Enumerations.Contries.US) + { + RulesResult requestsPerTimespanRuleResults = await requestsPerTimespanRule.ExecuteRule(request, configurations, rateLimiterStats); + _requestRepository.SaveRateLimiter(requestsPerTimespanRuleResults.updatedRateLimiterStats); + return requestsPerTimespanRuleResults; + } + else if(request.Country == Enumerations.Contries.EU) + { + RulesResult requestsTimespanPerLastRequestRuleResult = await requestsTimespanPerLastRequestRule.ExecuteRule(request, configurations, rateLimiterStats); + _requestRepository.SaveRateLimiter(requestsTimespanPerLastRequestRuleResult.updatedRateLimiterStats); + return requestsTimespanPerLastRequestRuleResult; + } + else + { + Models.RulesResult result1 = await requestsPerTimespanRule.ExecuteRule(request, configurations, rateLimiterStats); + Models.RulesResult result2 = await requestsTimespanPerLastRequestRule.ExecuteRule(request, configurations, result1.updatedRateLimiterStats); + _requestRepository.SaveRateLimiter(result2.updatedRateLimiterStats); + + return new Models.RulesResult { Message = $"{result1.Message}, {result2.Message}", Status = (result1.Status && result2.Status ) }; + } + + + } + } +} diff --git a/RateLimiter/Domain/Rules/RequestsPerTimespanRule.cs b/RateLimiter/Domain/Rules/RequestsPerTimespanRule.cs new file mode 100644 index 00000000..ffb844dc --- /dev/null +++ b/RateLimiter/Domain/Rules/RequestsPerTimespanRule.cs @@ -0,0 +1,52 @@ +using RateLimiter.Domain.Interfaces; +using RateLimiter.Domain.Models; +using System; +using System.Threading.Tasks; + +namespace RateLimiter.Domain.Rules +{ + public class RequestsPerTimespanRule : IRule + { + public async Task ExecuteRule (RateLimiterRequest request, Configurations configurations, RateLimiterStats rateLimiterStats) + { + RulesResult result = new RulesResult(); + + if (rateLimiterStats.NumberOfRequestsInTimespan < configurations.RequestsPerTimespan && (request.RequestDate - rateLimiterStats.LastRequestDateTime).TotalSeconds < configurations.TimespanSinceLastCall) + { + result.Status = true; + result.Message = ""; + result.updatedRateLimiterStats = new RateLimiterStats + { + Id = request.Id, + NumberOfRequestsInTimespan = rateLimiterStats.NumberOfRequestsInTimespan + 1, + LastRequestDateTime = rateLimiterStats.LastRequestDateTime + }; + } + else if (rateLimiterStats.NumberOfRequestsInTimespan <= configurations.RequestsPerTimespan && (request.RequestDate - rateLimiterStats.LastRequestDateTime).TotalSeconds > configurations.TimespanSinceLastCall ) + { + result.Status = true; + result.Message = ""; + result.updatedRateLimiterStats = new RateLimiterStats + { + Id = request.Id, + LastRequestDateTime = request.RequestDate, + NumberOfRequestsInTimespan = 1 + }; + } + else + { + result.Status = false; + result.Message = "To many requests for timespan."; + result.RetryAfter = configurations.TimespanSinceLastCall + (int)(request.RequestDate - rateLimiterStats.LastRequestDateTime).TotalSeconds; + result.updatedRateLimiterStats = new RateLimiterStats + { + Id = request.Id, + LastRequestDateTime = rateLimiterStats.LastRequestDateTime, + NumberOfRequestsInTimespan = rateLimiterStats.NumberOfRequestsInTimespan + }; + } + + return result; + } + } +} diff --git a/RateLimiter/Domain/Rules/RequestsTimespanPerLastRequestRule.cs b/RateLimiter/Domain/Rules/RequestsTimespanPerLastRequestRule.cs new file mode 100644 index 00000000..8c96c7f5 --- /dev/null +++ b/RateLimiter/Domain/Rules/RequestsTimespanPerLastRequestRule.cs @@ -0,0 +1,38 @@ +using RateLimiter.Domain.Interfaces; +using RateLimiter.Domain.Models; +using System.Threading.Tasks; + +namespace RateLimiter.Domain.Rules +{ + public class RequestsTimespanPerLastRequestRule : IRule + { + public async Task ExecuteRule(RateLimiterRequest request, Configurations configurations, RateLimiterStats rateLimiterStats) + { + RulesResult result = new RulesResult(); + + if ((request.RequestDate - rateLimiterStats.LastRequestDateTime).TotalSeconds > configurations.TimespanSinceLastCall) + { + result.Status = true; + result.Message = ""; + result.updatedRateLimiterStats = new RateLimiterStats + { + Id = request.Id, + LastRequestDateTime = request.RequestDate + }; + } + else + { + result.Status = false; + result.Message = "Timespan has not expired."; + result.RetryAfter = configurations.TimespanSinceLastCall + (int)(request.RequestDate - rateLimiterStats.LastRequestDateTime).TotalSeconds; + result.updatedRateLimiterStats = new RateLimiterStats + { + Id = request.Id, + LastRequestDateTime = rateLimiterStats.LastRequestDateTime + }; + } + + return result; + } + } +} diff --git a/RateLimiter/Infrastructure/RequestRepository.cs b/RateLimiter/Infrastructure/RequestRepository.cs new file mode 100644 index 00000000..053194b5 --- /dev/null +++ b/RateLimiter/Infrastructure/RequestRepository.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.Logging; +using RateLimiter.Domain.Models; +using System; +using System.Collections.Generic; + +namespace RateLimiter.Infrastructure +{ + public class RequestRepository : Domain.Interfaces.IRequestReqository + { + + private Dictionary RateLimiterCache = new Dictionary(); + private readonly ILogger _logger; + public RequestRepository(ILogger logger) + { + _logger = logger; + } + + public RateLimiterStats GetRateLimiter(Guid id) + { + RateLimiterStats rateLimiterStats; + try + { + var exists = RateLimiterCache.TryGetValue(id, out rateLimiterStats); + + if (exists) { return rateLimiterStats; } + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + } + + return null; + } + + public void SaveRateLimiter(RateLimiterStats rateLimiterStats) + { + try + { + var exists = RateLimiterCache.ContainsKey(rateLimiterStats.Id); + if (exists) { RateLimiterCache.Remove(rateLimiterStats.Id); } + RateLimiterCache.TryAdd(rateLimiterStats.Id, rateLimiterStats); + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + } + } + } +} diff --git a/RateLimiter/RateLimiter.csproj b/RateLimiter/RateLimiter.csproj index 19962f52..5fce3395 100644 --- a/RateLimiter/RateLimiter.csproj +++ b/RateLimiter/RateLimiter.csproj @@ -4,4 +4,7 @@ latest enable + + + \ No newline at end of file diff --git a/RateLimiterAPI/Controllers/SimpleRequestController.cs b/RateLimiterAPI/Controllers/SimpleRequestController.cs new file mode 100644 index 00000000..53dad05b --- /dev/null +++ b/RateLimiterAPI/Controllers/SimpleRequestController.cs @@ -0,0 +1,73 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using RateLimiter.Domain; +using RateLimiter.Domain.Interfaces; + +namespace RateLimiterAPI.Controllers +{ + [ApiController] + [Route("[controller]")] + public class SimpleRequestControllerController : ControllerBase + { + private readonly ILogger _logger; + private readonly IRuleRunner _ruleRunner; + public readonly IConfiguration _configuration; + + public SimpleRequestControllerController(ILogger logger, IConfiguration configuration, IRuleRunner ruleRunner) + { + _logger = logger; + _ruleRunner = ruleRunner; + _configuration = configuration; + } + + [HttpGet()] + [Route("/Token/{token}")] + public async Task Get(string token) + { + try + { + var results = await _ruleRunner.RunRules(ParseToken(token), PopulateConfigurations()); + + if (results.Status == false) + { + return new TooManyRequestsObjectResult(results.Message, results.RetryAfter); + } + return new ObjectResult(results.Message); + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + return new BadRequestObjectResult(ex.Message); + } + } + + private RateLimiter.Domain.Models.RateLimiterRequest ParseToken(string token) + { + RateLimiter.Domain.Models.RateLimiterRequest request = new RateLimiter.Domain.Models.RateLimiterRequest(); + + var bytes = Convert.FromBase64String(token); + var decodedString = System.Text.Encoding.ASCII.GetString(bytes); + + var properties = decodedString.Split(',',StringSplitOptions.None); + string country = properties[1]; + var countryCode = (RateLimiter.Domain.Enumerations.Contries)Enum.Parse(typeof(RateLimiter.Domain.Enumerations.Contries), country); + + return new RateLimiter.Domain.Models.RateLimiterRequest + { + Id = Guid.Parse(properties[0]), + Country = countryCode, + RequestDate = DateTime.UtcNow + }; + } + + private RateLimiter.Domain.Models.Configurations PopulateConfigurations() + { + return new RateLimiter.Domain.Models.Configurations + { + RequestsPerTimespan = _configuration.GetValue("RequestsPerTimespan"), + RequestTimespan = _configuration.GetValue("RequestTimespan"), + TimespanSinceLastCall = _configuration.GetValue("TimespanSinceLastCall") + }; + } + } +} diff --git a/RateLimiterAPI/Program.cs b/RateLimiterAPI/Program.cs new file mode 100644 index 00000000..9c4fd230 --- /dev/null +++ b/RateLimiterAPI/Program.cs @@ -0,0 +1,39 @@ +using System.Net; + +namespace RateLimiterAPI +{ + public class Program + { + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + builder.Configuration.AddEnvironmentVariables(); + // Add services to the container. + builder.Services.AddSingleton< RateLimiter.Domain.Interfaces.IRequestReqository, RateLimiter.Infrastructure.RequestRepository>(); + builder.Services.AddScoped< RateLimiter.Domain.Interfaces.IRuleRunner, RateLimiter.Domain.RuleRunner>(); + + builder.Services.AddControllers(); + // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseHttpsRedirection(); + + app.UseAuthorization(); + + + app.MapControllers(); + + app.Run(); + } + } +} diff --git a/RateLimiterAPI/Properties/launchSettings.json b/RateLimiterAPI/Properties/launchSettings.json new file mode 100644 index 00000000..4582aca8 --- /dev/null +++ b/RateLimiterAPI/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:34875", + "sslPort": 44315 + } + }, + "profiles": { + "RateLimiterAPI": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7007;http://localhost:5120", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/RateLimiterAPI/RateLimiterAPI.csproj b/RateLimiterAPI/RateLimiterAPI.csproj new file mode 100644 index 00000000..c8bfe1a8 --- /dev/null +++ b/RateLimiterAPI/RateLimiterAPI.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + diff --git a/RateLimiterAPI/TooManyRequestsObjectResult.cs b/RateLimiterAPI/TooManyRequestsObjectResult.cs new file mode 100644 index 00000000..0e3d3538 --- /dev/null +++ b/RateLimiterAPI/TooManyRequestsObjectResult.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc; + +namespace RateLimiterAPI +{ + [DefaultStatusCode(DefaultStatusCode)] + public class TooManyRequestsObjectResult : ObjectResult + { + private const int DefaultStatusCode = StatusCodes.Status429TooManyRequests; + public TimeSpan? RetryAfter { get; } + + public TooManyRequestsObjectResult([ActionResultObjectValue] object value) + : base(value) + { + StatusCode = DefaultStatusCode; + } + + public TooManyRequestsObjectResult([ActionResultObjectValue] object value, int retryAfter) + : this(value, TimeSpan.FromSeconds(retryAfter)) + { + } + + public TooManyRequestsObjectResult([ActionResultObjectValue] object value, TimeSpan retryAfter) + : this(value) + { + if (retryAfter < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(retryAfter), $"{nameof(retryAfter)} must be a non negative value"); + } + + RetryAfter = retryAfter; + } + + public override Task ExecuteResultAsync(ActionContext context) + { + ArgumentNullException.ThrowIfNull(context); + + if (RetryAfter.HasValue) + { + context.HttpContext.Response.Headers.RetryAfter = Math.Round(RetryAfter.Value.TotalSeconds).ToString(); + } + + return base.ExecuteResultAsync(context); + } + } +} diff --git a/RateLimiterAPI/appsettings.Development.json b/RateLimiterAPI/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/RateLimiterAPI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/RateLimiterAPI/appsettings.json b/RateLimiterAPI/appsettings.json new file mode 100644 index 00000000..59aeb831 --- /dev/null +++ b/RateLimiterAPI/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "RequestsPerTimespan": 5, + "RequestTimespan": 15, + "TimespanSinceLastCall": 10 +}