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

Mateus Carvalho #240

Closed
wants to merge 1 commit into from
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
59 changes: 59 additions & 0 deletions DevNotes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# DEV Notes

## Project Structure

- **RateLimiter Solution**
- **RateLimiter.Api**
- **RateLimiter**
- Class library containing attributes and rules, as well as some additional contract interfaces like `IGeoService` and `IRateLimitStorage`.
- **RateLimiter.Tests**
- **Integration**
- API Tests
- **Unit**
- Middleware Tests
- Rules Tests
- Storage Tests

In a real-world scenario, I would have broken the solution into multiple projects as shown below, so that each project builds on top of the others. Furthermore, we could package the solution into multiple NuGet packages that could be easily imported into your solution according to the type of project you are creating, storage you want to use, etc..

- **RateLimiter Solution**
- **RateLimiter.Api**
- Just the API project with controllers
- **RateLimiter.AspNetCore**
- Middleware implementation, and possibly any extensions to help with the integration with ASP.NET Core
- **RateLimiter.GeoRules**
- Geo Rule implementation
- **RateLimiter.Storage**
- Storage implementation
- **RateLimiter.Core**
- Attributes, basic rules and contract interfaces
- **RateLimiter.Integration.Tests**
- API Tests
- **RateLimiter.AspNetCore.Tests**
- Middleware Tests
- **RateLimiter.GeoRules.Tests**
- Geo Rule Tests
- **RateLimiter.Storage.Tests**
- In-Memory Storage Tests
- **RateLimiter.Core.Tests**
- Core Tests

## Components

### Simple Geo Service

This service is a simple implementation that serves as a helper to demonstrate the concept. It only considers the two examples given in the exercise (US, EU).

### Middleware

To facilitate the application of rate limiting and abstract it away from the controllers, a middleware was created. This middleware is responsible for checking the rate limits and returning the appropriate responses. While it performs some authentication verification, it's only an implementation detail since we are using a simple token for rate limiting. A more complex authentication mechanism could be implemented in the future, and features like per-customer rate limiting could be added.

## Testing

The tests are simple and cover the basic functionality of the rate limiting. They are not exhaustive but are a good starting point. Additional tests that would cover edge cases could be added to make the solution more robust, but in order to maintain the time box that I set for myself, those were left out. The `SimpleGeoService` was left untested since it is a simple implementation and the tests would be redundant.

## Suggestions

### .NET Version Upgrade

As of November 12th, 2024, .NET 6 will be officially out of support. I would recommend updating the projects to the latest version of .NET.
16 changes: 16 additions & 0 deletions RateLimiter.Api/Controllers/ResourceAController .cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Mvc;
using RateLimiter.Attributes;

namespace RateLimiter.Api.Controllers;

[ApiController]
[Route("api/[controller]")]
[FixedWindowRateLimit(5, 60)] // 5 requests per 60 seconds
public class ResourceAController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return Ok("Access to Resource A");
}
}
16 changes: 16 additions & 0 deletions RateLimiter.Api/Controllers/ResourceBController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Mvc;
using RateLimiter.Attributes;

namespace RateLimiter.Api.Controllers;

[ApiController]
[Route("api/[controller]")]
public class ResourceBController : ControllerBase
{
[HttpGet]
[FixedDelayRateLimit(10)] // 10 seconds delay between requests
public IActionResult Get()
{
return Ok("Access to Resource B");
}
}
17 changes: 17 additions & 0 deletions RateLimiter.Api/Controllers/ResourceDController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Mvc;
using RateLimiter.Attributes;

namespace RateLimiter.Api.Controllers;

[ApiController]
[Route("api/[controller]")]
[GeoBasedRateLimit("US",10, 60)] // US: 10 req/60 sec
[GeoBasedRateLimit("EU", 0, 15)] // EU: 15 sec delay
public class ResourceDController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return Ok("Access to Resource D");
}
}
86 changes: 86 additions & 0 deletions RateLimiter.Api/Middlewares/RateLimitingMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using RateLimiter.Attributes;
using RateLimiter.Rules;
using RateLimiter.Storages;
using System.Collections.Concurrent;

