-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Implement minimal RateLimitingMiddleware #41008
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
Changes from all commits
2721e4f
caa47a6
d649fca
6825683
f7e82a5
c5f2d94
7782861
2c683fc
301a4ee
8b1e074
7870d62
16bdcf3
4f20092
a417766
0c43a25
017797d
2b406b0
75aa2cf
ade42e0
d025eb8
b7affd6
ddddbd4
9abcb07
047ab3d
cea2af6
4761f1e
ac0ad26
fc92f12
43b16a6
9d3dffb
385d939
0b9875b
9d992ca
b90f534
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<Description>ASP.NET Core middleware for enforcing rate limiting in an application</Description> | ||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> | ||
<GenerateDocumentationFile>true</GenerateDocumentationFile> | ||
<PackageTags>aspnetcore</PackageTags> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" /> | ||
<Reference Include="Microsoft.Extensions.Logging.Abstractions" /> | ||
<Reference Include="Microsoft.Extensions.Options" /> | ||
<Reference Include="System.Threading.RateLimiting" /> | ||
|
||
<Compile Include="$(SharedSourceRoot)ValueStopwatch\*.cs" /> | ||
</ItemGroup> | ||
|
||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Threading.RateLimiting; | ||
|
||
namespace Microsoft.AspNetCore.RateLimiting; | ||
internal class NoLimiter<TResource> : PartitionedRateLimiter<TResource> | ||
{ | ||
public override int GetAvailablePermits(TResource resourceID) | ||
{ | ||
return 1; | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
protected override RateLimitLease AcquireCore(TResource resourceID, int permitCount) | ||
{ | ||
return new NoLimiterLease(); | ||
} | ||
|
||
protected override ValueTask<RateLimitLease> WaitAsyncCore(TResource resourceID, int permitCount, CancellationToken cancellationToken) | ||
{ | ||
return new ValueTask<RateLimitLease>(new NoLimiterLease()); | ||
} | ||
} | ||
|
||
internal class NoLimiterLease : RateLimitLease | ||
{ | ||
public override bool IsAcquired => true; | ||
|
||
public override IEnumerable<string> MetadataNames => new List<string>(); | ||
|
||
public override bool TryGetMetadata(string metadataName, out object? metadata) | ||
{ | ||
metadata = null; | ||
return false; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Runtime.CompilerServices; | ||
|
||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.RateLimiting.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
#nullable enable |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
Microsoft.AspNetCore.RateLimiting.RateLimiterOptions | ||
Microsoft.AspNetCore.RateLimiting.RateLimiterOptions.DefaultRejectionStatusCode.get -> int | ||
Microsoft.AspNetCore.RateLimiting.RateLimiterOptions.DefaultRejectionStatusCode.set -> void | ||
Microsoft.AspNetCore.RateLimiting.RateLimiterOptions.Limiter.get -> System.Threading.RateLimiting.PartitionedRateLimiter<Microsoft.AspNetCore.Http.HttpContext!>! | ||
Microsoft.AspNetCore.RateLimiting.RateLimiterOptions.Limiter.set -> void | ||
Microsoft.AspNetCore.RateLimiting.RateLimiterOptions.OnRejected.get -> System.Func<Microsoft.AspNetCore.Http.HttpContext!, System.Threading.RateLimiting.RateLimitLease!, System.Threading.Tasks.Task!>! | ||
Microsoft.AspNetCore.RateLimiting.RateLimiterOptions.OnRejected.set -> void | ||
Microsoft.AspNetCore.RateLimiting.RateLimiterOptions.RateLimiterOptions() -> void | ||
Microsoft.AspNetCore.RateLimiting.RateLimitingApplicationBuilderExtensions | ||
static Microsoft.AspNetCore.RateLimiting.RateLimitingApplicationBuilderExtensions.UseRateLimiter(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! | ||
static Microsoft.AspNetCore.RateLimiting.RateLimitingApplicationBuilderExtensions.UseRateLimiter(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, Microsoft.AspNetCore.RateLimiting.RateLimiterOptions! options) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Threading.RateLimiting; | ||
using Microsoft.AspNetCore.Http; | ||
|
||
namespace Microsoft.AspNetCore.RateLimiting; | ||
|
||
/// <summary> | ||
/// Specifies options for the rate limiting middleware. | ||
/// </summary> | ||
public sealed class RateLimiterOptions | ||
{ | ||
// TODO - Provide a default? | ||
private PartitionedRateLimiter<HttpContext> _limiter = new NoLimiter<HttpContext>(); | ||
private Func<HttpContext, RateLimitLease, Task> _onRejected = (context, lease) => | ||
{ | ||
return Task.CompletedTask; | ||
}; | ||
|
||
/// <summary> | ||
/// Gets or sets the <see cref="PartitionedRateLimiter{TResource}"/> | ||
/// </summary> | ||
public PartitionedRateLimiter<HttpContext> Limiter | ||
{ | ||
get => _limiter; | ||
set => _limiter = value ?? throw new ArgumentNullException(nameof(value)); | ||
} | ||
|
||
/// <summary> | ||
/// Gets or sets a <see cref="Func{HttpContext, RateLimitLease, Task}"/> that handles requests rejected by this middleware. | ||
/// </summary> | ||
public Func<HttpContext, RateLimitLease, Task> OnRejected | ||
{ | ||
get => _onRejected; | ||
set => _onRejected = value ?? throw new ArgumentNullException(nameof(value)); | ||
} | ||
|
||
/// <summary> | ||
/// Gets or sets the default status code to set on the response when a request is rejected. | ||
/// Defaults to <see cref="StatusCodes.Status503ServiceUnavailable"/>. | ||
/// </summary> | ||
/// <remarks> | ||
/// This status code will be set before <see cref="OnRejected"/> is called, so any status code set by | ||
/// <see cref="OnRejected"/> will "win" over this default. | ||
/// </remarks> | ||
public int DefaultRejectionStatusCode { get; set; } = StatusCodes.Status503ServiceUnavailable; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using Microsoft.AspNetCore.Builder; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Microsoft.AspNetCore.RateLimiting; | ||
|
||
/// <summary> | ||
/// Extension methods for the RateLimiting middleware. | ||
/// </summary> | ||
public static class RateLimitingApplicationBuilderExtensions | ||
{ | ||
/// <summary> | ||
/// Enables rate limiting for the application. | ||
/// </summary> | ||
/// <param name="app"></param> | ||
/// <returns></returns> | ||
public static IApplicationBuilder UseRateLimiter(this IApplicationBuilder app) | ||
{ | ||
ArgumentNullException.ThrowIfNull(app); | ||
|
||
return app.UseMiddleware<RateLimitingMiddleware>(); | ||
} | ||
|
||
/// <summary> | ||
/// Enables rate limiting for the application. | ||
/// </summary> | ||
/// <param name="app"></param> | ||
/// <param name="options"></param> | ||
/// <returns></returns> | ||
public static IApplicationBuilder UseRateLimiter(this IApplicationBuilder app, RateLimiterOptions options) | ||
{ | ||
ArgumentNullException.ThrowIfNull(app, nameof(app)); | ||
ArgumentNullException.ThrowIfNull(options, nameof(options)); | ||
|
||
return app.UseMiddleware<RateLimitingMiddleware>(Options.Create(options)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Threading.RateLimiting; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Microsoft.AspNetCore.RateLimiting; | ||
|
||
/// <summary> | ||
/// Limits the rate of requests allowed in the application, based on limits set by a user-provided <see cref="PartitionedRateLimiter{TResource}"/>. | ||
/// </summary> | ||
internal sealed partial class RateLimitingMiddleware | ||
{ | ||
private readonly RequestDelegate _next; | ||
private readonly Func<HttpContext, RateLimitLease, Task> _onRejected; | ||
private readonly ILogger _logger; | ||
private readonly PartitionedRateLimiter<HttpContext> _limiter; | ||
private readonly int _rejectionStatusCode; | ||
|
||
/// <summary> | ||
/// Creates a new <see cref="RateLimitingMiddleware"/>. | ||
/// </summary> | ||
/// <param name="next">The <see cref="RequestDelegate"/> representing the next middleware in the pipeline.</param> | ||
/// <param name="logger">The <see cref="ILogger"/> used for logging.</param> | ||
/// <param name="options">The options for the middleware.</param> | ||
public RateLimitingMiddleware(RequestDelegate next, ILogger<RateLimitingMiddleware> logger, IOptions<RateLimiterOptions> options) | ||
{ | ||
_next = next ?? throw new ArgumentNullException(nameof(next)); | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||
|
||
_limiter = options.Value.Limiter; | ||
_onRejected = options.Value.OnRejected; | ||
_rejectionStatusCode = options.Value.DefaultRejectionStatusCode; | ||
} | ||
|
||
// TODO - EventSource? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could be valuable to add EventSource logging to this, part of the next pass? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm fine with waiting for the next pass. It might be better to add EventSource logging to the rate limiter implemenations, but that might be too low level to be useful if there are many. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there an issue tracking this yet? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
/// <summary> | ||
/// Invokes the logic of the middleware. | ||
/// </summary> | ||
/// <param name="context">The <see cref="HttpContext"/>.</param> | ||
/// <returns>A <see cref="Task"/> that completes when the request leaves.</returns> | ||
public async Task Invoke(HttpContext context) | ||
{ | ||
using var lease = await TryAcquireAsync(context); | ||
if (lease.IsAcquired) | ||
{ | ||
await _next(context); | ||
} | ||
else | ||
{ | ||
RateLimiterLog.RequestRejectedLimitsExceeded(_logger); | ||
halter73 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// OnRejected "wins" over DefaultRejectionStatusCode - we set DefaultRejectionStatusCode first, | ||
// then call OnRejected in case it wants to do any further modification of the status code. | ||
context.Response.StatusCode = _rejectionStatusCode; | ||
await _onRejected(context, lease); | ||
} | ||
} | ||
|
||
private ValueTask<RateLimitLease> TryAcquireAsync(HttpContext context) | ||
{ | ||
var lease = _limiter.Acquire(context); | ||
if (lease.IsAcquired) | ||
{ | ||
return ValueTask.FromResult(lease); | ||
} | ||
|
||
return _limiter.WaitAsync(context, cancellationToken: context.RequestAborted); | ||
} | ||
|
||
private static partial class RateLimiterLog | ||
{ | ||
[LoggerMessage(1, LogLevel.Debug, "Rate limits exceeded, rejecting this request.", EventName = "RequestRejectedLimitsExceeded")] | ||
internal static partial void RequestRejectedLimitsExceeded(ILogger logger); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Reference Include="Microsoft.AspNetCore.Http" /> | ||
<Reference Include="Microsoft.AspNetCore.RateLimiting" /> | ||
</ItemGroup> | ||
</Project> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using Microsoft.AspNetCore.Builder; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Testing; | ||
using Microsoft.Extensions.DependencyInjection; | ||
|
||
namespace Microsoft.AspNetCore.RateLimiting; | ||
|
||
public class RateLimitingApplicationBuilderExtensionsTests : LoggedTest | ||
{ | ||
|
||
[Fact] | ||
public void UseRateLimiter_ThrowsOnNullAppBuilder() | ||
{ | ||
Assert.Throws<ArgumentNullException>(() => RateLimitingApplicationBuilderExtensions.UseRateLimiter(null)); | ||
} | ||
|
||
[Fact] | ||
public void UseRateLimiter_ThrowsOnNullOptions() | ||
{ | ||
var appBuilder = new ApplicationBuilder(new ServiceCollection().BuildServiceProvider()); | ||
Assert.Throws<ArgumentNullException>(() => appBuilder.UseRateLimiter(null)); | ||
} | ||
|
||
[Fact] | ||
public void UseRateLimiter_RespectsOptions() | ||
{ | ||
// These are the options that should get used | ||
var options = new RateLimiterOptions(); | ||
options.DefaultRejectionStatusCode = 429; | ||
options.Limiter = new TestPartitionedRateLimiter<HttpContext>(new TestRateLimiter(false)); | ||
|
||
// These should not get used | ||
var services = new ServiceCollection(); | ||
services.Configure<RateLimiterOptions>(options => | ||
{ | ||
options.Limiter = new TestPartitionedRateLimiter<HttpContext>(new TestRateLimiter(false)); | ||
options.DefaultRejectionStatusCode = 404; | ||
}); | ||
services.AddLogging(); | ||
var serviceProvider = services.BuildServiceProvider(); | ||
var appBuilder = new ApplicationBuilder(serviceProvider); | ||
|
||
// Act | ||
appBuilder.UseRateLimiter(options); | ||
var app = appBuilder.Build(); | ||
var context = new DefaultHttpContext(); | ||
app.Invoke(context); | ||
Assert.Equal(429, context.Response.StatusCode); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.AspNetCore.Testing; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Logging.Abstractions; | ||
using Microsoft.Extensions.Options; | ||
|
||
namespace Microsoft.AspNetCore.RateLimiting; | ||
|
||
public class RateLimitingMiddlewareTests : LoggedTest | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
[Fact] | ||
public void Ctor_ThrowsExceptionsWhenNullArgs() | ||
{ | ||
var options = CreateOptionsAccessor(); | ||
options.Value.Limiter = new TestPartitionedRateLimiter<HttpContext>(); | ||
|
||
Assert.Throws<ArgumentNullException>(() => new RateLimitingMiddleware( | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
null, | ||
new NullLoggerFactory().CreateLogger<RateLimitingMiddleware>(), | ||
options)); | ||
|
||
Assert.Throws<ArgumentNullException>(() => new RateLimitingMiddleware(c => | ||
{ | ||
return Task.CompletedTask; | ||
}, | ||
null, | ||
options)); | ||
} | ||
|
||
[Fact] | ||
public async Task RequestsCallNextIfAccepted() | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
var flag = false; | ||
var options = CreateOptionsAccessor(); | ||
options.Value.Limiter = new TestPartitionedRateLimiter<HttpContext>(new TestRateLimiter(true)); | ||
var middleware = new RateLimitingMiddleware(c => | ||
{ | ||
flag = true; | ||
return Task.CompletedTask; | ||
}, | ||
new NullLoggerFactory().CreateLogger<RateLimitingMiddleware>(), | ||
options); | ||
|
||
await middleware.Invoke(new DefaultHttpContext()); | ||
Assert.True(flag); | ||
} | ||
|
||
[Fact] | ||
public async Task RequestRejected_CallsOnRejectedAndGives503() | ||
{ | ||
var onRejectedInvoked = false; | ||
var options = CreateOptionsAccessor(); | ||
options.Value.Limiter = new TestPartitionedRateLimiter<HttpContext>(new TestRateLimiter(false)); | ||
options.Value.OnRejected = (httpContext, lease) => | ||
{ | ||
onRejectedInvoked = true; | ||
return Task.CompletedTask; | ||
}; | ||
|
||
var middleware = new RateLimitingMiddleware(c => | ||
{ | ||
return Task.CompletedTask; | ||
}, | ||
new NullLoggerFactory().CreateLogger<RateLimitingMiddleware>(), | ||
options); | ||
|
||
var context = new DefaultHttpContext(); | ||
await middleware.Invoke(context).DefaultTimeout(); | ||
Assert.True(onRejectedInvoked); | ||
Assert.Equal(StatusCodes.Status503ServiceUnavailable, context.Response.StatusCode); | ||
} | ||
|
||
[Fact] | ||
public async Task RequestRejected_WinsOverDefaultStatusCode() | ||
{ | ||
var onRejectedInvoked = false; | ||
var options = CreateOptionsAccessor(); | ||
options.Value.Limiter = new TestPartitionedRateLimiter<HttpContext>(new TestRateLimiter(false)); | ||
options.Value.OnRejected = (httpContext, lease) => | ||
{ | ||
onRejectedInvoked = true; | ||
httpContext.Response.StatusCode = 429; | ||
return Task.CompletedTask; | ||
}; | ||
|
||
var middleware = new RateLimitingMiddleware(c => | ||
{ | ||
return Task.CompletedTask; | ||
}, | ||
new NullLoggerFactory().CreateLogger<RateLimitingMiddleware>(), | ||
options); | ||
|
||
var context = new DefaultHttpContext(); | ||
await middleware.Invoke(context).DefaultTimeout(); | ||
Assert.True(onRejectedInvoked); | ||
Assert.Equal(StatusCodes.Status429TooManyRequests, context.Response.StatusCode); | ||
} | ||
|
||
[Fact] | ||
public async Task RequestAborted_ThrowsTaskCanceledException() | ||
{ | ||
var options = CreateOptionsAccessor(); | ||
options.Value.Limiter = new TestPartitionedRateLimiter<HttpContext>(new TestRateLimiter(false)); | ||
|
||
var middleware = new RateLimitingMiddleware(c => | ||
{ | ||
return Task.CompletedTask; | ||
}, | ||
new NullLoggerFactory().CreateLogger<RateLimitingMiddleware>(), | ||
options); | ||
|
||
var context = new DefaultHttpContext(); | ||
context.RequestAborted = new CancellationToken(true); | ||
await Assert.ThrowsAsync<TaskCanceledException>(() => middleware.Invoke(context)).DefaultTimeout(); | ||
} | ||
|
||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
private IOptions<RateLimiterOptions> CreateOptionsAccessor() => Options.Create(new RateLimiterOptions()); | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using Microsoft.AspNetCore.Http; | ||
|
||
namespace Microsoft.AspNetCore.RateLimiting; | ||
|
||
public class RateLimitingOptionsTests | ||
{ | ||
[Fact] | ||
public void ThrowsOnNullLimiter() | ||
{ | ||
var options = new RateLimiterOptions(); | ||
Assert.Throws<ArgumentNullException>(() => options.Limiter = null); | ||
} | ||
|
||
[Fact] | ||
public void ThrowsOnNullOnRejected() | ||
{ | ||
var options = new RateLimiterOptions(); | ||
Assert.Throws<ArgumentNullException>(() => options.OnRejected = null); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading.RateLimiting; | ||
using System.Threading.Tasks; | ||
|
||
namespace Microsoft.AspNetCore.RateLimiting; | ||
|
||
internal class TestPartitionedRateLimiter<TResource> : PartitionedRateLimiter<TResource> | ||
{ | ||
private List<RateLimiter> limiters = new List<RateLimiter>(); | ||
|
||
public TestPartitionedRateLimiter() { } | ||
|
||
public TestPartitionedRateLimiter(RateLimiter limiter) | ||
{ | ||
limiters.Add(limiter); | ||
} | ||
|
||
public void AddLimiter(RateLimiter limiter) | ||
{ | ||
limiters.Add(limiter); | ||
} | ||
|
||
public override int GetAvailablePermits(TResource resourceID) | ||
{ | ||
throw new NotImplementedException(); | ||
} | ||
|
||
protected override RateLimitLease AcquireCore(TResource resourceID, int permitCount) | ||
{ | ||
if (permitCount != 1) | ||
{ | ||
throw new ArgumentException("Tests only support 1 permit at a time"); | ||
} | ||
var leases = new List<RateLimitLease>(); | ||
foreach (var limiter in limiters) | ||
{ | ||
var lease = limiter.Acquire(); | ||
if (lease.IsAcquired) | ||
{ | ||
leases.Add(lease); | ||
} | ||
else | ||
{ | ||
foreach (var unusedLease in leases) | ||
{ | ||
unusedLease.Dispose(); | ||
} | ||
return new TestRateLimitLease(false, null); | ||
} | ||
} | ||
return new TestRateLimitLease(true, leases); | ||
} | ||
|
||
protected override async ValueTask<RateLimitLease> WaitAsyncCore(TResource resourceID, int permitCount, CancellationToken cancellationToken) | ||
{ | ||
if (permitCount != 1) | ||
{ | ||
throw new ArgumentException("Tests only support 1 permit at a time"); | ||
} | ||
var leases = new List<RateLimitLease>(); | ||
foreach (var limiter in limiters) | ||
{ | ||
leases.Add(await limiter.WaitAsync().ConfigureAwait(false)); | ||
} | ||
foreach (var lease in leases) | ||
{ | ||
if (!lease.IsAcquired) | ||
{ | ||
foreach (var unusedLease in leases) | ||
{ | ||
unusedLease.Dispose(); | ||
} | ||
return new TestRateLimitLease(false, null); | ||
} | ||
} | ||
return new TestRateLimitLease(true, leases); | ||
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Threading.RateLimiting; | ||
|
||
namespace Microsoft.AspNetCore.RateLimiting; | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
internal class TestRateLimitLease : RateLimitLease | ||
{ | ||
internal List<RateLimitLease> _leases; | ||
|
||
public TestRateLimitLease(bool isAcquired, List<RateLimitLease> leases) | ||
{ | ||
IsAcquired = isAcquired; | ||
_leases = leases; | ||
} | ||
|
||
public override bool IsAcquired { get; } | ||
|
||
public override IEnumerable<string> MetadataNames => throw new NotImplementedException(); | ||
|
||
public override bool TryGetMetadata(string metadataName, out object metadata) | ||
{ | ||
throw new NotImplementedException(); | ||
} | ||
|
||
protected override void Dispose(bool disposing) | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
if (_leases != null) | ||
{ | ||
foreach (var lease in _leases) | ||
{ | ||
lease.Dispose(); | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Threading.RateLimiting; | ||
|
||
namespace Microsoft.AspNetCore.RateLimiting; | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
internal class TestRateLimiter : RateLimiter | ||
{ | ||
private readonly bool _alwaysAccept; | ||
|
||
public TestRateLimiter(bool alwaysAccept) | ||
{ | ||
_alwaysAccept = alwaysAccept; | ||
} | ||
|
||
public override TimeSpan? IdleDuration => throw new NotImplementedException(); | ||
|
||
public override int GetAvailablePermits() | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
throw new NotImplementedException(); | ||
} | ||
|
||
protected override RateLimitLease AcquireCore(int permitCount) | ||
{ | ||
return new TestRateLimitLease(_alwaysAccept, null); | ||
} | ||
|
||
protected override ValueTask<RateLimitLease> WaitAsyncCore(int permitCount, CancellationToken cancellationToken) | ||
{ | ||
cancellationToken.ThrowIfCancellationRequested(); | ||
return new ValueTask<RateLimitLease>(new TestRateLimitLease(_alwaysAccept, null)); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.