Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

initial commit of RateLimiter #196

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions RateLimiter.API/Controllers/SampleItemsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using Microsoft.AspNetCore.Mvc;
using RateLimiter.API.Models;

namespace RateLimiter.API.Controllers;

[ApiController]
[Route("[controller]")]
public class SampleItemsController : ControllerBase
{
private static readonly Dictionary<int, SampleItemModel> _items = new();
private static int _currentId = 0;

[HttpGet]
public IActionResult GetAll()
{
return Ok(_items.Values);
}

[HttpGet("{id}")]
public IActionResult GetById(int id)
{
if (_items.TryGetValue(id, out var item))
{
return Ok(item);
}

return NotFound();
}

[HttpPost]
public IActionResult Create(SampleItemModel item)
{
item.Id = ++_currentId;
_items[item.Id] = item;
return CreatedAtAction(nameof(GetById), new { id = item.Id }, item);
}

[HttpPut("{id}")]
public IActionResult Update(int id, SampleItemModel item)
{
if (_items.ContainsKey(id))
{
item.Id = id;
_items[id] = item;
return Ok(item);
}

return NotFound();
}

[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
if (_items.Remove(id, out var item))
{
return Ok(item);
}

return NotFound();
}
}
7 changes: 7 additions & 0 deletions RateLimiter.API/Models/SampleItemModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace RateLimiter.API.Models;

public class SampleItemModel
{
public int Id { get; set; }
public string Name { get; set; }
}
41 changes: 41 additions & 0 deletions RateLimiter.API/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using RateLimiter.API.Controllers;
using RateLimiter.Implementation;

namespace RateLimiter.API;

public class Program
{
public static void Main(string[] args)
{
var hostBuilder = new HostBuilder()
.ConfigureHostConfiguration(configHost =>
{
configHost.SetBasePath(Directory.GetCurrentDirectory());
configHost.AddJsonFile("appsettings.json", optional: true);
configHost.AddEnvironmentVariables(prefix: "ASPNETCORE_");
configHost.AddCommandLine(args);
})
.ConfigureServices((hostContext, services) =>
{
var rateLimitConfig = new RateLimitConfig();
hostContext.Configuration.GetSection("RateLimiting").Bind(rateLimitConfig);

foreach (var ruleConfig in rateLimitConfig.Rules)
{
var ruleType = Type.GetType(ruleConfig.RuleType);
if (ruleType == null) continue;

var rule = (IRateLimitingRule)Activator.CreateInstance(ruleType, ruleConfig.Parameters)!;
services.AddSingleton(typeof(IRateLimitingRule), rule);
}

services.AddSingleton(rateLimitConfig);
});

var host = hostBuilder.Build();
host.Run();
}
}
20 changes: 20 additions & 0 deletions RateLimiter.API/RateLimiter.API.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.0-preview.5.24306.7" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\RateLimiter\RateLimiter.csproj" />
</ItemGroup>

</Project>
78 changes: 78 additions & 0 deletions RateLimiter.API/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"RateLimiting": {
"Endpoints": [
{
"Path": "/GetById",
"AppliedRules": [
{
"RuleName": "TimespanSinceLastCall",
"Parameters": {
"MinimumInterval": "00:00:05"
}
},
{
"RuleName": "RequestsPerTimespan",
"Parameters": {
"RequestLimit": 1024,
"Timespan": "01:00:00"
}
}
]
},
{
"Path": "/Create",
"AppliedRules": [
{
"RuleName": "TimespanSinceLastCall",
"Parameters": {
"MinimumInterval": "00:00:10"
}
},
{
"RuleName": "RequestsPerTimespan",
"Parameters": {
"RequestLimit": 512,
"Timespan": "00:30:00"
}
}
]
},
{
"Path": "/Update",
"AppliedRules": [
{
"RuleName": "TimespanSinceLastCall",
"Parameters": {
"MinimumInterval": "00:02:10"
}
},
{
"RuleName": "RequestsPerTimespan",
"Parameters": {
"RequestLimit": 128,
"Timespan": "00:06:00"
}
}
]
},
{
"Path": "/Delete",
"AppliedRules": [
{
"RuleName": "TimespanSinceLastCall",
"Parameters": {
"MinimumInterval": "00:01:00"
}
},
{
"RuleName": "RequestsPerTimespan",
"Parameters": {
"RequestLimit": 256,
"Timespan": "00:15:00"
}
}
]
}
]
}
}
1 change: 1 addition & 0 deletions RateLimiter.Tests/RateLimiter.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
</ItemGroup>
Expand Down
89 changes: 83 additions & 6 deletions RateLimiter.Tests/RateLimiterTest.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,90 @@
using NUnit.Framework;
using System;
using Moq;
using NUnit.Framework;
using RateLimiter.Implementation.Rules;

