Documentation Index
Fetch the complete documentation index at: https://mintlify.com/nimanikoo/Dotnet-RateLimiter/llms.txt
Use this file to discover all available pages before exploring further.
Dotnet-RateLimiter ships with a focused xUnit test suite that exercises RedisRateLimitingMiddleware in complete isolation from Redis. By mocking RedisRateLimiter with Moq, each test controls exactly what IsAllowedAsync returns, letting you verify the middleware’s key-building logic, identity resolution, and HTTP response codes without a live Redis instance.
Test Setup
Framework: xUnit 2.9.3
Mocking: Moq 4.20.72
Target framework: net10.0
The test class constructor builds a Mock<RedisRateLimiter> by first creating a Mock<IConnectionMultiplexer> and passing it to the RedisRateLimiter constructor. This is required because RedisRateLimiter takes an IConnectionMultiplexer as its sole constructor argument and uses it to obtain an IDatabase reference internally.
public RateLimitingMiddlewareTests()
{
var mockMultiplexer = new Mock<IConnectionMultiplexer>();
_mockLimiter = new Mock<RedisRateLimiter>(mockMultiplexer.Object);
}
The IsAllowedAsync method is declared virtual in RedisRateLimiter, which is a deliberate design choice — it is the prerequisite for Moq to intercept and override calls to that method in tests.
Test Cases
Full test class
public class RateLimitingMiddlewareTests
{
private readonly Mock<RedisRateLimiter> _mockLimiter;
private readonly RequestDelegate _next = (innerHttpContext) => Task.CompletedTask;
public RateLimitingMiddlewareTests()
{
var mockMultiplexer = new Mock<IConnectionMultiplexer>();
_mockLimiter = new Mock<RedisRateLimiter>(mockMultiplexer.Object);
}
[Fact]
public async Task InvokeAsync_AuthenticatedUser_UsesUserIdInKey()
{
// Arrange
var context = new DefaultHttpContext();
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, "user-123") };
var identity = new ClaimsIdentity(claims, "TestAuth");
context.User = new ClaimsPrincipal(identity);
var endpoint = new Endpoint(null, new EndpointMetadataCollection(new RedisRateLimitAttribute(10, 60)), "Test");
context.SetEndpoint(endpoint);
var middleware = new RedisRateLimitingMiddleware(_next, _mockLimiter.Object);
_mockLimiter.Setup(l => l.IsAllowedAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<TimeSpan>()))
.ReturnsAsync(true);
// Act
await middleware.InvokeAsync(context);
// Assert
_mockLimiter.Verify(l => l.IsAllowedAsync(
It.Is<string>(s => s.Contains("user:user-123")),
It.IsAny<int>(),
It.IsAny<TimeSpan>()),
Times.Once);
}
[Fact]
public async Task InvokeAsync_WhenRateLimitExceeded_Returns429()
{
// Arrange
var context = new DefaultHttpContext();
context.Connection.RemoteIpAddress = System.Net.IPAddress.Parse("127.0.0.1");
var endpoint = new Endpoint(null, new EndpointMetadataCollection(new RedisRateLimitAttribute(5, 60)), "Test");
context.SetEndpoint(endpoint);
var middleware = new RedisRateLimitingMiddleware(_next, _mockLimiter.Object);
_mockLimiter.Setup(l => l.IsAllowedAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<TimeSpan>()))
.ReturnsAsync(false);
// Act
await middleware.InvokeAsync(context);
// Assert
Assert.Equal(StatusCodes.Status429TooManyRequests, context.Response.StatusCode);
}
}
InvokeAsync_AuthenticatedUser_UsesUserIdInKey
Verifies that when a request carries a ClaimTypes.NameIdentifier claim of "user-123", the middleware calls IsAllowedAsync with a key that contains the substring "user:user-123". This confirms that the userType prefix and the claim-derived identity value are concatenated correctly before being forwarded to the Lua rate limiter.
InvokeAsync_WhenRateLimitExceeded_Returns429
Verifies the HTTP response code path. IsAllowedAsync is mocked to return false (rate limit exceeded), and the test asserts that the response status code is 429 Too Many Requests. The request is sent as an anonymous (unauthenticated) request with RemoteIpAddress set to 127.0.0.1.
Running the Tests
Run the full suite locally with:
Or with verbose output to see each test name:
dotnet test --verbosity normal
CI/CD pipeline
The .github/workflows/dotnet.yml workflow runs the test suite automatically on every push and pull request targeting the master branch:
name: .NET
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
Key Testing Patterns
Mocking IsAllowedAsync — allow or block
Return true to simulate a request within the rate limit:
_mockLimiter.Setup(l => l.IsAllowedAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<TimeSpan>()))
.ReturnsAsync(true);
Return false to simulate an exceeded rate limit (triggers 429):
_mockLimiter.Setup(l => l.IsAllowedAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<TimeSpan>()))
.ReturnsAsync(false);
Attaching RedisRateLimitAttribute to a fake endpoint
The middleware reads RedisRateLimitAttribute from endpoint.Metadata. Use EndpointMetadataCollection to attach it to a synthetic Endpoint object:
var endpoint = new Endpoint(
null,
new EndpointMetadataCollection(new RedisRateLimitAttribute(10, 60)),
"Test"
);
context.SetEndpoint(endpoint);
The first argument to RedisRateLimitAttribute is MaxRequests and the second is WindowSeconds.
Simulating an authenticated user
Build a ClaimsIdentity with an authentication type string (any non-null value marks it as authenticated) and a NameIdentifier claim, then assign it to context.User:
var claims = new[] { new Claim(ClaimTypes.NameIdentifier, "user-123") };
var identity = new ClaimsIdentity(claims, "TestAuth");
context.User = new ClaimsPrincipal(identity);
Simulating an anonymous user
Leave context.User at its default (unauthenticated) state and set the remote IP address instead:
context.Connection.RemoteIpAddress = System.Net.IPAddress.Parse("127.0.0.1");
The middleware will fall into the guest branch and use the IP address as identityKey.
The virtual keyword on IsAllowedAsync in RedisRateLimiter is intentional — it is required for Moq to generate a proxy class that overrides the method. Without virtual, calling .Setup() on the mock would have no effect and the real Redis-backed implementation would be invoked instead, causing the test to fail at runtime.