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

Final implementation #274

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
54 changes: 54 additions & 0 deletions RateLimiter.Tests/RateLimitRuleTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@

using NUnit.Framework;
using RateLimiter.Rules;
using System;
using System.Collections.Generic;

namespace RateLimiter.Tests
{
[TestFixture]
public class RateLimitRuleTests
{
[Test]
public void Should_AllowRequestsWithinLimit()
{
var rule = new XRequestsPerTimespanRule(3, TimeSpan.FromSeconds(10));
var clientId = "test-client";
var resource = "test-resource";


Assert.That(rule.IsRequestAllowed(clientId, resource), Is.True, "The request should be allowed.");
Assert.That(rule.IsRequestAllowed(clientId, resource), Is.True, "The request should be allowed.");
Assert.That(rule.IsRequestAllowed(clientId, resource), Is.True, "The request should be allowed.");
Assert.That(rule.IsRequestAllowed(clientId, resource), Is.False, "The request should not be allowed.");

}

[Test]
public void ClientRateLimit_AllowsWithinLimit()
{
var rule = new ClientRateLimitRule(5, TimeSpan.FromMinutes(1));
var clientId = "client1";

for (int i = 0; i < 5; i++)
{
Assert.That(rule.IsRequestAllowed(clientId, "resource1", "127.0.0.1"), Is.True, "Request should be allowed within the limit.");
}

Assert.That(rule.IsRequestAllowed(clientId, "resource1", "127.0.0.1"), Is.False, "Request should be denied after exceeding the limit.");
}

[Test]
public void ResourceRateLimit_AllowsWithinLimit()
{
var resourceLimits = new Dictionary<string, int> { { "resource1", 2 } };
var rule = new ResourceRateLimitRule(resourceLimits);

Assert.That(rule.IsRequestAllowed("client1", "resource1", "127.0.0.1"), Is.True);
Assert.That(rule.IsRequestAllowed("client1", "resource1", "127.0.0.1"), Is.True);
Assert.That(rule.IsRequestAllowed("client1", "resource1", "127.0.0.1"), Is.False);
}


}
}
6 changes: 3 additions & 3 deletions RateLimiter.Tests/RateLimiter.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
<ProjectReference Include="..\RateLimiter\RateLimiter.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="4.3.2" />
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0-beta.5" />
</ItemGroup>
</Project>
13 changes: 0 additions & 13 deletions RateLimiter.Tests/RateLimiterTest.cs

This file was deleted.

9 changes: 9 additions & 0 deletions RateLimiter/Interfaces/IRateLimitRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

namespace RateLimiter.Interfaces
{
public interface IRateLimitRule
{
bool IsRequestAllowed(string clientId, string resource);
bool IsRequestAllowed(string clientId, string resource, string ip);
}
}
12 changes: 12 additions & 0 deletions RateLimiter/Models/RequestLog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

using System;

namespace RateLimiter.Models
{
public class RequestLog
{
public string ClientId { get; set; }
public string Resource { get; set; }
public DateTime Timestamp { get; set; }
}
}
27 changes: 27 additions & 0 deletions RateLimiter/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

using RateLimiter.Rules;
using System;
using System.Threading;

namespace RateLimiter
{
public class Program
{
public static void Main(string[] args)
{
var rule = new XRequestsPerTimespanRule(5, TimeSpan.FromSeconds(10));
var clientId = "client1";
var resource = "api/resource";

Console.WriteLine("Rate Limiter Console App");
Console.WriteLine("Testing rate limiter with 7 requests...");

for (int i = 0; i < 7; i++)
{
var allowed = rule.IsRequestAllowed(clientId, resource);
Console.WriteLine($"Request {i + 1}: {(allowed ? "Allowed" : "Blocked")}");
Thread.Sleep(1000); // Simulate 1-second delay between requests
}
}
}
}
27 changes: 21 additions & 6 deletions RateLimiter/RateLimiter.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<StartupObject>RateLimiter.Program</StartupObject>
</PropertyGroup>

<ItemGroup>
<!-- Entity Framework Core In-Memory Provider -->
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="6.0.0" />

<!-- Moq for Mocking in Tests (if needed) -->
<PackageReference Include="Moq" Version="4.20.2" />

