Skip to content

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

Merged
merged 34 commits into from
Apr 19, 2022
Merged
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2721e4f
First RateLimiting commit
wtgodbe Feb 25, 2022
caa47a6
More
wtgodbe Feb 25, 2022
d649fca
ctrl-s
wtgodbe Feb 28, 2022
6825683
lil bit
wtgodbe Mar 11, 2022
f7e82a5
Small
wtgodbe Mar 25, 2022
c5f2d94
Check-in launchSettings.json files from Middleware (#40695)
wtgodbe Mar 14, 2022
7782861
Merge main
wtgodbe Mar 30, 2022
2c683fc
sln
wtgodbe Mar 30, 2022
301a4ee
More
wtgodbe Mar 31, 2022
8b1e074
More
wtgodbe Mar 31, 2022
7870d62
More
wtgodbe Apr 1, 2022
16bdcf3
Remove stuff
wtgodbe Apr 1, 2022
4f20092
rm
wtgodbe Apr 1, 2022
a417766
Not SharedFx
wtgodbe Apr 1, 2022
0c43a25
Feedback
wtgodbe Apr 1, 2022
017797d
Internal+IVT
wtgodbe Apr 1, 2022
2b406b0
Feedback
wtgodbe Apr 1, 2022
75aa2cf
Feedback chunk 1
wtgodbe Apr 5, 2022
ade42e0
Feedback chunk 2
wtgodbe Apr 5, 2022
d025eb8
Small feedback
wtgodbe Apr 5, 2022
b7affd6
Merge
wtgodbe Apr 13, 2022
ddddbd4
Fix extension methods
wtgodbe Apr 13, 2022
9abcb07
Small fix
wtgodbe Apr 13, 2022
047ab3d
Func
wtgodbe Apr 13, 2022
cea2af6
Config status code
wtgodbe Apr 13, 2022
4761f1e
Merge remote-tracking branch 'upstream/main' into wtgodbe/RateLimidMi…
wtgodbe Apr 15, 2022
ac0ad26
Feedback, add servicecollection extension
wtgodbe Apr 15, 2022
fc92f12
Update API
wtgodbe Apr 19, 2022
43b16a6
Merge from main
wtgodbe Apr 19, 2022
9d3dffb
Some feedback
wtgodbe Apr 19, 2022
385d939
Lil more feedback
wtgodbe Apr 19, 2022
0b9875b
Partially fix test
wtgodbe Apr 19, 2022
9d992ca
Fix/Add tests
wtgodbe Apr 19, 2022
b90f534
Add another test
wtgodbe Apr 19, 2022
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
40 changes: 38 additions & 2 deletions AspNetCore.sln
Original file line number Diff line number Diff line change
@@ -1698,9 +1698,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ResultsOfTGenerator", "src\
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OpenApi", "OpenApi", "{2299CCD8-8F9C-4F2B-A633-9BF4DA81022B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OpenApi.Tests", "src\OpenApi\test\Microsoft.AspNetCore.OpenApi.Tests.csproj", "{3AEFB466-6310-4F3F-923F-9154224E3629}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.OpenApi.Tests", "src\OpenApi\test\Microsoft.AspNetCore.OpenApi.Tests.csproj", "{3AEFB466-6310-4F3F-923F-9154224E3629}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.OpenApi", "src\OpenApi\src\Microsoft.AspNetCore.OpenApi.csproj", "{EFC8EA45-572D-4D8D-A597-9045A2D8EC40}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.OpenApi", "src\OpenApi\src\Microsoft.AspNetCore.OpenApi.csproj", "{EFC8EA45-572D-4D8D-A597-9045A2D8EC40}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RateLimiting", "src\Middleware\RateLimiting\src\Microsoft.AspNetCore.RateLimiting.csproj", "{8EE73488-2B92-42BD-96C9-0DD65405C828}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RateLimiting.Tests", "src\Middleware\RateLimiting\test\Microsoft.AspNetCore.RateLimiting.Tests.csproj", "{41FF4F96-98D2-4482-A2A7-4B179E80D285}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -10191,6 +10195,38 @@ Global
{EFC8EA45-572D-4D8D-A597-9045A2D8EC40}.Release|x64.Build.0 = Release|Any CPU
{EFC8EA45-572D-4D8D-A597-9045A2D8EC40}.Release|x86.ActiveCfg = Release|Any CPU
{EFC8EA45-572D-4D8D-A597-9045A2D8EC40}.Release|x86.Build.0 = Release|Any CPU
{8EE73488-2B92-42BD-96C9-0DD65405C828}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8EE73488-2B92-42BD-96C9-0DD65405C828}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8EE73488-2B92-42BD-96C9-0DD65405C828}.Debug|arm64.ActiveCfg = Debug|Any CPU
{8EE73488-2B92-42BD-96C9-0DD65405C828}.Debug|arm64.Build.0 = Debug|Any CPU
{8EE73488-2B92-42BD-96C9-0DD65405C828}.Debug|x64.ActiveCfg = Debug|Any CPU
{8EE73488-2B92-42BD-96C9-0DD65405C828}.Debug|x64.Build.0 = Debug|Any CPU
{8EE73488-2B92-42BD-96C9-0DD65405C828}.Debug|x86.ActiveCfg = Debug|Any CPU
{8EE73488-2B92-42BD-96C9-0DD65405C828}.Debug|x86.Build.0 = Debug|Any CPU
{8EE73488-2B92-42BD-96C9-0DD65405C828}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8EE73488-2B92-42BD-96C9-0DD65405C828}.Release|Any CPU.Build.0 = Release|Any CPU
{8EE73488-2B92-42BD-96C9-0DD65405C828}.Release|arm64.ActiveCfg = Release|Any CPU
{8EE73488-2B92-42BD-96C9-0DD65405C828}.Release|arm64.Build.0 = Release|Any CPU
{8EE73488-2B92-42BD-96C9-0DD65405C828}.Release|x64.ActiveCfg = Release|Any CPU
{8EE73488-2B92-42BD-96C9-0DD65405C828}.Release|x64.Build.0 = Release|Any CPU
{8EE73488-2B92-42BD-96C9-0DD65405C828}.Release|x86.ActiveCfg = Release|Any CPU
{8EE73488-2B92-42BD-96C9-0DD65405C828}.Release|x86.Build.0 = Release|Any CPU
{41FF4F96-98D2-4482-A2A7-4B179E80D285}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{41FF4F96-98D2-4482-A2A7-4B179E80D285}.Debug|Any CPU.Build.0 = Debug|Any CPU
{41FF4F96-98D2-4482-A2A7-4B179E80D285}.Debug|arm64.ActiveCfg = Debug|Any CPU
{41FF4F96-98D2-4482-A2A7-4B179E80D285}.Debug|arm64.Build.0 = Debug|Any CPU
{41FF4F96-98D2-4482-A2A7-4B179E80D285}.Debug|x64.ActiveCfg = Debug|Any CPU
{41FF4F96-98D2-4482-A2A7-4B179E80D285}.Debug|x64.Build.0 = Debug|Any CPU
{41FF4F96-98D2-4482-A2A7-4B179E80D285}.Debug|x86.ActiveCfg = Debug|Any CPU
{41FF4F96-98D2-4482-A2A7-4B179E80D285}.Debug|x86.Build.0 = Debug|Any CPU
{41FF4F96-98D2-4482-A2A7-4B179E80D285}.Release|Any CPU.ActiveCfg = Release|Any CPU
{41FF4F96-98D2-4482-A2A7-4B179E80D285}.Release|Any CPU.Build.0 = Release|Any CPU
{41FF4F96-98D2-4482-A2A7-4B179E80D285}.Release|arm64.ActiveCfg = Release|Any CPU
{41FF4F96-98D2-4482-A2A7-4B179E80D285}.Release|arm64.Build.0 = Release|Any CPU
{41FF4F96-98D2-4482-A2A7-4B179E80D285}.Release|x64.ActiveCfg = Release|Any CPU
{41FF4F96-98D2-4482-A2A7-4B179E80D285}.Release|x64.Build.0 = Release|Any CPU
{41FF4F96-98D2-4482-A2A7-4B179E80D285}.Release|x86.ActiveCfg = Release|Any CPU
{41FF4F96-98D2-4482-A2A7-4B179E80D285}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
1 change: 1 addition & 0 deletions eng/ProjectReferences.props
Original file line number Diff line number Diff line change
@@ -89,6 +89,7 @@
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Localization.Routing" ProjectPath="$(RepoRoot)src\Middleware\Localization.Routing\src\Microsoft.AspNetCore.Localization.Routing.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Localization" ProjectPath="$(RepoRoot)src\Middleware\Localization\src\Microsoft.AspNetCore.Localization.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.MiddlewareAnalysis" ProjectPath="$(RepoRoot)src\Middleware\MiddlewareAnalysis\src\Microsoft.AspNetCore.MiddlewareAnalysis.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.RateLimiting" ProjectPath="$(RepoRoot)src\Middleware\RateLimiting\src\Microsoft.AspNetCore.RateLimiting.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.ResponseCaching.Abstractions" ProjectPath="$(RepoRoot)src\Middleware\ResponseCaching.Abstractions\src\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.ResponseCaching" ProjectPath="$(RepoRoot)src\Middleware\ResponseCaching\src\Microsoft.AspNetCore.ResponseCaching.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.ResponseCompression" ProjectPath="$(RepoRoot)src\Middleware\ResponseCompression\src\Microsoft.AspNetCore.ResponseCompression.csproj" />
4 changes: 3 additions & 1 deletion src/Middleware/Middleware.slnf
Original file line number Diff line number Diff line change
@@ -76,6 +76,8 @@
"src\\Middleware\\MiddlewareAnalysis\\samples\\MiddlewareAnalysisSample\\MiddlewareAnalysisSample.csproj",
"src\\Middleware\\MiddlewareAnalysis\\src\\Microsoft.AspNetCore.MiddlewareAnalysis.csproj",
"src\\Middleware\\MiddlewareAnalysis\\test\\Microsoft.AspNetCore.MiddlewareAnalysis.Tests.csproj",
"src\\Middleware\\RateLimiting\\src\\Microsoft.AspNetCore.RateLimiting.csproj",
"src\\Middleware\\RateLimiting\\test\\Microsoft.AspNetCore.RateLimiting.Tests.csproj",
"src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj",
"src\\Middleware\\ResponseCaching\\samples\\ResponseCachingSample\\ResponseCachingSample.csproj",
"src\\Middleware\\ResponseCaching\\src\\Microsoft.AspNetCore.ResponseCaching.csproj",
@@ -115,4 +117,4 @@
"src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj"
]
}
}
}
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>
36 changes: 36 additions & 0 deletions src/Middleware/RateLimiting/src/NoLimiter.cs
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;
}

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;
}
}
6 changes: 6 additions & 0 deletions src/Middleware/RateLimiting/src/Properties/AssemblyInfo.cs
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")]
1 change: 1 addition & 0 deletions src/Middleware/RateLimiting/src/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
11 changes: 11 additions & 0 deletions src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt
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!
48 changes: 48 additions & 0 deletions src/Middleware/RateLimiting/src/RateLimiterOptions.cs
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));
}
}
78 changes: 78 additions & 0 deletions src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs
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));

_logger = logger ?? throw new ArgumentNullException(nameof(logger));

_limiter = options.Value.Limiter;
_onRejected = options.Value.OnRejected;
_rejectionStatusCode = options.Value.DefaultRejectionStatusCode;
}

// TODO - EventSource?
Copy link
Member Author

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an issue tracking this yet?

Copy link
Member Author

Choose a reason for hiding this comment

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