Skip to content
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
56 changes: 55 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,58 @@
**Rate-limiting pattern**
## Usage

### Register and configure Rate limiting services(rules)

_Example:_

```csharp
builder.Services.
AddFixedWindow(configure =>
{
configure.RuleConditions = new List<Func<AccessToken, bool>>
{
token => token.Region == Region.us,
token => !string.IsNullOrEmpty(token.UserId)
};

configure.Limit = 10;
configure.WindowSize = TimeSpan.FromMinutes(2);
}).
AddTimeBasedRateLimiting(configure =>
{
configure.RuleConditions = new List<Func<AccessToken, bool>>
{
token => token.Region == Region.us || token.Region == Region.eu,
token => !string.IsNullOrEmpty(token.UserId)
};

configure.MinTimeBetweenRequests = TimeSpan.FromSeconds(10);
});
```


Rules are applied based on conditions. If the condition is not specified then it applies to all requests

```csharp
configure.RuleConditions = new List<Func<AccessToken, bool>>
{
token => token.Region == Region.us,
token => !string.IsNullOrEmpty(token.UserId)
};
```

### Set up the RateLimitMiddleware

Add the RateLimit middleware to your `Configure` method in the `Startup` class or directly into your `IWebHostBuilder`.

_Example:_

```csharp
app.UseRateLimiting();
```



**Rate-limiting pattern**

Rate limiting involves restricting the number of requests that a client can make.
A client is identified with an access token, which is used for every request to a resource.
Expand Down
11 changes: 11 additions & 0 deletions RateLimiter.SampleApi/Controllers/ValuesController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace RateLimiter.SampleApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
}
}
32 changes: 32 additions & 0 deletions RateLimiter.SampleApi/Controllers/WeatherForecastController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Mvc;

namespace RateLimiter.SampleApi.Controllers;

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

private readonly ILogger<WeatherForecastController> _logger;

public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}

[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
55 changes: 55 additions & 0 deletions RateLimiter.SampleApi/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using RateLimiter;
using RateLimiter.Extensions;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddMemoryCache();

builder.Services.
AddFixedWindow(configure =>
{
configure.RuleConditions = new List<Func<AccessToken, bool>>
{
token => token.Region == Region.us,
token => !string.IsNullOrEmpty(token.UserId)
};

configure.Limit = 10;
configure.WindowSize = TimeSpan.FromMinutes(2);
}).
AddTimeBasedRateLimiting(configure =>
{
configure.RuleConditions = new List<Func<AccessToken, bool>>
{
token => token.Region == Region.us || token.Region == Region.eu,
token => !string.IsNullOrEmpty(token.UserId)
};

configure.MinTimeBetweenRequests = TimeSpan.FromSeconds(10);
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

app.UseRateLimiting();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

31 changes: 31 additions & 0 deletions RateLimiter.SampleApi/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:61262",
"sslPort": 44376
}
},
"profiles": {
"RateLimiter.SampleApi": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7103;http://localhost:5109",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
17 changes: 17 additions & 0 deletions RateLimiter.SampleApi/RateLimiter.SampleApi.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<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>

</Project>
8 changes: 8 additions & 0 deletions RateLimiter.SampleApi/RateLimiter.SampleApi.csproj.user
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Controller_SelectedScaffolderID>ApiControllerEmptyScaffolder</Controller_SelectedScaffolderID>
<Controller_SelectedScaffolderCategoryPath>root/Common/Api</Controller_SelectedScaffolderCategoryPath>
<WebStackScaffolding_ControllerDialogWidth>650</WebStackScaffolding_ControllerDialogWidth>
</PropertyGroup>
</Project>
13 changes: 13 additions & 0 deletions RateLimiter.SampleApi/WeatherForecast.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace RateLimiter.SampleApi
{
public class WeatherForecast
{
public DateTime Date { get; set; }

public int TemperatureC { get; set; }

public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);

public string? Summary { get; set; }
}
}
8 changes: 8 additions & 0 deletions RateLimiter.SampleApi/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.SampleApi/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
71 changes: 71 additions & 0 deletions RateLimiter.Tests/MiddlewareTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using NUnit.Framework;
using RateLimiter.Rules;
using RateLimiter.Rules.FixedWindowRule;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace RateLimiter.Tests
{
[TestFixture]
public class MiddlewareTest
{
private IMemoryCache _cache;

[SetUp]
public void Setup()
{
_cache = new MemoryCache(new MemoryCacheOptions());
}

[Test]
public async Task Middleware_AllowsRequest_WhenWithinLimit()
{
var options = new FixedWindowRuleOptions
{
RuleConditions = null, //rules apply to all requests
WindowSize = TimeSpan.FromMinutes(1),
Limit = 5
};

var rules = new List<IRateLimitRule> { new FixedWindowRule(options, _cache) };
var middleware = new RateLimitMiddleware(async (context) => { context.Response.StatusCode = 200; }, rules);

var context = new DefaultHttpContext();
context.Connection.RemoteIpAddress = System.Net.IPAddress.Parse("127.0.0.1");
context.Request.Path = "/test";

await middleware.InvokeAsync(context);
Assert.AreEqual(200, context.Response.StatusCode);
}

[Test]
public async Task Middleware_BlocksRequest_WhenLimitExceeded()
{
var options = new FixedWindowRuleOptions
{
WindowSize = TimeSpan.FromMinutes(2),
Limit = 1
};

var rules = new List<IRateLimitRule> { new FixedWindowRule(options, _cache) };
var middleware = new RateLimitMiddleware(async (context) => { context.Response.StatusCode = 200; }, rules);

var context = new DefaultHttpContext();
context.Connection.RemoteIpAddress = System.Net.IPAddress.Parse("127.0.0.1");
context.Request.Path = "/test";

await middleware.InvokeAsync(context);
Assert.AreEqual(200, context.Response.StatusCode);

context = new DefaultHttpContext();
context.Connection.RemoteIpAddress = System.Net.IPAddress.Parse("127.0.0.1");
context.Request.Path = "/test";

await middleware.InvokeAsync(context);
Assert.AreEqual(StatusCodes.Status429TooManyRequests, context.Response.StatusCode);
}
}
}
2 changes: 2 additions & 0 deletions RateLimiter.Tests/RateLimiter.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
<ProjectReference Include="..\RateLimiter\RateLimiter.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.3.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
Expand Down
Loading