<!-- NUnit for Unit Testing -->
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
</ItemGroup>
</Project>
47 changes: 47 additions & 0 deletions RateLimiter/Rules/ClientRateLimitRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using RateLimiter.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RateLimiter.Rules
{
public class ClientRateLimitRule : IRateLimitRule
{
private readonly int _requestLimit;
private readonly TimeSpan _timeWindow;
private readonly Dictionary<string, Queue<DateTime>> _clientRequests = new();

public ClientRateLimitRule(int requestLimit, TimeSpan timeWindow)
{
_requestLimit = requestLimit;
_timeWindow = timeWindow;
}

public bool IsRequestAllowed(string clientId, string resource, string ip)
{
if (!_clientRequests.ContainsKey(clientId))
_clientRequests[clientId] = new Queue<DateTime>();

var requests = _clientRequests[clientId];
var now = DateTime.UtcNow;

while (requests.Count > 0 && requests.Peek() <= now - _timeWindow)
requests.Dequeue();

if (requests.Count < _requestLimit)
{
requests.Enqueue(now);
return true;
}

return false;
}

public bool IsRequestAllowed(string clientId, string resource)
{
return this.IsRequestAllowed(clientId, resource, string.Empty);
}
}
}
48 changes: 48 additions & 0 deletions RateLimiter/Rules/ResourceRateLimitRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using RateLimiter.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RateLimiter.Rules
{
public class ResourceRateLimitRule : IRateLimitRule
{
private readonly Dictionary<string, int> _resourceLimits;
private readonly Dictionary<string, Queue<DateTime>> _resourceRequests = new();

public ResourceRateLimitRule(Dictionary<string, int> resourceLimits)
{
_resourceLimits = resourceLimits;
}

public bool IsRequestAllowed(string clientId, string resource, string ip)
{
if (!_resourceLimits.ContainsKey(resource))
return true;

if (!_resourceRequests.ContainsKey(resource))
_resourceRequests[resource] = new Queue<DateTime>();

var requests = _resourceRequests[resource];
var now = DateTime.UtcNow;

while (requests.Count > 0 && requests.Peek() <= now - TimeSpan.FromMinutes(1))
requests.Dequeue();

if (requests.Count < _resourceLimits[resource])
{
requests.Enqueue(now);
return true;
}

return false;
}

public bool IsRequestAllowed(string clientId, string resource)
{
return this.IsRequestAllowed(clientId, resource, string.Empty);
}
}
}
51 changes: 51 additions & 0 deletions RateLimiter/Rules/XRequestsPerTimespanRule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@

using RateLimiter.Interfaces;
using RateLimiter.Models;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace RateLimiter.Rules
{
public class XRequestsPerTimespanRule : IRateLimitRule
{
private readonly int _maxRequests;
private readonly TimeSpan _timespan;
private readonly ConcurrentDictionary<string, List<DateTime>> _requestLogs;

public XRequestsPerTimespanRule(int maxRequests, TimeSpan timespan)
{
_maxRequests = maxRequests;
_timespan = timespan;
_requestLogs = new ConcurrentDictionary<string, List<DateTime>>();
}

public bool IsRequestAllowed(string clientId, string resource)
{
var key = $"{clientId}:{resource}";
var now = DateTime.UtcNow;

_requestLogs.TryAdd(key, new List<DateTime>());
var requestLog = _requestLogs[key];

lock (requestLog)
{
requestLog.RemoveAll(timestamp => (now - timestamp) > _timespan);

if (requestLog.Count < _maxRequests)
{
requestLog.Add(now);
return true;
}

return false;
}
}

public bool IsRequestAllowed(string clientId, string resource, string ip)
{

return this.IsRequestAllowed(clientId, resource, string.Empty);
}
}
}
15 changes: 15 additions & 0 deletions RateLimiter/Utilities/DefaultTimeProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

using System;

namespace RateLimiter.Utilities
{
public interface ITimeProvider
{
DateTime GetCurrentTime();
}

public class DefaultTimeProvider : ITimeProvider
{
public DateTime GetCurrentTime() => DateTime.UtcNow;
}
}
Loading