namespace RateLimiter.Tests;

[TestFixture]
public class RateLimiterTest
{
[Test]
public void Example()
{
Assert.That(true, Is.True);
}
[Test]
public void IsRequestAllowed_RequestAfterMinimumInterval_ReturnsTrue()
{
// Arrange
var ruleMock = new Mock<TimespanSinceLastCallRule>(TimeSpan.FromMinutes(1));

var clientId = "testClient";
var resource = "testResource";
ruleMock
.Setup(r => r.GetLastRequestTime(clientId, resource))
.Returns(DateTime.UtcNow.AddMinutes(-25));

// Act
var result = ruleMock.Object.IsRequestAllowed(clientId, resource);

// Assert
Assert.IsTrue(result);
}

[Test]
public void IsRequestAllowed_RequestBeforeMinimumInterval_ReturnsFalse()
{
// Arrange
var rule = new Mock<TimespanSinceLastCallRule>(TimeSpan.FromMinutes(1));
var clientId = "testClient";
var resource = "testResource";

rule.Object.SaveRequest(clientId, resource, DateTime.UtcNow);

// Act
var result = rule.Object.IsRequestAllowed(clientId, resource);

// Assert
Assert.IsFalse(result);
}

[Test]
public void IsRequestAllowed_RequestsBelowMaxWithinTimespan_ReturnsTrue()
{
// Arrange
var maxRequests = 5;
var timespan = TimeSpan.FromHours(1);
var rule = new RequestsPerTimespanRule(maxRequests, timespan);
var clientId = "testClient";
var resource = "testResource";

for (int i = 0; i < maxRequests - 1; i++)
{
rule.SaveRequest(clientId, resource, DateTime.UtcNow.AddMinutes(-i * 256));
}

// Act
var result = rule.IsRequestAllowed(clientId, resource);

// Assert
Assert.IsTrue(result);
}

[Test]
public void IsRequestAllowed_RequestsExceedMaxWithinTimespan_ReturnsFalse()
{
// Arrange
var maxRequests = 5;
var timespan = TimeSpan.FromHours(1);
var rule = new RequestsPerTimespanRule(maxRequests, timespan);
var clientId = "testClient";
var resource = "testResource";

for (int i = 0; i < maxRequests; i++)
{
rule.SaveRequest(clientId, resource, DateTime.UtcNow.AddMinutes(-i * 10));
}

// Act
var result = rule.IsRequestAllowed(clientId, resource);

// Assert
Assert.IsFalse(result);
}
}
6 changes: 6 additions & 0 deletions RateLimiter.sln
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
README.md = README.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RateLimiter.API", "RateLimiter.API\RateLimiter.API.csproj", "{1182DAAD-13CF-4412-8044-761B33A95FB3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -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
{1182DAAD-13CF-4412-8044-761B33A95FB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1182DAAD-13CF-4412-8044-761B33A95FB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1182DAAD-13CF-4412-8044-761B33A95FB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1182DAAD-13CF-4412-8044-761B33A95FB3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
43 changes: 43 additions & 0 deletions RateLimiter/BaseRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace RateLimiter;

public abstract class BaseRule : IRateLimitingRule
{
private static readonly ConcurrentDictionary<string, List<DateTime>> Requests = new();

public abstract bool IsRequestAllowed(string clientId, string resource);

public virtual DateTime? GetLastRequestTime(string clientId, string resource)
{
var key = $"{clientId}:{resource}";
if (Requests.TryGetValue(key, out var timestamps) && timestamps.Any())
{
return timestamps.Last();
}
return null;
}

protected int GetRequestCount(string clientId, string resource, DateTime start, DateTime end)
{
var key = $"{clientId}:{resource}";
if (Requests.TryGetValue(key, out var timestamps))
{
return timestamps.Count(timestamp => timestamp >= start && timestamp <= end);
}
return 0;
}

public void SaveRequest(string clientId, string resource, DateTime timestamp)
{
var key = $"{clientId}:{resource}";
Requests.AddOrUpdate(key, new List<DateTime> { timestamp }, (_, timestampList) =>
{
timestampList.Add(timestamp);
return timestampList;
});
}
}
6 changes: 6 additions & 0 deletions RateLimiter/IRateLimitingRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace RateLimiter;

public interface IRateLimitingRule
{
bool IsRequestAllowed(string clientId, string resource);
}
Loading