namespace RateLimiter.Api.Middlewares;
public class RateLimitingMiddleware
{
private readonly RequestDelegate _next;
private readonly IServiceProvider _serviceProvider;
private readonly IRateLimitStore _store;
private readonly ConcurrentDictionary<string, IRateLimitRule> _rulesCache = new();

public RateLimitingMiddleware(RequestDelegate next, IServiceProvider serviceProvider, IRateLimitStore store)
{
_next = next;
_serviceProvider = serviceProvider;
_store = store;
}

public async Task InvokeAsync(HttpContext context)
{
var endpoint = context.GetEndpoint();
if (endpoint == null)
{
await _next(context);
return;
}

var clientId = context.Request.Headers["Authorization"].FirstOrDefault();
if (string.IsNullOrEmpty(clientId))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync("Client ID is missing.");
return;
}

var actionKey = endpoint.DisplayName;

// Get rate limit rules from attributes
var rateLimitRules = GetRateLimitRules(endpoint);

if (rateLimitRules.Any())
{
var compositeRule = new CompositeRule(rateLimitRules);

if (!await compositeRule.IsRequestAllowedAsync(clientId, actionKey, _store))
{
context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
context.Response.Headers["Retry-After"] = "60"; // Example value
await context.Response.WriteAsync("Rate limit exceeded.");
return;
}
}

await _next(context);
}

private IEnumerable<IRateLimitRule> GetRateLimitRules(Endpoint endpoint)
{
var rateLimitAttributes = endpoint.Metadata.GetOrderedMetadata<RateLimitAttribute>();
var rules = new List<IRateLimitRule>();

foreach (var attribute in rateLimitAttributes)
{
var key = GetAttributeKey(attribute);

// Use a cache to prevent creating multiple instances of the same rule
var rule = _rulesCache.GetOrAdd(key, _ =>
{
return attribute.CreateRule(_serviceProvider);
});
rules.Add(rule);
}

return rules;
}

private string GetAttributeKey(RateLimitAttribute attribute)
{
// Create a unique key based on the attribute's type and properties
var properties = attribute.GetType().GetProperties()
.Select(p => p.GetValue(attribute)?.ToString() ?? "");
return attribute.GetType().FullName + ":" + string.Join(":", properties);
}
}
38 changes: 38 additions & 0 deletions RateLimiter.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using RateLimiter.Api.Middlewares;
using RateLimiter.Api.Services;
using RateLimiter.Geo;
using RateLimiter.Storages;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();

// Register IGeoService and IRateLimitStore
builder.Services.AddSingleton<IGeoService, SimpleGeoService>();
builder.Services.AddSingleton<IRateLimitStore, InMemoryStore>();

// 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();
}

// Use the rate-limiting middleware
app.UseMiddleware<RateLimitingMiddleware>();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
31 changes: 31 additions & 0 deletions RateLimiter.Api/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:23373",
"sslPort": 44324
}
},
"profiles": {
"RateLimiter.Api": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7287;http://localhost:5194",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
22 changes: 22 additions & 0 deletions RateLimiter.Api/RateLimiter.Api.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>

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

<!--https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-8.0#basic-tests-with-the-default-webapplicationfactory-->
<ItemGroup>
<InternalsVisibleTo Include="RateLimiter.Tests" />
</ItemGroup>

</Project>
17 changes: 17 additions & 0 deletions RateLimiter.Api/Services/SimpleGeoService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using RateLimiter.Geo;

namespace RateLimiter.Api.Services;

public class SimpleGeoService : IGeoService
{
public async Task<string> GetLocationAsync(string clientId)
{
// Emulate proper implementation
return await Task.Run(() =>
{
// Simplified logic for demonstration
return clientId.StartsWith("US") ? "US" : clientId.StartsWith("EU") ? "EU" : "Other";

});
}
}
8 changes: 8 additions & 0 deletions RateLimiter.Api/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions RateLimiter.Api/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Loading
Loading