From 2721e4f94e3033dbec7c54903a7a8fbe9459c425 Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Fri, 25 Feb 2022 09:24:13 -0800 Subject: [PATCH 01/30] First RateLimiting commit --- AspNetCore.sln | 40 ++++++++++++++++++- src/Middleware/Middleware.slnf | 4 +- .../Microsoft.AspNetCore.RateLimiting.csproj | 20 ++++++++++ .../src/Policies/RateLimitingPolicy.cs | 13 ++++++ .../RateLimiting/src/PublicAPI.Shipped.txt | 1 + .../RateLimiting/src/PublicAPI.Unshipped.txt | 1 + .../src/RateLimitingMiddleware.cs | 24 +++++++++++ .../RateLimiting/src/RateLimitingOptions.cs | 8 ++++ ...osoft.AspNetCore.RateLimiting.Tests.csproj | 17 ++++++++ 9 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 src/Middleware/RateLimiting/src/Microsoft.AspNetCore.RateLimiting.csproj create mode 100644 src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs create mode 100644 src/Middleware/RateLimiting/src/PublicAPI.Shipped.txt create mode 100644 src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt create mode 100644 src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs create mode 100644 src/Middleware/RateLimiting/src/RateLimitingOptions.cs create mode 100644 src/Middleware/RateLimiting/test/Microsoft.AspNetCore.RateLimiting.Tests.csproj diff --git a/AspNetCore.sln b/AspNetCore.sln index e78367fb74db..716065835aac 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1654,14 +1654,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SDK-Analyzers", "SDK-Analyz EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Components", "Components", "{CC45FA2D-128B-485D-BA6D-DFD9735CB3C3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Components.SdkAnalyzers", "src\Tools\SDK-Analyzers\Components\src\Microsoft.AspNetCore.Components.SdkAnalyzers.csproj", "{825BCF97-67A9-4834-B3A8-C3DC97A90E41}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.SdkAnalyzers", "src\Tools\SDK-Analyzers\Components\src\Microsoft.AspNetCore.Components.SdkAnalyzers.csproj", "{825BCF97-67A9-4834-B3A8-C3DC97A90E41}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Components.SdkAnalyzers.Tests", "src\Tools\SDK-Analyzers\Components\test\Microsoft.AspNetCore.Components.SdkAnalyzers.Tests.csproj", "{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.SdkAnalyzers.Tests", "src\Tools\SDK-Analyzers\Components\test\Microsoft.AspNetCore.Components.SdkAnalyzers.Tests.csproj", "{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LinkabilityChecker", "LinkabilityChecker", "{94F95276-7CDF-44A8-B159-D09702EF6794}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinkabilityChecker", "src\Tools\LinkabilityChecker\LinkabilityChecker.csproj", "{EA7D844B-C180-41C7-9D55-273AD88BF71F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RateLimiting", "src\Middleware\RateLimiting\src\Microsoft.AspNetCore.RateLimiting.csproj", "{49DB48C3-9ABF-4379-918F-B4424290B667}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RateLimiting.Tests", "src\Middleware\RateLimiting\test\Microsoft.AspNetCore.RateLimiting.Tests.csproj", "{F0FFF391-F642-4E0A-A044-D35376C62181}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -9954,6 +9958,38 @@ Global {EA7D844B-C180-41C7-9D55-273AD88BF71F}.Release|x64.Build.0 = Release|Any CPU {EA7D844B-C180-41C7-9D55-273AD88BF71F}.Release|x86.ActiveCfg = Release|Any CPU {EA7D844B-C180-41C7-9D55-273AD88BF71F}.Release|x86.Build.0 = Release|Any CPU + {49DB48C3-9ABF-4379-918F-B4424290B667}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {49DB48C3-9ABF-4379-918F-B4424290B667}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49DB48C3-9ABF-4379-918F-B4424290B667}.Debug|arm64.ActiveCfg = Debug|Any CPU + {49DB48C3-9ABF-4379-918F-B4424290B667}.Debug|arm64.Build.0 = Debug|Any CPU + {49DB48C3-9ABF-4379-918F-B4424290B667}.Debug|x64.ActiveCfg = Debug|Any CPU + {49DB48C3-9ABF-4379-918F-B4424290B667}.Debug|x64.Build.0 = Debug|Any CPU + {49DB48C3-9ABF-4379-918F-B4424290B667}.Debug|x86.ActiveCfg = Debug|Any CPU + {49DB48C3-9ABF-4379-918F-B4424290B667}.Debug|x86.Build.0 = Debug|Any CPU + {49DB48C3-9ABF-4379-918F-B4424290B667}.Release|Any CPU.ActiveCfg = Release|Any CPU + {49DB48C3-9ABF-4379-918F-B4424290B667}.Release|Any CPU.Build.0 = Release|Any CPU + {49DB48C3-9ABF-4379-918F-B4424290B667}.Release|arm64.ActiveCfg = Release|Any CPU + {49DB48C3-9ABF-4379-918F-B4424290B667}.Release|arm64.Build.0 = Release|Any CPU + {49DB48C3-9ABF-4379-918F-B4424290B667}.Release|x64.ActiveCfg = Release|Any CPU + {49DB48C3-9ABF-4379-918F-B4424290B667}.Release|x64.Build.0 = Release|Any CPU + {49DB48C3-9ABF-4379-918F-B4424290B667}.Release|x86.ActiveCfg = Release|Any CPU + {49DB48C3-9ABF-4379-918F-B4424290B667}.Release|x86.Build.0 = Release|Any CPU + {F0FFF391-F642-4E0A-A044-D35376C62181}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F0FFF391-F642-4E0A-A044-D35376C62181}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F0FFF391-F642-4E0A-A044-D35376C62181}.Debug|arm64.ActiveCfg = Debug|Any CPU + {F0FFF391-F642-4E0A-A044-D35376C62181}.Debug|arm64.Build.0 = Debug|Any CPU + {F0FFF391-F642-4E0A-A044-D35376C62181}.Debug|x64.ActiveCfg = Debug|Any CPU + {F0FFF391-F642-4E0A-A044-D35376C62181}.Debug|x64.Build.0 = Debug|Any CPU + {F0FFF391-F642-4E0A-A044-D35376C62181}.Debug|x86.ActiveCfg = Debug|Any CPU + {F0FFF391-F642-4E0A-A044-D35376C62181}.Debug|x86.Build.0 = Debug|Any CPU + {F0FFF391-F642-4E0A-A044-D35376C62181}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F0FFF391-F642-4E0A-A044-D35376C62181}.Release|Any CPU.Build.0 = Release|Any CPU + {F0FFF391-F642-4E0A-A044-D35376C62181}.Release|arm64.ActiveCfg = Release|Any CPU + {F0FFF391-F642-4E0A-A044-D35376C62181}.Release|arm64.Build.0 = Release|Any CPU + {F0FFF391-F642-4E0A-A044-D35376C62181}.Release|x64.ActiveCfg = Release|Any CPU + {F0FFF391-F642-4E0A-A044-D35376C62181}.Release|x64.Build.0 = Release|Any CPU + {F0FFF391-F642-4E0A-A044-D35376C62181}.Release|x86.ActiveCfg = Release|Any CPU + {F0FFF391-F642-4E0A-A044-D35376C62181}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Middleware/Middleware.slnf b/src/Middleware/Middleware.slnf index ea55360ff484..2321ac94ea79 100644 --- a/src/Middleware/Middleware.slnf +++ b/src/Middleware/Middleware.slnf @@ -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" ] } -} \ No newline at end of file +} diff --git a/src/Middleware/RateLimiting/src/Microsoft.AspNetCore.RateLimiting.csproj b/src/Middleware/RateLimiting/src/Microsoft.AspNetCore.RateLimiting.csproj new file mode 100644 index 000000000000..72aff185ffda --- /dev/null +++ b/src/Middleware/RateLimiting/src/Microsoft.AspNetCore.RateLimiting.csproj @@ -0,0 +1,20 @@ + + + + ASP.NET Core middleware for enforcing rate limiting in an application + $(DefaultNetCoreTargetFramework) + true + aspnetcore + enable + + + + + + + + + + + + diff --git a/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs b/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs new file mode 100644 index 000000000000..6ea6c02f4975 --- /dev/null +++ b/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs @@ -0,0 +1,13 @@ +// 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.Policies; +public class RateLimitingPolicy +{ + private readonly RateLimiter _limiter; + private readonly QueueProcessingOrder _order; + private readonly Endpoint _endpoint; +} diff --git a/src/Middleware/RateLimiting/src/PublicAPI.Shipped.txt b/src/Middleware/RateLimiting/src/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..ab058de62d44 --- /dev/null +++ b/src/Middleware/RateLimiting/src/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..19b8f0c1ea67 --- /dev/null +++ b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware.RateLimitingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options) -> void \ No newline at end of file diff --git a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs new file mode 100644 index 000000000000..b0d62ee126c8 --- /dev/null +++ b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs @@ -0,0 +1,24 @@ +// 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.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.RateLimiting; +public class RateLimitingMiddleware +{ + + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public RateLimitingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IOptions options) + { + _next = next; + _logger = loggerFactory.CreateLogger(); + } + + public async Task Invoke(HttpContext context) + { + } +} diff --git a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs new file mode 100644 index 000000000000..303f1e3b78d4 --- /dev/null +++ b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.RateLimiting; + +public class RateLimitingOptions +{ +} diff --git a/src/Middleware/RateLimiting/test/Microsoft.AspNetCore.RateLimiting.Tests.csproj b/src/Middleware/RateLimiting/test/Microsoft.AspNetCore.RateLimiting.Tests.csproj new file mode 100644 index 000000000000..5bb19e39bebf --- /dev/null +++ b/src/Middleware/RateLimiting/test/Microsoft.AspNetCore.RateLimiting.Tests.csproj @@ -0,0 +1,17 @@ + + + + $(DefaultNetCoreTargetFramework) + + + + + + + + + + + + + From caa47a60bd6e9cef69bafc5c14d4aeb57430fc37 Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Fri, 25 Feb 2022 12:58:07 -0800 Subject: [PATCH 02/30] More --- .../Policies/IRateLimitingPolicyMetadata.cs | 12 ++++++ .../src/Policies/RateLimitingPolicy.cs | 1 - .../src/Policies/RateLimitingPolicyBuilder.cs | 8 ++++ .../Policies/RateLimitingPolicyMetadata.cs | 25 ++++++++++++ .../RateLimiting/src/PublicAPI.Unshipped.txt | 12 +++++- ...tingEndpointConventionBuilderExtensions.cs | 38 +++++++++++++++++++ 6 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 src/Middleware/RateLimiting/src/Policies/IRateLimitingPolicyMetadata.cs create mode 100644 src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyBuilder.cs create mode 100644 src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyMetadata.cs create mode 100644 src/Middleware/RateLimiting/src/RateLimitingEndpointConventionBuilderExtensions.cs diff --git a/src/Middleware/RateLimiting/src/Policies/IRateLimitingPolicyMetadata.cs b/src/Middleware/RateLimiting/src/Policies/IRateLimitingPolicyMetadata.cs new file mode 100644 index 000000000000..b22fb9df6515 --- /dev/null +++ b/src/Middleware/RateLimiting/src/Policies/IRateLimitingPolicyMetadata.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.RateLimiting.Policies; + +public interface IRateLimitingPolicyMetadata +{ + /// + /// The policy which needs to be applied. + /// + RateLimitingPolicy Policy { get; } +} diff --git a/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs b/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs index 6ea6c02f4975..f9bae83679d5 100644 --- a/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs +++ b/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs @@ -9,5 +9,4 @@ public class RateLimitingPolicy { private readonly RateLimiter _limiter; private readonly QueueProcessingOrder _order; - private readonly Endpoint _endpoint; } diff --git a/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyBuilder.cs b/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyBuilder.cs new file mode 100644 index 000000000000..b635fd4ea69f --- /dev/null +++ b/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyBuilder.cs @@ -0,0 +1,8 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.RateLimiting.Policies; +public class RateLimitingPolicyBuilder +{ + private readonly RateLimitingPolicy _policy = new RateLimitingPolicy(); +} diff --git a/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyMetadata.cs b/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyMetadata.cs new file mode 100644 index 000000000000..481a8f41b3d1 --- /dev/null +++ b/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyMetadata.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.RateLimiting.Policies; + +/// +/// Metadata that provides a Rate Limiting policy. +/// +public class RateLimitingPolicyMetadata : IRateLimitingPolicyMetadata +{ + + /// + /// Creates a new instance of using the specified policy. + /// + /// The policy which needs to be applied. + public RateLimitingPolicyMetadata(RateLimitingPolicy policy) + { + Policy = policy; + } + + /// + /// The policy which needs to be applied. + /// + public RateLimitingPolicy Policy { get; } +} diff --git a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt index 19b8f0c1ea67..b8f4ca3126fa 100644 --- a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt @@ -1 +1,11 @@ -Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware.RateLimitingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options) -> void \ No newline at end of file +Microsoft.AspNetCore.Builder.RateLimitingEndpointConventionBuilderExtensions +Microsoft.AspNetCore.RateLimiting.Policies.IRateLimitingPolicyMetadata +Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicy +Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyBuilder +Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyMetadata +Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyMetadata.Policy.get -> Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicy! +Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyMetadata.RateLimitingPolicyMetadata(Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicy! policy) -> void +Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware +Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware.RateLimitingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options) -> void +Microsoft.AspNetCore.RateLimiting.RateLimitingOptions +static Microsoft.AspNetCore.Builder.RateLimitingEndpointConventionBuilderExtensions.RequireRateLimiting(this TBuilder builder, System.Action! configurePolicy) -> TBuilder \ No newline at end of file diff --git a/src/Middleware/RateLimiting/src/RateLimitingEndpointConventionBuilderExtensions.cs b/src/Middleware/RateLimiting/src/RateLimitingEndpointConventionBuilderExtensions.cs new file mode 100644 index 000000000000..ba5598431649 --- /dev/null +++ b/src/Middleware/RateLimiting/src/RateLimitingEndpointConventionBuilderExtensions.cs @@ -0,0 +1,38 @@ +// 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.RateLimiting.Policies; + +namespace Microsoft.AspNetCore.Builder; +public static class RateLimitingEndpointConventionBuilderExtensions +{ + /// + /// Adds the specified Rate Limiting policy to the endpoint(s). + /// + /// The endpoint convention builder. + /// A delegate which can use a policy builder to build a policy. + /// The original convention builder parameter. + public static TBuilder RequireRateLimiting(this TBuilder builder, Action configurePolicy) where TBuilder : IEndpointConventionBuilder + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (configurePolicy == null) + { + throw new ArgumentNullException(nameof(configurePolicy)); + } + + var policyBuilder = new RateLimitingPolicyBuilder(); + configurePolicy(policyBuilder); + var policy = policyBuilder.Build(); + + builder.Add(endpointBuilder => + { + endpointBuilder.Metadata.Add(new RateLimitingPolicyMetadata(policy)); + }); + return builder; + } +} From d649fca472182515ed24f6343e2ea94c3c9e904a Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Sun, 27 Feb 2022 20:04:19 -0800 Subject: [PATCH 03/30] ctrl-s --- Rate_limiting_middleware_Qs.txt | 30 ++++++++++++++ .../Policies/IRateLimitingPolicyMetadata.cs | 4 +- .../src/Policies/RateLimitingPolicy.cs | 18 ++++++++- .../src/Policies/RateLimitingPolicyBuilder.cs | 8 ---- .../Policies/RateLimitingPolicyMetadata.cs | 10 ++--- .../RateLimiting/src/PublicAPI.Unshipped.txt | 13 ++++-- ...tingEndpointConventionBuilderExtensions.cs | 16 ++++---- .../RateLimiting/src/RateLimitingOptions.cs | 40 +++++++++++++++++++ .../src/RateLimitingOptionsExtensions.cs | 23 +++++++++++ 9 files changed, 133 insertions(+), 29 deletions(-) create mode 100644 Rate_limiting_middleware_Qs.txt delete mode 100644 src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyBuilder.cs create mode 100644 src/Middleware/RateLimiting/src/RateLimitingOptionsExtensions.cs diff --git a/Rate_limiting_middleware_Qs.txt b/Rate_limiting_middleware_Qs.txt new file mode 100644 index 000000000000..3b160b70e36c --- /dev/null +++ b/Rate_limiting_middleware_Qs.txt @@ -0,0 +1,30 @@ +Rate limiting middleware Q's + +- How exactly does user set which RateLimiter we're using? + Pass instance? + .WithConcurrencyLimiter()/.WithTokenBucketLimiter()? +- Do we provide defaults for existing RateLimiters (TokenBucket, Concurrency) +- HttpContext has endpoint - how does mapping of name:policy work? + https://github.com/dotnet/aspnetcore/blob/f23cfb88eb0af4f9dce3341c37fc7399e80f8a11/src/Middleware/CORS/src/Infrastructure/CorsMiddleware.cs#L117-L123 + Set metadata on endpoint, check against that for resolution +- How does user add policy to endpoint? + https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/CORS/src/Infrastructure/CorsEndpointConventionBuilderExtensions.cs + - What do defaults look like for this? + +What's going on here? https://github.com/dotnet/aspnetcore/blob/cceceeb21a06573425009c3d547bd9e297bf0f06/src/Middleware/CORS/src/Infrastructure/CorsMiddlewareExtensions.cs#L34-L66 + +Current idea - looks a lot like CORS. User can either set a global policy (DefaultPolicy), or per-endpoint policy. Policies live in Options. When there's a per-endpoint policy, it gets added as metadata to the endpoint. In Middleware, we check metadata on endpoint for policy - if there, use that. Else use default. + If a user wants ratelimiting only on certain endpoints, do we have no default, or have a NoPolicy? + +https://github.com/dotnet/aspnetcore/blob/5141acd2ef5ce41528178f7b1f89ffeaa9242e86/src/Middleware/CORS/src/Infrastructure/CorsOptions.cs#L13-L16 - why Task and not just Policy? + +For multiple limiters on an endpoint/pipeline - must be able to use them in the order the user specifies + +GenericRateLimiter is not a RateLimiter - Policy should keep both, do either/or-ing based on which one it has + The TResource in GenericRateLimiter is HttpContext today - will get funky when it's not just HttpContext later + +What to do when rejecting? ConcurrencyLimiterMiddleware gives 503 + +What to do about aggregate limiters? Runtime or Middleware?? + +What if user wants to apply endpoint policy _and_ default policy? DisableDefault? diff --git a/src/Middleware/RateLimiting/src/Policies/IRateLimitingPolicyMetadata.cs b/src/Middleware/RateLimiting/src/Policies/IRateLimitingPolicyMetadata.cs index b22fb9df6515..ad2686e0743f 100644 --- a/src/Middleware/RateLimiting/src/Policies/IRateLimitingPolicyMetadata.cs +++ b/src/Middleware/RateLimiting/src/Policies/IRateLimitingPolicyMetadata.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.RateLimiting.Policies; public interface IRateLimitingPolicyMetadata { /// - /// The policy which needs to be applied. + /// The name of the policy which needs to be applied. /// - RateLimitingPolicy Policy { get; } + string Name { get; } } diff --git a/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs b/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs index f9bae83679d5..ffc318c7c786 100644 --- a/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs +++ b/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs @@ -5,8 +5,22 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.RateLimiting.Policies; -public class RateLimitingPolicy +internal class RateLimitingPolicy { private readonly RateLimiter _limiter; - private readonly QueueProcessingOrder _order; + //private readonly RateLimiter _limiterOfT; + + public RateLimitingPolicy(RateLimiter limiter) + { + //Error handling() + _limiter = limiter; + } + + /* + public RateLimitingPolicy(RateLimiter limiter) + { + //Error handling() + _limiter = limiter; + } + */ } diff --git a/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyBuilder.cs b/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyBuilder.cs deleted file mode 100644 index b635fd4ea69f..000000000000 --- a/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyBuilder.cs +++ /dev/null @@ -1,8 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.RateLimiting.Policies; -public class RateLimitingPolicyBuilder -{ - private readonly RateLimitingPolicy _policy = new RateLimitingPolicy(); -} diff --git a/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyMetadata.cs b/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyMetadata.cs index 481a8f41b3d1..a64f1d6c65da 100644 --- a/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyMetadata.cs +++ b/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyMetadata.cs @@ -12,14 +12,14 @@ public class RateLimitingPolicyMetadata : IRateLimitingPolicyMetadata /// /// Creates a new instance of using the specified policy. /// - /// The policy which needs to be applied. - public RateLimitingPolicyMetadata(RateLimitingPolicy policy) + /// The name of the policy which needs to be applied. + public RateLimitingPolicyMetadata(string name) { - Policy = policy; + Name = name; } /// - /// The policy which needs to be applied. + /// The name of the policy which needs to be applied. /// - public RateLimitingPolicy Policy { get; } + public string Name { get; } } diff --git a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt index b8f4ca3126fa..f741df168b83 100644 --- a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt @@ -1,11 +1,18 @@ Microsoft.AspNetCore.Builder.RateLimitingEndpointConventionBuilderExtensions Microsoft.AspNetCore.RateLimiting.Policies.IRateLimitingPolicyMetadata +Microsoft.AspNetCore.RateLimiting.Policies.IRateLimitingPolicyMetadata.Name.get -> string! Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicy +Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicy.Order.get -> System.Threading.RateLimiting.QueueProcessingOrder +Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicy.Order.set -> void +Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicy.RateLimitingPolicy() -> void Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyBuilder +Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyBuilder.Build() -> Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicy! +Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyBuilder.UseNewestFirst() -> Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyBuilder! +Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyBuilder.UseOldestFirst() -> Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyBuilder! Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyMetadata -Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyMetadata.Policy.get -> Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicy! -Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyMetadata.RateLimitingPolicyMetadata(Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicy! policy) -> void +Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyMetadata.Name.get -> string! +Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyMetadata.RateLimitingPolicyMetadata(string! name) -> void Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware.RateLimitingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options) -> void Microsoft.AspNetCore.RateLimiting.RateLimitingOptions -static Microsoft.AspNetCore.Builder.RateLimitingEndpointConventionBuilderExtensions.RequireRateLimiting(this TBuilder builder, System.Action! configurePolicy) -> TBuilder \ No newline at end of file +static Microsoft.AspNetCore.Builder.RateLimitingEndpointConventionBuilderExtensions.RequireRateLimiting(this TBuilder builder, Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! options, string! name) -> TBuilder \ No newline at end of file diff --git a/src/Middleware/RateLimiting/src/RateLimitingEndpointConventionBuilderExtensions.cs b/src/Middleware/RateLimiting/src/RateLimitingEndpointConventionBuilderExtensions.cs index ba5598431649..c2887eee3fac 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingEndpointConventionBuilderExtensions.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingEndpointConventionBuilderExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.RateLimiting.Policies; namespace Microsoft.AspNetCore.Builder; @@ -11,28 +12,25 @@ public static class RateLimitingEndpointConventionBuilderExtensions /// Adds the specified Rate Limiting policy to the endpoint(s). /// /// The endpoint convention builder. - /// A delegate which can use a policy builder to build a policy. + /// The name of the RateLimiter to add to the endpoint. /// The original convention builder parameter. - public static TBuilder RequireRateLimiting(this TBuilder builder, Action configurePolicy) where TBuilder : IEndpointConventionBuilder + public static TBuilder RequireRateLimiting(this TBuilder builder, String name) where TBuilder : IEndpointConventionBuilder { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } - if (configurePolicy == null) + if (name == null) { - throw new ArgumentNullException(nameof(configurePolicy)); + throw new ArgumentNullException(nameof(name)); } - var policyBuilder = new RateLimitingPolicyBuilder(); - configurePolicy(policyBuilder); - var policy = policyBuilder.Build(); - builder.Add(endpointBuilder => { - endpointBuilder.Metadata.Add(new RateLimitingPolicyMetadata(policy)); + endpointBuilder.Metadata.Add(new RateLimitingPolicyMetadata(name)); }); + return builder; } } diff --git a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs index 303f1e3b78d4..ef65ac8f3eab 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs @@ -1,8 +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; +using Microsoft.AspNetCore.RateLimiting.Policies; + namespace Microsoft.AspNetCore.RateLimiting; public class RateLimitingOptions { + private string _defaultPolicyName = "__DefaultRateLimitingPolicy"; + internal IDictionary PolicyMap { get; } + = new Dictionary(StringComparer.Ordinal); + + public RateLimitingOptions AddLimiter(string name, RateLimiter limiter) + { + PolicyMap[name] = new RateLimitingPolicy(limiter); + return this; + } + + /* + public void AddLimiter(string name, RateLimiter limiter) where T : HttpContext + { + PolicyMap[name] = new RateLimitingPolicy(limiter); + } + */ + + /// + /// Gets the policy based on the + /// + /// The name of the policy to lookup. + /// The if the policy was added.null otherwise. + internal RateLimitingPolicy? GetPolicy(string name) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (PolicyMap.TryGetValue(name, out var result)) + { + return result; + } + + return null; + } } diff --git a/src/Middleware/RateLimiting/src/RateLimitingOptionsExtensions.cs b/src/Middleware/RateLimiting/src/RateLimitingOptionsExtensions.cs new file mode 100644 index 000000000000..b80b240c1cc8 --- /dev/null +++ b/src/Middleware/RateLimiting/src/RateLimitingOptionsExtensions.cs @@ -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 System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.RateLimiting; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.RateLimiting; +public static class RateLimitingOptionsExtensions +{ + public static RateLimitingOptions AddTokenBucketRateLimiter(this RateLimitingOptions options, string name, TokenBucketRateLimiterOptions tokenBucketRateLimiterOptions) + { + return options.AddLimiter(name, new TokenBucketRateLimiter(tokenBucketRateLimiterOptions)); + } + + public static RateLimitingOptions AddConcurrencyLimiter(this RateLimitingOptions options, string name, ConcurrencyLimiterOptions concurrencyLimiterOptions) + { + return options.AddLimiter(name, new ConcurrencyLimiter(concurrencyLimiterOptions)); + } +} From 682568393899467fb09bf5a20a39caaeffe6093e Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Fri, 11 Mar 2022 10:46:10 -0800 Subject: [PATCH 04/30] lil bit --- .../RateLimiting/src/Policies/RateLimitingPolicy.cs | 5 ++++- src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt | 5 ++++- .../src/RateLimitingEndpointConventionBuilderExtensions.cs | 2 -- src/Middleware/RateLimiting/src/RateLimitingOptions.cs | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs b/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs index ffc318c7c786..949c61f38b42 100644 --- a/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs +++ b/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs @@ -12,7 +12,10 @@ internal class RateLimitingPolicy public RateLimitingPolicy(RateLimiter limiter) { - //Error handling() + if (limiter == null) + { + throw new ArgumentNullException(nameof(limiter)); + } _limiter = limiter; } diff --git a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt index f741df168b83..20881be6c7c4 100644 --- a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt @@ -15,4 +15,7 @@ Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyMetadata.RateLimiti Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware.RateLimitingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options) -> void Microsoft.AspNetCore.RateLimiting.RateLimitingOptions -static Microsoft.AspNetCore.Builder.RateLimitingEndpointConventionBuilderExtensions.RequireRateLimiting(this TBuilder builder, Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! options, string! name) -> TBuilder \ No newline at end of file +Microsoft.AspNetCore.RateLimiting.RateLimitingOptionsExtensions +static Microsoft.AspNetCore.Builder.RateLimitingEndpointConventionBuilderExtensions.RequireRateLimiting(this TBuilder builder, string! name) -> TBuilder +static Microsoft.AspNetCore.RateLimiting.RateLimitingOptionsExtensions.AddConcurrencyLimiter(this Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! options, string! name, System.Threading.RateLimiting.ConcurrencyLimiterOptions! concurrencyLimiterOptions) -> Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! +static Microsoft.AspNetCore.RateLimiting.RateLimitingOptionsExtensions.AddTokenBucketRateLimiter(this Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! options, string! name, System.Threading.RateLimiting.TokenBucketRateLimiterOptions! tokenBucketRateLimiterOptions) -> Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! \ No newline at end of file diff --git a/src/Middleware/RateLimiting/src/RateLimitingEndpointConventionBuilderExtensions.cs b/src/Middleware/RateLimiting/src/RateLimitingEndpointConventionBuilderExtensions.cs index c2887eee3fac..b766b7b01a8b 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingEndpointConventionBuilderExtensions.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingEndpointConventionBuilderExtensions.cs @@ -1,8 +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 Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.RateLimiting.Policies; namespace Microsoft.AspNetCore.Builder; diff --git a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs index ef65ac8f3eab..e0772d4830b5 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs @@ -30,7 +30,7 @@ public void AddLimiter(string name, RateLimiter limiter) where T : HttpCon /// Gets the policy based on the /// /// The name of the policy to lookup. - /// The if the policy was added.null otherwise. + /// The if the policy was added.null otherwise. internal RateLimitingPolicy? GetPolicy(string name) { if (name == null) From f7e82a5a2c973726a2b9f7db6090997fa1fb20b2 Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Fri, 25 Mar 2022 10:02:43 -0700 Subject: [PATCH 05/30] Small --- .../RateLimiting/src/PublicAPI.Unshipped.txt | 7 +--- .../RateLimiting/src/RateLimitingOptions.cs | 38 ++++++++++++++++++- .../src/RateLimitingOptionsExtensions.cs | 19 +++++++--- 3 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt index 20881be6c7c4..21e6c1f84bf4 100644 --- a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt @@ -1,20 +1,17 @@ Microsoft.AspNetCore.Builder.RateLimitingEndpointConventionBuilderExtensions Microsoft.AspNetCore.RateLimiting.Policies.IRateLimitingPolicyMetadata Microsoft.AspNetCore.RateLimiting.Policies.IRateLimitingPolicyMetadata.Name.get -> string! -Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicy Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicy.Order.get -> System.Threading.RateLimiting.QueueProcessingOrder Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicy.Order.set -> void Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicy.RateLimitingPolicy() -> void -Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyBuilder -Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyBuilder.Build() -> Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicy! -Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyBuilder.UseNewestFirst() -> Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyBuilder! -Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyBuilder.UseOldestFirst() -> Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyBuilder! Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyMetadata Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyMetadata.Name.get -> string! Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyMetadata.RateLimitingPolicyMetadata(string! name) -> void Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware.RateLimitingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options) -> void Microsoft.AspNetCore.RateLimiting.RateLimitingOptions +Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.AddDefaultLimiter(System.Threading.RateLimiting.RateLimiter! limiter) -> Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! +Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.AddLimiter(string! name, System.Threading.RateLimiting.RateLimiter! limiter) -> Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! Microsoft.AspNetCore.RateLimiting.RateLimitingOptionsExtensions static Microsoft.AspNetCore.Builder.RateLimitingEndpointConventionBuilderExtensions.RequireRateLimiting(this TBuilder builder, string! name) -> TBuilder static Microsoft.AspNetCore.RateLimiting.RateLimitingOptionsExtensions.AddConcurrencyLimiter(this Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! options, string! name, System.Threading.RateLimiting.ConcurrencyLimiterOptions! concurrencyLimiterOptions) -> Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! diff --git a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs index e0772d4830b5..5bedbc7ebc63 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs @@ -9,12 +9,48 @@ namespace Microsoft.AspNetCore.RateLimiting; public class RateLimitingOptions { - private string _defaultPolicyName = "__DefaultRateLimitingPolicy"; + private string _defaultLimitingPolicyName = "__DefaultRateLimitingPolicy"; internal IDictionary PolicyMap { get; } = new Dictionary(StringComparer.Ordinal); + /// + /// Adds a new rate limiter and sets it as the default. + /// + /// The to be added. + public RateLimitingOptions AddDefaultLimiter(RateLimiter limiter) + { + // Provide a better duplicate-name error message for the default policy + if (PolicyMap.ContainsKey(_defaultLimitingPolicyName)) + { + throw new ArgumentException("Default policy is already set."); + } + AddLimiter(_defaultLimitingPolicyName, limiter); + return this; + } + + + /// + /// Adds a new rate limiter with the given name. + /// + /// The name to be associated with the given + /// The to be added. public RateLimitingOptions AddLimiter(string name, RateLimiter limiter) { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (limiter == null) + { + throw new ArgumentNullException(nameof(limiter)); + } + + if (PolicyMap.ContainsKey(name)) + { + throw new ArgumentException("There already exists a policy with the name {name}"); + } + PolicyMap[name] = new RateLimitingPolicy(limiter); return this; } diff --git a/src/Middleware/RateLimiting/src/RateLimitingOptionsExtensions.cs b/src/Middleware/RateLimiting/src/RateLimitingOptionsExtensions.cs index b80b240c1cc8..4fecdafb2656 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingOptionsExtensions.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingOptionsExtensions.cs @@ -1,21 +1,30 @@ // 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; public static class RateLimitingOptionsExtensions { + /// + /// Adds a new with the given to the . + /// + /// The to add a limiter to. + /// The name that will be associated with the limiter. + /// The to be used for the limiter. + /// This . public static RateLimitingOptions AddTokenBucketRateLimiter(this RateLimitingOptions options, string name, TokenBucketRateLimiterOptions tokenBucketRateLimiterOptions) { return options.AddLimiter(name, new TokenBucketRateLimiter(tokenBucketRateLimiterOptions)); } + /// + /// Adds a new with the given to the . + /// + /// The to add a limiter to. + /// The name that will be associated with the limiter. + /// The to be used for the limiter. + /// This . public static RateLimitingOptions AddConcurrencyLimiter(this RateLimitingOptions options, string name, ConcurrencyLimiterOptions concurrencyLimiterOptions) { return options.AddLimiter(name, new ConcurrencyLimiter(concurrencyLimiterOptions)); From c5f2d94167b34d3efa3ffb8713815596828f80e2 Mon Sep 17 00:00:00 2001 From: William Godbe Date: Mon, 14 Mar 2022 15:02:05 -0700 Subject: [PATCH 06/30] Check-in launchSettings.json files from Middleware (#40695) --- .../Properties/launchSettings.json | 12 ++++++++++++ .../Properties/launchSettings.json | 12 ++++++++++++ .../Properties/launchSettings.json | 12 ++++++++++++ .../Properties/launchSettings.json | 12 ++++++++++++ .../Properties/launchSettings.json | 12 ++++++++++++ .../WelcomePageSample/Properties/launchSettings.json | 12 ++++++++++++ .../Properties/launchSettings.json | 12 ++++++++++++ 7 files changed, 84 insertions(+) create mode 100644 src/Middleware/CORS/test/testassets/CorsMiddlewareWebSite/Properties/launchSettings.json create mode 100644 src/Middleware/Diagnostics/test/testassets/DatabaseErrorPageSample/Properties/launchSettings.json create mode 100644 src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Properties/launchSettings.json create mode 100644 src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Properties/launchSettings.json create mode 100644 src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Properties/launchSettings.json create mode 100644 src/Middleware/Diagnostics/test/testassets/WelcomePageSample/Properties/launchSettings.json create mode 100644 src/Middleware/Localization/testassets/LocalizationWebsite/Properties/launchSettings.json diff --git a/src/Middleware/CORS/test/testassets/CorsMiddlewareWebSite/Properties/launchSettings.json b/src/Middleware/CORS/test/testassets/CorsMiddlewareWebSite/Properties/launchSettings.json new file mode 100644 index 000000000000..485cac49a974 --- /dev/null +++ b/src/Middleware/CORS/test/testassets/CorsMiddlewareWebSite/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "CorsMiddlewareWebSite": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:61226;http://localhost:61227" + } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics/test/testassets/DatabaseErrorPageSample/Properties/launchSettings.json b/src/Middleware/Diagnostics/test/testassets/DatabaseErrorPageSample/Properties/launchSettings.json new file mode 100644 index 000000000000..3fbc5274ba38 --- /dev/null +++ b/src/Middleware/Diagnostics/test/testassets/DatabaseErrorPageSample/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "DatabaseErrorPageSample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:61218;http://localhost:61219" + } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Properties/launchSettings.json b/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Properties/launchSettings.json new file mode 100644 index 000000000000..8f4f5d821bdc --- /dev/null +++ b/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "DeveloperExceptionPageSample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:61228;http://localhost:61229" + } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Properties/launchSettings.json b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Properties/launchSettings.json new file mode 100644 index 000000000000..c69c9dd556ac --- /dev/null +++ b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "ExceptionHandlerSample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:61220;http://localhost:61221" + } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Properties/launchSettings.json b/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Properties/launchSettings.json new file mode 100644 index 000000000000..f27a4afd7135 --- /dev/null +++ b/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "StatusCodePagesSample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:61230;http://localhost:61231" + } + } +} \ No newline at end of file diff --git a/src/Middleware/Diagnostics/test/testassets/WelcomePageSample/Properties/launchSettings.json b/src/Middleware/Diagnostics/test/testassets/WelcomePageSample/Properties/launchSettings.json new file mode 100644 index 000000000000..b82719da65c1 --- /dev/null +++ b/src/Middleware/Diagnostics/test/testassets/WelcomePageSample/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "WelcomePageSample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:61223;http://localhost:61225" + } + } +} \ No newline at end of file diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/Properties/launchSettings.json b/src/Middleware/Localization/testassets/LocalizationWebsite/Properties/launchSettings.json new file mode 100644 index 000000000000..3b71d90077f8 --- /dev/null +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "LocalizationWebsite": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:61222;http://localhost:61224" + } + } +} \ No newline at end of file From 2c683fc78ef216d000481e39591bed044ed88637 Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Wed, 30 Mar 2022 09:48:37 -0700 Subject: [PATCH 07/30] sln --- AspNetCore.sln | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/AspNetCore.sln b/AspNetCore.sln index bd9f06617e80..0261994c81b8 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1696,6 +1696,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildAfterTargetingPack", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuildAfterTargetingPack", "src\BuildAfterTargetingPack\BuildAfterTargetingPack.csproj", "{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RateLimiting", "src\Middleware\RateLimiting\src\Microsoft.AspNetCore.RateLimiting.csproj", "{00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RateLimiting.Tests", "src\Middleware\RateLimiting\test\Microsoft.AspNetCore.RateLimiting.Tests.csproj", "{5125141D-2B8B-4A13-95D3-1CDAA1EF276A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -10151,6 +10155,38 @@ Global {8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|x64.Build.0 = Release|Any CPU {8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|x86.ActiveCfg = Release|Any CPU {8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|x86.Build.0 = Release|Any CPU + {00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Debug|arm64.ActiveCfg = Debug|Any CPU + {00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Debug|arm64.Build.0 = Debug|Any CPU + {00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Debug|x64.ActiveCfg = Debug|Any CPU + {00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Debug|x64.Build.0 = Debug|Any CPU + {00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Debug|x86.ActiveCfg = Debug|Any CPU + {00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Debug|x86.Build.0 = Debug|Any CPU + {00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Release|Any CPU.Build.0 = Release|Any CPU + {00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Release|arm64.ActiveCfg = Release|Any CPU + {00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Release|arm64.Build.0 = Release|Any CPU + {00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Release|x64.ActiveCfg = Release|Any CPU + {00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Release|x64.Build.0 = Release|Any CPU + {00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Release|x86.ActiveCfg = Release|Any CPU + {00C3F1D7-0B5E-4FFE-949C-AA11C1E4BBE7}.Release|x86.Build.0 = Release|Any CPU + {5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Debug|arm64.ActiveCfg = Debug|Any CPU + {5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Debug|arm64.Build.0 = Debug|Any CPU + {5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Debug|x64.ActiveCfg = Debug|Any CPU + {5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Debug|x64.Build.0 = Debug|Any CPU + {5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Debug|x86.ActiveCfg = Debug|Any CPU + {5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Debug|x86.Build.0 = Debug|Any CPU + {5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Release|Any CPU.Build.0 = Release|Any CPU + {5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Release|arm64.ActiveCfg = Release|Any CPU + {5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Release|arm64.Build.0 = Release|Any CPU + {5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Release|x64.ActiveCfg = Release|Any CPU + {5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Release|x64.Build.0 = Release|Any CPU + {5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Release|x86.ActiveCfg = Release|Any CPU + {5125141D-2B8B-4A13-95D3-1CDAA1EF276A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -10995,4 +11031,4 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} EndGlobalSection -EndGlobal \ No newline at end of file +EndGlobal From 301a4eef1573ab9d1582098b7e9677e3cf8cbf6c Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Thu, 31 Mar 2022 09:08:03 -0700 Subject: [PATCH 08/30] More --- eng/ProjectReferences.props | 1 + eng/SharedFramework.Local.props | 1 + .../src/Policies/RateLimitingPolicy.cs | 1 - .../RateLimiting/src/PublicAPI.Unshipped.txt | 13 ++- .../src/RateLimitingMiddleware.cs | 105 +++++++++++++++++- .../RateLimiting/src/RateLimitingOptions.cs | 37 ++++-- ...osoft.AspNetCore.RateLimiting.Tests.csproj | 10 +- .../test/RateLimitingOptionsTests.cs | 16 +++ 8 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index 94a43b1c5331..963800c8f9da 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -89,6 +89,7 @@ + diff --git a/eng/SharedFramework.Local.props b/eng/SharedFramework.Local.props index f32bf007b8a4..7e678d7c0fb7 100644 --- a/eng/SharedFramework.Local.props +++ b/eng/SharedFramework.Local.props @@ -77,6 +77,7 @@ + diff --git a/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs b/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs index 949c61f38b42..ac99782dc49d 100644 --- a/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs +++ b/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Threading.RateLimiting; -using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.RateLimiting.Policies; internal class RateLimitingPolicy diff --git a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt index 21e6c1f84bf4..4ee680551dc6 100644 --- a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt @@ -1,18 +1,21 @@ -Microsoft.AspNetCore.Builder.RateLimitingEndpointConventionBuilderExtensions +Microsoft.AspNetCore.Builder.RateLimitingEndpointConventionBuilderExtensions Microsoft.AspNetCore.RateLimiting.Policies.IRateLimitingPolicyMetadata Microsoft.AspNetCore.RateLimiting.Policies.IRateLimitingPolicyMetadata.Name.get -> string! -Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicy.Order.get -> System.Threading.RateLimiting.QueueProcessingOrder -Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicy.Order.set -> void -Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicy.RateLimitingPolicy() -> void Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyMetadata Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyMetadata.Name.get -> string! Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyMetadata.RateLimitingPolicyMetadata(string! name) -> void Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware +Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware.RateLimitingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options) -> void Microsoft.AspNetCore.RateLimiting.RateLimitingOptions Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.AddDefaultLimiter(System.Threading.RateLimiting.RateLimiter! limiter) -> Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.AddLimiter(string! name, System.Threading.RateLimiting.RateLimiter! limiter) -> Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! +Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.AddLimiter(System.Threading.RateLimiting.PartitionedRateLimiter! limiter) -> void +Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.get -> System.Threading.RateLimiting.PartitionedRateLimiter? +Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.get -> Microsoft.AspNetCore.Http.RequestDelegate! +Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.set -> void +Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.RateLimitingOptions() -> void Microsoft.AspNetCore.RateLimiting.RateLimitingOptionsExtensions static Microsoft.AspNetCore.Builder.RateLimitingEndpointConventionBuilderExtensions.RequireRateLimiting(this TBuilder builder, string! name) -> TBuilder static Microsoft.AspNetCore.RateLimiting.RateLimitingOptionsExtensions.AddConcurrencyLimiter(this Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! options, string! name, System.Threading.RateLimiting.ConcurrencyLimiterOptions! concurrencyLimiterOptions) -> Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! -static Microsoft.AspNetCore.RateLimiting.RateLimitingOptionsExtensions.AddTokenBucketRateLimiter(this Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! options, string! name, System.Threading.RateLimiting.TokenBucketRateLimiterOptions! tokenBucketRateLimiterOptions) -> Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! \ No newline at end of file +static Microsoft.AspNetCore.RateLimiting.RateLimitingOptionsExtensions.AddTokenBucketRateLimiter(this Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! options, string! name, System.Threading.RateLimiting.TokenBucketRateLimiterOptions! tokenBucketRateLimiterOptions) -> Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! diff --git a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs index b0d62ee126c8..5ace71ad1e9e 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs @@ -1,24 +1,127 @@ // 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; -public class RateLimitingMiddleware +public partial class RateLimitingMiddleware { private readonly RequestDelegate _next; + private readonly RequestDelegate _onRejected; private readonly ILogger _logger; + private readonly PartitionedRateLimiter _limiter; + private RateLimitLease? _lease; public RateLimitingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IOptions options) { + if (options.Value.Limiter == null) + { + throw new ArgumentException("The value of 'options.Limiter' must not be null.", nameof(options)); + } + + if (options.Value.OnRejected == null) + { + throw new ArgumentException("The value of 'options.OnRejected' must not be null.", nameof(options)); + } + _next = next; _logger = loggerFactory.CreateLogger(); + _limiter = options.Value.Limiter; + _onRejected = options.Value.OnRejected; } + // TODO - EventSource? public async Task Invoke(HttpContext context) { + var acquireLeaseTask = TryAcquireAsync(context); + + // Make sure we only ever call GetResult once on the TryEnterAsync ValueTask b/c it resets. + bool result; + + if (acquireLeaseTask.IsCompleted) + { + result = acquireLeaseTask.Result; + } + else + { + result = await acquireLeaseTask; + } + + if (result) + { + try + { + await _next(context); + } + finally + { + OnCompletion(); + } + } + else + { + RateLimiterLog.RequestRejectedLimitsExceeded(_logger); + context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; + await _onRejected(context); + } + } + + private ValueTask TryAcquireAsync(HttpContext context) + { + // a return value of 'false' indicates that the request is rejected + // a return value of 'true' indicates that the request may proceed + + var lease = _limiter.Acquire(context); + if (lease.IsAcquired) + { + _lease = lease; + return ValueTask.FromResult(true); + } + + var task = _limiter.WaitAsync(context); + if (task.IsCompletedSuccessfully) + { + lease = task.Result; + if (lease.IsAcquired) + { + _lease = lease; + return ValueTask.FromResult(true); + } + + return ValueTask.FromResult(false); + } + + return Awaited(task); + } + + private void OnCompletion() + { + if (_lease != null) + { + _lease.Dispose(); + } + } + + private async ValueTask Awaited(ValueTask task) + { + var lease = await task; + + if (lease.IsAcquired) + { + _lease = lease; + return true; + } + + return false; + } + + private static partial class RateLimiterLog + { + [LoggerMessage(1, LogLevel.Debug, "Rate limits exceeded, rejecting this request with a '503 server not available' error", EventName = "RequestRejectedLimitsExceeded")] + internal static partial void RequestRejectedLimitsExceeded(ILogger logger); } } diff --git a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs index 5bedbc7ebc63..cfe9737023cb 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs @@ -9,9 +9,18 @@ namespace Microsoft.AspNetCore.RateLimiting; public class RateLimitingOptions { - private string _defaultLimitingPolicyName = "__DefaultRateLimitingPolicy"; - internal IDictionary PolicyMap { get; } + private readonly string _defaultLimitingPolicyName = "__DefaultRateLimitingPolicy"; + private IDictionary PolicyMap { get; } = new Dictionary(StringComparer.Ordinal); + private PartitionedRateLimiter? _limiter; + + /// + /// Gets the + /// + public PartitionedRateLimiter? Limiter + { + get => _limiter; + } /// /// Adds a new rate limiter and sets it as the default. @@ -28,7 +37,6 @@ public RateLimitingOptions AddDefaultLimiter(RateLimiter limiter) return this; } - /// /// Adds a new rate limiter with the given name. /// @@ -55,12 +63,27 @@ public RateLimitingOptions AddLimiter(string name, RateLimiter limiter) return this; } - /* - public void AddLimiter(string name, RateLimiter limiter) where T : HttpContext + /// + /// Adds a new rate limiter. + /// + /// The to be added. + public void AddLimiter(PartitionedRateLimiter limiter) { - PolicyMap[name] = new RateLimitingPolicy(limiter); + if (limiter == null) + { + throw new ArgumentNullException(nameof(limiter)); + } + _limiter = limiter; } - */ + + /// + /// A that handles requests rejected by this middleware. + /// If it doesn't modify the response, an empty 503 response will be written. + /// + public RequestDelegate OnRejected { get; set; } = context => + { + return Task.CompletedTask; + }; /// /// Gets the policy based on the diff --git a/src/Middleware/RateLimiting/test/Microsoft.AspNetCore.RateLimiting.Tests.csproj b/src/Middleware/RateLimiting/test/Microsoft.AspNetCore.RateLimiting.Tests.csproj index 5bb19e39bebf..9712d186644a 100644 --- a/src/Middleware/RateLimiting/test/Microsoft.AspNetCore.RateLimiting.Tests.csproj +++ b/src/Middleware/RateLimiting/test/Microsoft.AspNetCore.RateLimiting.Tests.csproj @@ -1,17 +1,11 @@ - + $(DefaultNetCoreTargetFramework) - - - - - - - + diff --git a/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs b/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs new file mode 100644 index 000000000000..ac1a7dac8ef7 --- /dev/null +++ b/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs @@ -0,0 +1,16 @@ +// 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 RateLimitingOptions(); + var ex = Assert.Throws(() => options.AddLimiter(null)); + } +} From 8b1e074a5413c362cc8cfd6b47853d5266ecfa11 Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Thu, 31 Mar 2022 13:39:06 -0700 Subject: [PATCH 09/30] More --- .../src/RateLimitingMiddleware.cs | 8 +- .../RateLimiting/src/RateLimitingOptions.cs | 1 + .../test/RateLimitingMiddlewareTests.cs | 73 ++++++++++++++++ .../test/TestPartitionedRateLimiter.cs | 86 +++++++++++++++++++ .../RateLimiting/test/TestRateLimitLease.cs | 40 +++++++++ 5 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs create mode 100644 src/Middleware/RateLimiting/test/TestPartitionedRateLimiter.cs create mode 100644 src/Middleware/RateLimiting/test/TestRateLimitLease.cs diff --git a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs index 5ace71ad1e9e..99219712dd6f 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs @@ -18,6 +18,8 @@ public partial class RateLimitingMiddleware public RateLimitingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IOptions options) { + _next = next ?? throw new ArgumentNullException(nameof(next)); + if (options.Value.Limiter == null) { throw new ArgumentException("The value of 'options.Limiter' must not be null.", nameof(options)); @@ -28,7 +30,11 @@ public RateLimitingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory throw new ArgumentException("The value of 'options.OnRejected' must not be null.", nameof(options)); } - _next = next; + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + _logger = loggerFactory.CreateLogger(); _limiter = options.Value.Limiter; _onRejected = options.Value.OnRejected; diff --git a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs index cfe9737023cb..13710505aa6b 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs @@ -12,6 +12,7 @@ public class RateLimitingOptions private readonly string _defaultLimitingPolicyName = "__DefaultRateLimitingPolicy"; private IDictionary PolicyMap { get; } = new Dictionary(StringComparer.Ordinal); + // TODO - Provide a default? private PartitionedRateLimiter? _limiter; /// diff --git a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs new file mode 100644 index 000000000000..9e37c49c616d --- /dev/null +++ b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs @@ -0,0 +1,73 @@ +// 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.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using Moq; + +namespace Microsoft.AspNetCore.RateLimiting; +public class RateLimitingMiddlewareTests : LoggedTest +{ + [Fact] + public void Ctor_ThrowsExceptionsWhenNullArgs() + { + var options = CreateOptionsAccessor(); + options.Value.AddLimiter(new TestPartitionedRateLimiter()); + + Assert.Throws(() => new RateLimitingMiddleware( + null, + new NullLoggerFactory(), + options)); + + Assert.Throws(() => new RateLimitingMiddleware(c => + { + return Task.CompletedTask; + }, + null, + options)); + } + + [Fact] + public void Ctor_ThrowsExceptionsWhenNullLimiter() + { + // Default Options instance has no limiter set + var ex = Assert.Throws(() => new RateLimitingMiddleware(c => + { + return Task.CompletedTask; + }, + new NullLoggerFactory(), + CreateOptionsAccessor())); + Assert.Contains("The value of 'options.Limiter' must not be null.", ex.Message); + } + + [Fact] + public void Ctor_ThrowsExceptionsWhenNullOnRejected() + { + var options = CreateOptionsAccessor(); + options.Value.OnRejected = null; + options.Value.AddLimiter(new TestPartitionedRateLimiter()); + var ex = Assert.Throws(() => new RateLimitingMiddleware(c => + { + return Task.CompletedTask; + }, + new NullLoggerFactory(), + options)); + Assert.Contains("The value of 'options.OnRejected' must not be null.", ex.Message); + } + + private IOptions CreateOptionsAccessor() + { + var options = new RateLimitingOptions(); + var optionsAccessor = Mock.Of>(o => o.Value == options); + return optionsAccessor; + } +} diff --git a/src/Middleware/RateLimiting/test/TestPartitionedRateLimiter.cs b/src/Middleware/RateLimiting/test/TestPartitionedRateLimiter.cs new file mode 100644 index 000000000000..b36e5c7a3cbd --- /dev/null +++ b/src/Middleware/RateLimiting/test/TestPartitionedRateLimiter.cs @@ -0,0 +1,86 @@ +// 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 : PartitionedRateLimiter +{ + + private List limiters = new List(); + + 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(); + 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 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(); + 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); + + } +} diff --git a/src/Middleware/RateLimiting/test/TestRateLimitLease.cs b/src/Middleware/RateLimiting/test/TestRateLimitLease.cs new file mode 100644 index 000000000000..7e3962b69b6a --- /dev/null +++ b/src/Middleware/RateLimiting/test/TestRateLimitLease.cs @@ -0,0 +1,40 @@ +// 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 TestRateLimitLease : RateLimitLease +{ + internal List _leases; + + public TestRateLimitLease(bool isAcquired, List leases) + { + IsAcquired = isAcquired; + _leases = leases; + } + + public override bool IsAcquired { get; } + + public override IEnumerable MetadataNames => throw new NotImplementedException(); + + public override bool TryGetMetadata(string metadataName, out object metadata) + { + throw new NotImplementedException(); + } + protected override void Dispose(bool disposing) + { + if (_leases != null) + { + foreach (var lease in _leases) + { + lease.Dispose(); + } + } + } +} From 7870d620ccd91dd86956cb5b0aea814685790c60 Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Fri, 1 Apr 2022 10:12:14 -0700 Subject: [PATCH 10/30] More --- .../test/RateLimitingMiddlewareTests.cs | 44 +++++++++++++++++++ .../RateLimiting/test/TestRateLimiter.cs | 34 ++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/Middleware/RateLimiting/test/TestRateLimiter.cs diff --git a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs index 9e37c49c616d..0f5a027dad6a 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs @@ -64,10 +64,54 @@ public void Ctor_ThrowsExceptionsWhenNullOnRejected() Assert.Contains("The value of 'options.OnRejected' must not be null.", ex.Message); } + [Fact] + public async Task RequestsCallNextIfAccepted() + { + var flag = false; + var options = CreateOptionsAccessor(); + options.Value.AddLimiter(new TestPartitionedRateLimiter(new TestRateLimiter(true))); + var middleware = new RateLimitingMiddleware(c => + { + flag = true; + return Task.CompletedTask; + }, + new NullLoggerFactory(), + options); + + await middleware.Invoke(new DefaultHttpContext()); + Assert.True(flag); + } + + [Fact] + public async Task RequestRejected_CallsOnRejectedAndGives503() + { + bool onRejectedInvoked = false; + var options = CreateOptionsAccessor(); + options.Value.AddLimiter(new TestPartitionedRateLimiter(new TestRateLimiter(false))); + options.Value.OnRejected = httpContext => + { + onRejectedInvoked = true; + return Task.CompletedTask; + }; + + var middleware = new RateLimitingMiddleware(c => + { + return Task.CompletedTask; + }, + new NullLoggerFactory(), + options); + + var context = new DefaultHttpContext(); + await middleware.Invoke(context).DefaultTimeout(); + Assert.True(onRejectedInvoked); + Assert.Equal(StatusCodes.Status503ServiceUnavailable, context.Response.StatusCode); + } + private IOptions CreateOptionsAccessor() { var options = new RateLimitingOptions(); var optionsAccessor = Mock.Of>(o => o.Value == options); return optionsAccessor; } + } diff --git a/src/Middleware/RateLimiting/test/TestRateLimiter.cs b/src/Middleware/RateLimiting/test/TestRateLimiter.cs new file mode 100644 index 000000000000..44ff22c5e6b4 --- /dev/null +++ b/src/Middleware/RateLimiting/test/TestRateLimiter.cs @@ -0,0 +1,34 @@ +// 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 TestRateLimiter : RateLimiter +{ + private readonly bool _alwaysAccept; + + public TestRateLimiter(bool alwaysAccept) + { + _alwaysAccept = alwaysAccept; + } + public override int GetAvailablePermits() + { + throw new NotImplementedException(); + } + + protected override RateLimitLease AcquireCore(int permitCount) + { + return new TestRateLimitLease(_alwaysAccept, null); + } + + protected override ValueTask WaitAsyncCore(int permitCount, CancellationToken cancellationToken) + { + return new ValueTask(new TestRateLimitLease(_alwaysAccept, null)); + } +} From 16bdcf30653cd0a7689be8043c3eb4e1ee497a8b Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Fri, 1 Apr 2022 10:21:40 -0700 Subject: [PATCH 11/30] Remove stuff --- .../Policies/IRateLimitingPolicyMetadata.cs | 12 ---- .../src/Policies/RateLimitingPolicy.cs | 28 -------- .../Policies/RateLimitingPolicyMetadata.cs | 25 ------- .../RateLimiting/src/PublicAPI.Unshipped.txt | 14 +--- ...tingEndpointConventionBuilderExtensions.cs | 34 --------- .../src/RateLimitingMiddleware.cs | 15 ++++ .../RateLimiting/src/RateLimitingOptions.cs | 71 ++----------------- .../src/RateLimitingOptionsExtensions.cs | 32 --------- 8 files changed, 21 insertions(+), 210 deletions(-) delete mode 100644 src/Middleware/RateLimiting/src/Policies/IRateLimitingPolicyMetadata.cs delete mode 100644 src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs delete mode 100644 src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyMetadata.cs delete mode 100644 src/Middleware/RateLimiting/src/RateLimitingEndpointConventionBuilderExtensions.cs delete mode 100644 src/Middleware/RateLimiting/src/RateLimitingOptionsExtensions.cs diff --git a/src/Middleware/RateLimiting/src/Policies/IRateLimitingPolicyMetadata.cs b/src/Middleware/RateLimiting/src/Policies/IRateLimitingPolicyMetadata.cs deleted file mode 100644 index ad2686e0743f..000000000000 --- a/src/Middleware/RateLimiting/src/Policies/IRateLimitingPolicyMetadata.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.RateLimiting.Policies; - -public interface IRateLimitingPolicyMetadata -{ - /// - /// The name of the policy which needs to be applied. - /// - string Name { get; } -} diff --git a/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs b/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs deleted file mode 100644 index ac99782dc49d..000000000000 --- a/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicy.cs +++ /dev/null @@ -1,28 +0,0 @@ -// 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.Policies; -internal class RateLimitingPolicy -{ - private readonly RateLimiter _limiter; - //private readonly RateLimiter _limiterOfT; - - public RateLimitingPolicy(RateLimiter limiter) - { - if (limiter == null) - { - throw new ArgumentNullException(nameof(limiter)); - } - _limiter = limiter; - } - - /* - public RateLimitingPolicy(RateLimiter limiter) - { - //Error handling() - _limiter = limiter; - } - */ -} diff --git a/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyMetadata.cs b/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyMetadata.cs deleted file mode 100644 index a64f1d6c65da..000000000000 --- a/src/Middleware/RateLimiting/src/Policies/RateLimitingPolicyMetadata.cs +++ /dev/null @@ -1,25 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.RateLimiting.Policies; - -/// -/// Metadata that provides a Rate Limiting policy. -/// -public class RateLimitingPolicyMetadata : IRateLimitingPolicyMetadata -{ - - /// - /// Creates a new instance of using the specified policy. - /// - /// The name of the policy which needs to be applied. - public RateLimitingPolicyMetadata(string name) - { - Name = name; - } - - /// - /// The name of the policy which needs to be applied. - /// - public string Name { get; } -} diff --git a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt index 4ee680551dc6..ebd88490645b 100644 --- a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt @@ -1,21 +1,9 @@ -Microsoft.AspNetCore.Builder.RateLimitingEndpointConventionBuilderExtensions -Microsoft.AspNetCore.RateLimiting.Policies.IRateLimitingPolicyMetadata -Microsoft.AspNetCore.RateLimiting.Policies.IRateLimitingPolicyMetadata.Name.get -> string! -Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyMetadata -Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyMetadata.Name.get -> string! -Microsoft.AspNetCore.RateLimiting.Policies.RateLimitingPolicyMetadata.RateLimitingPolicyMetadata(string! name) -> void Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware.RateLimitingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options) -> void Microsoft.AspNetCore.RateLimiting.RateLimitingOptions -Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.AddDefaultLimiter(System.Threading.RateLimiting.RateLimiter! limiter) -> Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! -Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.AddLimiter(string! name, System.Threading.RateLimiting.RateLimiter! limiter) -> Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! -Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.AddLimiter(System.Threading.RateLimiting.PartitionedRateLimiter! limiter) -> void +Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.AddLimiter(System.Threading.RateLimiting.PartitionedRateLimiter! limiter) -> Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.get -> System.Threading.RateLimiting.PartitionedRateLimiter? Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.get -> Microsoft.AspNetCore.Http.RequestDelegate! Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.set -> void Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.RateLimitingOptions() -> void -Microsoft.AspNetCore.RateLimiting.RateLimitingOptionsExtensions -static Microsoft.AspNetCore.Builder.RateLimitingEndpointConventionBuilderExtensions.RequireRateLimiting(this TBuilder builder, string! name) -> TBuilder -static Microsoft.AspNetCore.RateLimiting.RateLimitingOptionsExtensions.AddConcurrencyLimiter(this Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! options, string! name, System.Threading.RateLimiting.ConcurrencyLimiterOptions! concurrencyLimiterOptions) -> Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! -static Microsoft.AspNetCore.RateLimiting.RateLimitingOptionsExtensions.AddTokenBucketRateLimiter(this Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! options, string! name, System.Threading.RateLimiting.TokenBucketRateLimiterOptions! tokenBucketRateLimiterOptions) -> Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! diff --git a/src/Middleware/RateLimiting/src/RateLimitingEndpointConventionBuilderExtensions.cs b/src/Middleware/RateLimiting/src/RateLimitingEndpointConventionBuilderExtensions.cs deleted file mode 100644 index b766b7b01a8b..000000000000 --- a/src/Middleware/RateLimiting/src/RateLimitingEndpointConventionBuilderExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -// 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.RateLimiting.Policies; - -namespace Microsoft.AspNetCore.Builder; -public static class RateLimitingEndpointConventionBuilderExtensions -{ - /// - /// Adds the specified Rate Limiting policy to the endpoint(s). - /// - /// The endpoint convention builder. - /// The name of the RateLimiter to add to the endpoint. - /// The original convention builder parameter. - public static TBuilder RequireRateLimiting(this TBuilder builder, String name) where TBuilder : IEndpointConventionBuilder - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - builder.Add(endpointBuilder => - { - endpointBuilder.Metadata.Add(new RateLimitingPolicyMetadata(name)); - }); - - return builder; - } -} diff --git a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs index 99219712dd6f..0ef716d1677e 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs @@ -7,6 +7,10 @@ using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.RateLimiting; + +/// +/// Limits the rate of requests allowed in the application, based on limits set by a user-provided . +/// public partial class RateLimitingMiddleware { @@ -16,6 +20,12 @@ public partial class RateLimitingMiddleware private readonly PartitionedRateLimiter _limiter; private RateLimitLease? _lease; + /// + /// Creates a new . + /// + /// The representing the next middleware in the pipeline. + /// The used for logging. + /// The options for the middleware. public RateLimitingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IOptions options) { _next = next ?? throw new ArgumentNullException(nameof(next)); @@ -41,6 +51,11 @@ public RateLimitingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory } // TODO - EventSource? + /// + /// Invokes the logic of the middleware. + /// + /// The . + /// A that completes when the request leaves. public async Task Invoke(HttpContext context) { var acquireLeaseTask = TryAcquireAsync(context); diff --git a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs index 13710505aa6b..c0793ad6eb3b 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs @@ -3,15 +3,14 @@ using System.Threading.RateLimiting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.RateLimiting.Policies; namespace Microsoft.AspNetCore.RateLimiting; +/// +/// Specifies options for the . +/// public class RateLimitingOptions { - private readonly string _defaultLimitingPolicyName = "__DefaultRateLimitingPolicy"; - private IDictionary PolicyMap { get; } - = new Dictionary(StringComparer.Ordinal); // TODO - Provide a default? private PartitionedRateLimiter? _limiter; @@ -23,58 +22,18 @@ public PartitionedRateLimiter? Limiter get => _limiter; } - /// - /// Adds a new rate limiter and sets it as the default. - /// - /// The to be added. - public RateLimitingOptions AddDefaultLimiter(RateLimiter limiter) - { - // Provide a better duplicate-name error message for the default policy - if (PolicyMap.ContainsKey(_defaultLimitingPolicyName)) - { - throw new ArgumentException("Default policy is already set."); - } - AddLimiter(_defaultLimitingPolicyName, limiter); - return this; - } - - /// - /// Adds a new rate limiter with the given name. - /// - /// The name to be associated with the given - /// The to be added. - public RateLimitingOptions AddLimiter(string name, RateLimiter limiter) - { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - if (limiter == null) - { - throw new ArgumentNullException(nameof(limiter)); - } - - if (PolicyMap.ContainsKey(name)) - { - throw new ArgumentException("There already exists a policy with the name {name}"); - } - - PolicyMap[name] = new RateLimitingPolicy(limiter); - return this; - } - /// /// Adds a new rate limiter. /// /// The to be added. - public void AddLimiter(PartitionedRateLimiter limiter) + public RateLimitingOptions AddLimiter(PartitionedRateLimiter limiter) { if (limiter == null) { throw new ArgumentNullException(nameof(limiter)); } _limiter = limiter; + return this; } /// @@ -85,24 +44,4 @@ public void AddLimiter(PartitionedRateLimiter lim { return Task.CompletedTask; }; - - /// - /// Gets the policy based on the - /// - /// The name of the policy to lookup. - /// The if the policy was added.null otherwise. - internal RateLimitingPolicy? GetPolicy(string name) - { - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - if (PolicyMap.TryGetValue(name, out var result)) - { - return result; - } - - return null; - } } diff --git a/src/Middleware/RateLimiting/src/RateLimitingOptionsExtensions.cs b/src/Middleware/RateLimiting/src/RateLimitingOptionsExtensions.cs deleted file mode 100644 index 4fecdafb2656..000000000000 --- a/src/Middleware/RateLimiting/src/RateLimitingOptionsExtensions.cs +++ /dev/null @@ -1,32 +0,0 @@ -// 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; -public static class RateLimitingOptionsExtensions -{ - /// - /// Adds a new with the given to the . - /// - /// The to add a limiter to. - /// The name that will be associated with the limiter. - /// The to be used for the limiter. - /// This . - public static RateLimitingOptions AddTokenBucketRateLimiter(this RateLimitingOptions options, string name, TokenBucketRateLimiterOptions tokenBucketRateLimiterOptions) - { - return options.AddLimiter(name, new TokenBucketRateLimiter(tokenBucketRateLimiterOptions)); - } - - /// - /// Adds a new with the given to the . - /// - /// The to add a limiter to. - /// The name that will be associated with the limiter. - /// The to be used for the limiter. - /// This . - public static RateLimitingOptions AddConcurrencyLimiter(this RateLimitingOptions options, string name, ConcurrencyLimiterOptions concurrencyLimiterOptions) - { - return options.AddLimiter(name, new ConcurrencyLimiter(concurrencyLimiterOptions)); - } -} From 4f200924333941b02eb04d44a62cec3b34471b80 Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Fri, 1 Apr 2022 10:24:26 -0700 Subject: [PATCH 12/30] rm --- Rate_limiting_middleware_Qs.txt | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 Rate_limiting_middleware_Qs.txt diff --git a/Rate_limiting_middleware_Qs.txt b/Rate_limiting_middleware_Qs.txt deleted file mode 100644 index 3b160b70e36c..000000000000 --- a/Rate_limiting_middleware_Qs.txt +++ /dev/null @@ -1,30 +0,0 @@ -Rate limiting middleware Q's - -- How exactly does user set which RateLimiter we're using? - Pass instance? - .WithConcurrencyLimiter()/.WithTokenBucketLimiter()? -- Do we provide defaults for existing RateLimiters (TokenBucket, Concurrency) -- HttpContext has endpoint - how does mapping of name:policy work? - https://github.com/dotnet/aspnetcore/blob/f23cfb88eb0af4f9dce3341c37fc7399e80f8a11/src/Middleware/CORS/src/Infrastructure/CorsMiddleware.cs#L117-L123 - Set metadata on endpoint, check against that for resolution -- How does user add policy to endpoint? - https://github.com/dotnet/aspnetcore/blob/main/src/Middleware/CORS/src/Infrastructure/CorsEndpointConventionBuilderExtensions.cs - - What do defaults look like for this? - -What's going on here? https://github.com/dotnet/aspnetcore/blob/cceceeb21a06573425009c3d547bd9e297bf0f06/src/Middleware/CORS/src/Infrastructure/CorsMiddlewareExtensions.cs#L34-L66 - -Current idea - looks a lot like CORS. User can either set a global policy (DefaultPolicy), or per-endpoint policy. Policies live in Options. When there's a per-endpoint policy, it gets added as metadata to the endpoint. In Middleware, we check metadata on endpoint for policy - if there, use that. Else use default. - If a user wants ratelimiting only on certain endpoints, do we have no default, or have a NoPolicy? - -https://github.com/dotnet/aspnetcore/blob/5141acd2ef5ce41528178f7b1f89ffeaa9242e86/src/Middleware/CORS/src/Infrastructure/CorsOptions.cs#L13-L16 - why Task and not just Policy? - -For multiple limiters on an endpoint/pipeline - must be able to use them in the order the user specifies - -GenericRateLimiter is not a RateLimiter - Policy should keep both, do either/or-ing based on which one it has - The TResource in GenericRateLimiter is HttpContext today - will get funky when it's not just HttpContext later - -What to do when rejecting? ConcurrencyLimiterMiddleware gives 503 - -What to do about aggregate limiters? Runtime or Middleware?? - -What if user wants to apply endpoint policy _and_ default policy? DisableDefault? From a41776614a31232b0dc9564eac6acd6794147ad0 Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Fri, 1 Apr 2022 10:26:17 -0700 Subject: [PATCH 13/30] Not SharedFx --- eng/SharedFramework.Local.props | 1 - 1 file changed, 1 deletion(-) diff --git a/eng/SharedFramework.Local.props b/eng/SharedFramework.Local.props index 7e678d7c0fb7..f32bf007b8a4 100644 --- a/eng/SharedFramework.Local.props +++ b/eng/SharedFramework.Local.props @@ -77,7 +77,6 @@ - From 0c43a25d79e65db93e9dbea5a313fd2d1961c42e Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Fri, 1 Apr 2022 14:29:05 -0700 Subject: [PATCH 14/30] Feedback --- .../RateLimiting/src/PublicAPI.Unshipped.txt | 4 +- .../src/RateLimitingMiddleware.cs | 46 ++++++------------- .../RateLimiting/src/RateLimitingOptions.cs | 18 +++----- .../src/RateLimitingServicesExtensions.cs | 34 ++++++++++++++ .../test/RateLimitingMiddlewareTests.cs | 8 ++-- .../test/RateLimitingOptionsTests.cs | 2 +- 6 files changed, 62 insertions(+), 50 deletions(-) create mode 100644 src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs diff --git a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt index ebd88490645b..8b9c0a923b33 100644 --- a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt @@ -2,8 +2,10 @@ Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware.RateLimitingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options) -> void Microsoft.AspNetCore.RateLimiting.RateLimitingOptions -Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.AddLimiter(System.Threading.RateLimiting.PartitionedRateLimiter! limiter) -> Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.get -> System.Threading.RateLimiting.PartitionedRateLimiter? +Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.set -> void Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.get -> Microsoft.AspNetCore.Http.RequestDelegate! Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.set -> void Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.RateLimitingOptions() -> void +Microsoft.AspNetCore.RateLimiting.RateLimitingServicesExtensions +static Microsoft.AspNetCore.RateLimiting.RateLimitingServicesExtensions.AddRateLimiting(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs index 0ef716d1677e..a8c6f2859136 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs @@ -11,14 +11,13 @@ namespace Microsoft.AspNetCore.RateLimiting; /// /// Limits the rate of requests allowed in the application, based on limits set by a user-provided . /// -public partial class RateLimitingMiddleware +public sealed partial class RateLimitingMiddleware { private readonly RequestDelegate _next; private readonly RequestDelegate _onRejected; private readonly ILogger _logger; private readonly PartitionedRateLimiter _limiter; - private RateLimitLease? _lease; /// /// Creates a new . @@ -62,15 +61,17 @@ public async Task Invoke(HttpContext context) // Make sure we only ever call GetResult once on the TryEnterAsync ValueTask b/c it resets. bool result; + RateLimitLease lease; if (acquireLeaseTask.IsCompleted) { - result = acquireLeaseTask.Result; + lease = acquireLeaseTask.Result; } else { - result = await acquireLeaseTask; + lease = await acquireLeaseTask; } + result = lease.IsAcquired; if (result) { @@ -80,7 +81,7 @@ public async Task Invoke(HttpContext context) } finally { - OnCompletion(); + OnCompletion(lease); } } else @@ -91,53 +92,34 @@ public async Task Invoke(HttpContext context) } } - private ValueTask TryAcquireAsync(HttpContext context) + private ValueTask TryAcquireAsync(HttpContext context) { - // a return value of 'false' indicates that the request is rejected - // a return value of 'true' indicates that the request may proceed - var lease = _limiter.Acquire(context); if (lease.IsAcquired) { - _lease = lease; - return ValueTask.FromResult(true); + return ValueTask.FromResult(lease); } var task = _limiter.WaitAsync(context); if (task.IsCompletedSuccessfully) { - lease = task.Result; - if (lease.IsAcquired) - { - _lease = lease; - return ValueTask.FromResult(true); - } - - return ValueTask.FromResult(false); + return task; } return Awaited(task); } - private void OnCompletion() + private static void OnCompletion(RateLimitLease lease) { - if (_lease != null) + if (lease != null) { - _lease.Dispose(); + lease.Dispose(); } } - private async ValueTask Awaited(ValueTask task) + private static async ValueTask Awaited(ValueTask task) { - var lease = await task; - - if (lease.IsAcquired) - { - _lease = lease; - return true; - } - - return false; + return await task; } private static partial class RateLimiterLog diff --git a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs index c0793ad6eb3b..134edd0e6a02 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs @@ -20,20 +20,14 @@ public class RateLimitingOptions public PartitionedRateLimiter? Limiter { get => _limiter; - } - - /// - /// Adds a new rate limiter. - /// - /// The to be added. - public RateLimitingOptions AddLimiter(PartitionedRateLimiter limiter) - { - if (limiter == null) + set { - throw new ArgumentNullException(nameof(limiter)); + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + _limiter = value; } - _limiter = limiter; - return this; } /// diff --git a/src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs b/src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs new file mode 100644 index 000000000000..5d1ee58f69b7 --- /dev/null +++ b/src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.RateLimiting; + +/// +/// Extension methods for the RateLimiting middleware. +/// +public static class RateLimitingServicesExtensions +{ + + /// + /// Adds rate limiting services. + /// + /// The for adding services. + /// A delegate to configure the . + /// + public static IServiceCollection AddRateLimiting(this IServiceCollection services, Action configureOptions) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } + + services.Configure(configureOptions); + return services; + } +} diff --git a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs index 0f5a027dad6a..c577afe59b6e 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs @@ -21,7 +21,7 @@ public class RateLimitingMiddlewareTests : LoggedTest public void Ctor_ThrowsExceptionsWhenNullArgs() { var options = CreateOptionsAccessor(); - options.Value.AddLimiter(new TestPartitionedRateLimiter()); + options.Value.Limiter = new TestPartitionedRateLimiter(); Assert.Throws(() => new RateLimitingMiddleware( null, @@ -54,7 +54,7 @@ public void Ctor_ThrowsExceptionsWhenNullOnRejected() { var options = CreateOptionsAccessor(); options.Value.OnRejected = null; - options.Value.AddLimiter(new TestPartitionedRateLimiter()); + options.Value.Limiter = new TestPartitionedRateLimiter(); var ex = Assert.Throws(() => new RateLimitingMiddleware(c => { return Task.CompletedTask; @@ -69,7 +69,7 @@ public async Task RequestsCallNextIfAccepted() { var flag = false; var options = CreateOptionsAccessor(); - options.Value.AddLimiter(new TestPartitionedRateLimiter(new TestRateLimiter(true))); + options.Value.Limiter = new TestPartitionedRateLimiter(new TestRateLimiter(true)); var middleware = new RateLimitingMiddleware(c => { flag = true; @@ -87,7 +87,7 @@ public async Task RequestRejected_CallsOnRejectedAndGives503() { bool onRejectedInvoked = false; var options = CreateOptionsAccessor(); - options.Value.AddLimiter(new TestPartitionedRateLimiter(new TestRateLimiter(false))); + options.Value.Limiter = new TestPartitionedRateLimiter(new TestRateLimiter(false)); options.Value.OnRejected = httpContext => { onRejectedInvoked = true; diff --git a/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs b/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs index ac1a7dac8ef7..6072ef7f66c9 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs @@ -11,6 +11,6 @@ public class RateLimitingOptionsTests public void ThrowsOnNullLimiter() { var options = new RateLimitingOptions(); - var ex = Assert.Throws(() => options.AddLimiter(null)); + var ex = Assert.Throws(() => options.Limiter = null); } } From 017797d5e600ca9e8f93802b47b4369d20321ae9 Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Fri, 1 Apr 2022 14:42:51 -0700 Subject: [PATCH 15/30] Internal+IVT --- src/Middleware/RateLimiting/src/Properties/AssemblyInfo.cs | 6 ++++++ src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt | 3 --- src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 src/Middleware/RateLimiting/src/Properties/AssemblyInfo.cs diff --git a/src/Middleware/RateLimiting/src/Properties/AssemblyInfo.cs b/src/Middleware/RateLimiting/src/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..2588fa961b28 --- /dev/null +++ b/src/Middleware/RateLimiting/src/Properties/AssemblyInfo.cs @@ -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")] diff --git a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt index 8b9c0a923b33..32228a66e8ee 100644 --- a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt @@ -1,6 +1,3 @@ -Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware -Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.RateLimiting.RateLimitingMiddleware.RateLimitingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.Extensions.Options.IOptions! options) -> void Microsoft.AspNetCore.RateLimiting.RateLimitingOptions Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.get -> System.Threading.RateLimiting.PartitionedRateLimiter? Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.set -> void diff --git a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs index a8c6f2859136..af24080b7579 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.RateLimiting; /// /// Limits the rate of requests allowed in the application, based on limits set by a user-provided . /// -public sealed partial class RateLimitingMiddleware +internal sealed partial class RateLimitingMiddleware { private readonly RequestDelegate _next; From 2b406b076fcd3e7ac4f3b1f9a1a41b1c032d5659 Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Fri, 1 Apr 2022 15:12:24 -0700 Subject: [PATCH 16/30] Feedback --- src/Middleware/RateLimiting/src/NoLimiter.cs | 36 +++++++++++++++++++ .../RateLimiting/src/PublicAPI.Unshipped.txt | 2 +- .../src/RateLimitingMiddleware.cs | 10 ------ .../RateLimiting/src/RateLimitingOptions.cs | 22 +++++++++--- .../test/RateLimitingMiddlewareTests.cs | 28 --------------- .../test/RateLimitingOptionsTests.cs | 7 ++++ 6 files changed, 61 insertions(+), 44 deletions(-) create mode 100644 src/Middleware/RateLimiting/src/NoLimiter.cs diff --git a/src/Middleware/RateLimiting/src/NoLimiter.cs b/src/Middleware/RateLimiting/src/NoLimiter.cs new file mode 100644 index 000000000000..d3b61d1192b8 --- /dev/null +++ b/src/Middleware/RateLimiting/src/NoLimiter.cs @@ -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 : PartitionedRateLimiter +{ + public override int GetAvailablePermits(TResource resourceID) + { + return 1; + } + + protected override RateLimitLease AcquireCore(TResource resourceID, int permitCount) + { + return new NoLimiterLease(); + } + + protected override ValueTask WaitAsyncCore(TResource resourceID, int permitCount, CancellationToken cancellationToken) + { + return new ValueTask(new NoLimiterLease()); + } +} + +internal class NoLimiterLease : RateLimitLease +{ + public override bool IsAcquired => true; + + public override IEnumerable MetadataNames => new List(); + + public override bool TryGetMetadata(string metadataName, out object? metadata) + { + metadata = null; + return false; + } +} diff --git a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt index 32228a66e8ee..746cce5444de 100644 --- a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt @@ -1,5 +1,5 @@ Microsoft.AspNetCore.RateLimiting.RateLimitingOptions -Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.get -> System.Threading.RateLimiting.PartitionedRateLimiter? +Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.get -> System.Threading.RateLimiting.PartitionedRateLimiter! Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.set -> void Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.get -> Microsoft.AspNetCore.Http.RequestDelegate! Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.set -> void diff --git a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs index af24080b7579..5983a1b2c579 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs @@ -29,16 +29,6 @@ public RateLimitingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory { _next = next ?? throw new ArgumentNullException(nameof(next)); - if (options.Value.Limiter == null) - { - throw new ArgumentException("The value of 'options.Limiter' must not be null.", nameof(options)); - } - - if (options.Value.OnRejected == null) - { - throw new ArgumentException("The value of 'options.OnRejected' must not be null.", nameof(options)); - } - if (loggerFactory == null) { throw new ArgumentNullException(nameof(loggerFactory)); diff --git a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs index 134edd0e6a02..940be8580abd 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs @@ -12,12 +12,16 @@ namespace Microsoft.AspNetCore.RateLimiting; public class RateLimitingOptions { // TODO - Provide a default? - private PartitionedRateLimiter? _limiter; + private PartitionedRateLimiter _limiter = new NoLimiter(); + private RequestDelegate _onRejected = context => + { + return Task.CompletedTask; + }; /// /// Gets the /// - public PartitionedRateLimiter? Limiter + public PartitionedRateLimiter Limiter { get => _limiter; set @@ -34,8 +38,16 @@ public PartitionedRateLimiter? Limiter /// A that handles requests rejected by this middleware. /// If it doesn't modify the response, an empty 503 response will be written. /// - public RequestDelegate OnRejected { get; set; } = context => + public RequestDelegate OnRejected { - return Task.CompletedTask; - }; + get => _onRejected; + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + _onRejected = value; + } + } } diff --git a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs index c577afe59b6e..2ce4c43c9046 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs @@ -36,34 +36,6 @@ public void Ctor_ThrowsExceptionsWhenNullArgs() options)); } - [Fact] - public void Ctor_ThrowsExceptionsWhenNullLimiter() - { - // Default Options instance has no limiter set - var ex = Assert.Throws(() => new RateLimitingMiddleware(c => - { - return Task.CompletedTask; - }, - new NullLoggerFactory(), - CreateOptionsAccessor())); - Assert.Contains("The value of 'options.Limiter' must not be null.", ex.Message); - } - - [Fact] - public void Ctor_ThrowsExceptionsWhenNullOnRejected() - { - var options = CreateOptionsAccessor(); - options.Value.OnRejected = null; - options.Value.Limiter = new TestPartitionedRateLimiter(); - var ex = Assert.Throws(() => new RateLimitingMiddleware(c => - { - return Task.CompletedTask; - }, - new NullLoggerFactory(), - options)); - Assert.Contains("The value of 'options.OnRejected' must not be null.", ex.Message); - } - [Fact] public async Task RequestsCallNextIfAccepted() { diff --git a/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs b/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs index 6072ef7f66c9..3e7eefc27ae5 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs @@ -13,4 +13,11 @@ public void ThrowsOnNullLimiter() var options = new RateLimitingOptions(); var ex = Assert.Throws(() => options.Limiter = null); } + + [Fact] + public void ThrowsOnNullOnRejected() + { + var options = new RateLimitingOptions(); + var ex = Assert.Throws(() => options.OnRejected = null); + } } From 75aa2cf550bae8a93cdc5c95c01203cc698b6f0f Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Tue, 5 Apr 2022 14:34:38 -0700 Subject: [PATCH 17/30] Feedback chunk 1 --- .../src/RateLimitingMiddleware.cs | 37 +++++-------------- .../RateLimiting/src/RateLimitingOptions.cs | 22 ++--------- .../src/RateLimitingServicesExtensions.cs | 1 - .../test/RateLimitingMiddlewareTests.cs | 6 +-- .../test/TestPartitionedRateLimiter.cs | 1 - .../RateLimiting/test/TestRateLimitLease.cs | 1 + .../RateLimiting/test/TestRateLimiter.cs | 1 + 7 files changed, 18 insertions(+), 51 deletions(-) diff --git a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs index 5983a1b2c579..772f8d296452 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs @@ -13,7 +13,6 @@ namespace Microsoft.AspNetCore.RateLimiting; /// internal sealed partial class RateLimitingMiddleware { - private readonly RequestDelegate _next; private readonly RequestDelegate _onRejected; private readonly ILogger _logger; @@ -23,18 +22,18 @@ internal sealed partial class RateLimitingMiddleware /// Creates a new . /// /// The representing the next middleware in the pipeline. - /// The used for logging. + /// The used for logging. /// The options for the middleware. - public RateLimitingMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, IOptions options) + public RateLimitingMiddleware(RequestDelegate next, ILogger logger, IOptions options) { _next = next ?? throw new ArgumentNullException(nameof(next)); - if (loggerFactory == null) + if (logger == null) { - throw new ArgumentNullException(nameof(loggerFactory)); + throw new ArgumentNullException(nameof(logger)); } - _logger = loggerFactory.CreateLogger(); + _logger = logger; _limiter = options.Value.Limiter; _onRejected = options.Value.OnRejected; } @@ -49,8 +48,6 @@ public async Task Invoke(HttpContext context) { var acquireLeaseTask = TryAcquireAsync(context); - // Make sure we only ever call GetResult once on the TryEnterAsync ValueTask b/c it resets. - bool result; RateLimitLease lease; if (acquireLeaseTask.IsCompleted) @@ -61,7 +58,8 @@ public async Task Invoke(HttpContext context) { lease = await acquireLeaseTask; } - result = lease.IsAcquired; + // Make sure we only ever call GetResult once on the TryEnterAsync ValueTask b/c it resets. + var result = lease.IsAcquired; if (result) { @@ -90,27 +88,10 @@ private ValueTask TryAcquireAsync(HttpContext context) return ValueTask.FromResult(lease); } - var task = _limiter.WaitAsync(context); - if (task.IsCompletedSuccessfully) - { - return task; - } - - return Awaited(task); + return _limiter.WaitAsync(context, cancellationToken: context.RequestAborted); } - private static void OnCompletion(RateLimitLease lease) - { - if (lease != null) - { - lease.Dispose(); - } - } - - private static async ValueTask Awaited(ValueTask task) - { - return await task; - } + private static void OnCompletion(RateLimitLease lease) => lease?.Dispose(); private static partial class RateLimiterLog { diff --git a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs index 940be8580abd..b40804824582 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs @@ -7,9 +7,9 @@ namespace Microsoft.AspNetCore.RateLimiting; /// -/// Specifies options for the . +/// Specifies options for the rate limiting middleware. /// -public class RateLimitingOptions +public sealed class RateLimitingOptions { // TODO - Provide a default? private PartitionedRateLimiter _limiter = new NoLimiter(); @@ -24,14 +24,7 @@ public class RateLimitingOptions public PartitionedRateLimiter Limiter { get => _limiter; - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - _limiter = value; - } + set => _limiter = value ?? throw new ArgumentNullException(nameof(value)); } /// @@ -41,13 +34,6 @@ public PartitionedRateLimiter Limiter public RequestDelegate OnRejected { get => _onRejected; - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } - _onRejected = value; - } + set => _onRejected = value ?? throw new ArgumentNullException(nameof(value)); } } diff --git a/src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs b/src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs index 5d1ee58f69b7..4645d527428d 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs @@ -10,7 +10,6 @@ namespace Microsoft.AspNetCore.RateLimiting; /// public static class RateLimitingServicesExtensions { - /// /// Adds rate limiting services. /// diff --git a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs index 2ce4c43c9046..5d599c1ee2f6 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs @@ -25,7 +25,7 @@ public void Ctor_ThrowsExceptionsWhenNullArgs() Assert.Throws(() => new RateLimitingMiddleware( null, - new NullLoggerFactory(), + new NullLoggerFactory().CreateLogger(), options)); Assert.Throws(() => new RateLimitingMiddleware(c => @@ -47,7 +47,7 @@ public async Task RequestsCallNextIfAccepted() flag = true; return Task.CompletedTask; }, - new NullLoggerFactory(), + new NullLoggerFactory().CreateLogger(), options); await middleware.Invoke(new DefaultHttpContext()); @@ -70,7 +70,7 @@ public async Task RequestRejected_CallsOnRejectedAndGives503() { return Task.CompletedTask; }, - new NullLoggerFactory(), + new NullLoggerFactory().CreateLogger(), options); var context = new DefaultHttpContext(); diff --git a/src/Middleware/RateLimiting/test/TestPartitionedRateLimiter.cs b/src/Middleware/RateLimiting/test/TestPartitionedRateLimiter.cs index b36e5c7a3cbd..7e3017a95bce 100644 --- a/src/Middleware/RateLimiting/test/TestPartitionedRateLimiter.cs +++ b/src/Middleware/RateLimiting/test/TestPartitionedRateLimiter.cs @@ -12,7 +12,6 @@ namespace Microsoft.AspNetCore.RateLimiting; internal class TestPartitionedRateLimiter : PartitionedRateLimiter { - private List limiters = new List(); public TestPartitionedRateLimiter() { } diff --git a/src/Middleware/RateLimiting/test/TestRateLimitLease.cs b/src/Middleware/RateLimiting/test/TestRateLimitLease.cs index 7e3962b69b6a..8372e8c9d8dc 100644 --- a/src/Middleware/RateLimiting/test/TestRateLimitLease.cs +++ b/src/Middleware/RateLimiting/test/TestRateLimitLease.cs @@ -27,6 +27,7 @@ public override bool TryGetMetadata(string metadataName, out object metadata) { throw new NotImplementedException(); } + protected override void Dispose(bool disposing) { if (_leases != null) diff --git a/src/Middleware/RateLimiting/test/TestRateLimiter.cs b/src/Middleware/RateLimiting/test/TestRateLimiter.cs index 44ff22c5e6b4..3b0f64aeba0a 100644 --- a/src/Middleware/RateLimiting/test/TestRateLimiter.cs +++ b/src/Middleware/RateLimiting/test/TestRateLimiter.cs @@ -17,6 +17,7 @@ public TestRateLimiter(bool alwaysAccept) { _alwaysAccept = alwaysAccept; } + public override int GetAvailablePermits() { throw new NotImplementedException(); From ade42e09090c3b7c57a91ba3a641b2234ddf3612 Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Tue, 5 Apr 2022 14:47:08 -0700 Subject: [PATCH 18/30] Feedback chunk 2 --- .../src/RateLimitingMiddleware.cs | 32 ++++++------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs index 772f8d296452..89c33bd2012e 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs @@ -46,37 +46,23 @@ public RateLimitingMiddleware(RequestDelegate next, ILoggerA that completes when the request leaves. public async Task Invoke(HttpContext context) { - var acquireLeaseTask = TryAcquireAsync(context); - - RateLimitLease lease; - - if (acquireLeaseTask.IsCompleted) - { - lease = acquireLeaseTask.Result; - } - else - { - lease = await acquireLeaseTask; - } - // Make sure we only ever call GetResult once on the TryEnterAsync ValueTask b/c it resets. - var result = lease.IsAcquired; - - if (result) + using var lease = await TryAcquireAsync(context); + try { - try + if (lease.IsAcquired) { await _next(context); } - finally + else { - OnCompletion(lease); + RateLimiterLog.RequestRejectedLimitsExceeded(_logger); + context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; + await _onRejected(context); } } - else + finally { - RateLimiterLog.RequestRejectedLimitsExceeded(_logger); - context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; - await _onRejected(context); + OnCompletion(lease); } } From d025eb8d4c1283f0ea09156e16644e0ce90ca76d Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Tue, 5 Apr 2022 14:58:17 -0700 Subject: [PATCH 19/30] Small feedback --- src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs index 89c33bd2012e..c85d9c9f27ed 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs @@ -62,7 +62,7 @@ public async Task Invoke(HttpContext context) } finally { - OnCompletion(lease); + lease?.Dispose(); } } @@ -77,8 +77,6 @@ private ValueTask TryAcquireAsync(HttpContext context) return _limiter.WaitAsync(context, cancellationToken: context.RequestAborted); } - private static void OnCompletion(RateLimitLease lease) => lease?.Dispose(); - private static partial class RateLimiterLog { [LoggerMessage(1, LogLevel.Debug, "Rate limits exceeded, rejecting this request with a '503 server not available' error", EventName = "RequestRejectedLimitsExceeded")] From ddddbd4ab2be531ff281b9c3a00778658a55953f Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Wed, 13 Apr 2022 11:00:57 -0700 Subject: [PATCH 20/30] Fix extension methods --- .../RateLimiting/src/PublicAPI.Unshipped.txt | 5 +- .../src/RateLimitingExtensions.cs | 48 +++++++++++++++++++ .../src/RateLimitingServicesExtensions.cs | 33 ------------- 3 files changed, 51 insertions(+), 35 deletions(-) create mode 100644 src/Middleware/RateLimiting/src/RateLimitingExtensions.cs delete mode 100644 src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs diff --git a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt index 746cce5444de..c1d9c9a8e7ee 100644 --- a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt @@ -1,8 +1,9 @@ +Microsoft.AspNetCore.RateLimiting.RateLimitingExtensions Microsoft.AspNetCore.RateLimiting.RateLimitingOptions Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.get -> System.Threading.RateLimiting.PartitionedRateLimiter! Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.set -> void Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.get -> Microsoft.AspNetCore.Http.RequestDelegate! Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.set -> void Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.RateLimitingOptions() -> void -Microsoft.AspNetCore.RateLimiting.RateLimitingServicesExtensions -static Microsoft.AspNetCore.RateLimiting.RateLimitingServicesExtensions.AddRateLimiting(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.AspNetCore.RateLimiting.RateLimitingExtensions.UseRateLimiting(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +static Microsoft.AspNetCore.RateLimiting.RateLimitingExtensions.UseRateLimiting(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! options) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! diff --git a/src/Middleware/RateLimiting/src/RateLimitingExtensions.cs b/src/Middleware/RateLimiting/src/RateLimitingExtensions.cs new file mode 100644 index 000000000000..c58674d3e3db --- /dev/null +++ b/src/Middleware/RateLimiting/src/RateLimitingExtensions.cs @@ -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 Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.RateLimiting; + +/// +/// Extension methods for the RateLimiting middleware. +/// +public static class RateLimitingExtensions +{ + /// + /// Enables rate limiting for the application. + /// + /// + /// + public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMiddleware(); + } + + /// + /// Enables rate limiting for the application with the given options. + /// + /// + /// + /// + public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder app, RateLimitingOptions options) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + return app.UseMiddleware(Options.Create(options)); + } +} diff --git a/src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs b/src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs deleted file mode 100644 index 4645d527428d..000000000000 --- a/src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.RateLimiting; - -/// -/// Extension methods for the RateLimiting middleware. -/// -public static class RateLimitingServicesExtensions -{ - /// - /// Adds rate limiting services. - /// - /// The for adding services. - /// A delegate to configure the . - /// - public static IServiceCollection AddRateLimiting(this IServiceCollection services, Action configureOptions) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - if (configureOptions == null) - { - throw new ArgumentNullException(nameof(configureOptions)); - } - - services.Configure(configureOptions); - return services; - } -} From 9abcb077ad82eabc696e2ca2f5daae4ceed002b6 Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Wed, 13 Apr 2022 11:06:07 -0700 Subject: [PATCH 21/30] Small fix --- .../src/RateLimitingMiddleware.cs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs index c85d9c9f27ed..3fe3681e1bb8 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs @@ -47,22 +47,15 @@ public RateLimitingMiddleware(RequestDelegate next, ILogger Date: Wed, 13 Apr 2022 11:14:12 -0700 Subject: [PATCH 22/30] Func --- src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt | 2 +- src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs | 4 ++-- src/Middleware/RateLimiting/src/RateLimitingOptions.cs | 4 ++-- .../RateLimiting/test/RateLimitingMiddlewareTests.cs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt index c1d9c9a8e7ee..d40e59a53484 100644 --- a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt @@ -2,7 +2,7 @@ Microsoft.AspNetCore.RateLimiting.RateLimitingExtensions Microsoft.AspNetCore.RateLimiting.RateLimitingOptions Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.get -> System.Threading.RateLimiting.PartitionedRateLimiter! Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.set -> void -Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.get -> Microsoft.AspNetCore.Http.RequestDelegate! +Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.get -> System.Func! Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.set -> void Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.RateLimitingOptions() -> void static Microsoft.AspNetCore.RateLimiting.RateLimitingExtensions.UseRateLimiting(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! diff --git a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs index 3fe3681e1bb8..d5077a97bb3a 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs @@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.RateLimiting; internal sealed partial class RateLimitingMiddleware { private readonly RequestDelegate _next; - private readonly RequestDelegate _onRejected; + private readonly Func _onRejected; private readonly ILogger _logger; private readonly PartitionedRateLimiter _limiter; @@ -55,7 +55,7 @@ public async Task Invoke(HttpContext context) { RateLimiterLog.RequestRejectedLimitsExceeded(_logger); context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; - await _onRejected(context); + await _onRejected(context, lease); } } diff --git a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs index b40804824582..0c10e4b26d49 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingOptions.cs @@ -13,7 +13,7 @@ public sealed class RateLimitingOptions { // TODO - Provide a default? private PartitionedRateLimiter _limiter = new NoLimiter(); - private RequestDelegate _onRejected = context => + private Func _onRejected = (context, lease) => { return Task.CompletedTask; }; @@ -31,7 +31,7 @@ public PartitionedRateLimiter Limiter /// A that handles requests rejected by this middleware. /// If it doesn't modify the response, an empty 503 response will be written. /// - public RequestDelegate OnRejected + public Func OnRejected { get => _onRejected; set => _onRejected = value ?? throw new ArgumentNullException(nameof(value)); diff --git a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs index 5d599c1ee2f6..1e164c568128 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs @@ -60,7 +60,7 @@ public async Task RequestRejected_CallsOnRejectedAndGives503() bool onRejectedInvoked = false; var options = CreateOptionsAccessor(); options.Value.Limiter = new TestPartitionedRateLimiter(new TestRateLimiter(false)); - options.Value.OnRejected = httpContext => + options.Value.OnRejected = (httpContext, lease) => { onRejectedInvoked = true; return Task.CompletedTask; From cea2af6178784dba5d7f6827594ba117b5439d61 Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Wed, 13 Apr 2022 13:02:50 -0700 Subject: [PATCH 23/30] Config status code --- .../RateLimiting/src/PublicAPI.Unshipped.txt | 2 ++ .../RateLimiting/src/RateLimitingMiddleware.cs | 5 ++++- .../RateLimiting/src/RateLimitingOptions.cs | 12 +++++++++--- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt index d40e59a53484..283f66201bde 100644 --- a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt @@ -5,5 +5,7 @@ Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.set -> void Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.get -> System.Func! Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.set -> void Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.RateLimitingOptions() -> void +Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.RejectionStatusCode.get -> System.Net.HttpStatusCode +Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.RejectionStatusCode.set -> void static Microsoft.AspNetCore.RateLimiting.RateLimitingExtensions.UseRateLimiting(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! static Microsoft.AspNetCore.RateLimiting.RateLimitingExtensions.UseRateLimiting(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! options) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! diff --git a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs index d5077a97bb3a..31c65b81feab 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Net; using System.Threading.RateLimiting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -17,6 +18,7 @@ internal sealed partial class RateLimitingMiddleware private readonly Func _onRejected; private readonly ILogger _logger; private readonly PartitionedRateLimiter _limiter; + private readonly HttpStatusCode _rejectionStatusCode; /// /// Creates a new . @@ -36,6 +38,7 @@ public RateLimitingMiddleware(RequestDelegate next, ILogger - /// Gets the + /// Gets or sets the /// public PartitionedRateLimiter Limiter { @@ -28,12 +29,17 @@ public PartitionedRateLimiter Limiter } /// - /// A that handles requests rejected by this middleware. - /// If it doesn't modify the response, an empty 503 response will be written. + /// Gets or sets a that handles requests rejected by this middleware. /// public Func OnRejected { get => _onRejected; set => _onRejected = value ?? throw new ArgumentNullException(nameof(value)); } + + /// + /// Gets or sets the to set on the response when a request is rejected. + /// Defaults to . + /// + public HttpStatusCode RejectionStatusCode { get; set; } = HttpStatusCode.ServiceUnavailable; } From ac0ad26b8006af743d8b12b6760fec18af54ade2 Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Fri, 15 Apr 2022 11:02:35 -0700 Subject: [PATCH 24/30] Feedback, add servicecollection extension --- .../Microsoft.AspNetCore.RateLimiting.csproj | 1 - .../RateLimiting/src/PublicAPI.Unshipped.txt | 3 +- .../src/RateLimitingExtensions.cs | 21 ------------ .../src/RateLimitingServicesExtensions.cs | 33 +++++++++++++++++++ .../test/RateLimitingMiddlewareTests.cs | 3 +- .../test/RateLimitingOptionsTests.cs | 4 +-- .../RateLimiting/test/TestRateLimitLease.cs | 6 +--- .../RateLimiting/test/TestRateLimiter.cs | 7 ++-- 8 files changed, 42 insertions(+), 36 deletions(-) create mode 100644 src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs diff --git a/src/Middleware/RateLimiting/src/Microsoft.AspNetCore.RateLimiting.csproj b/src/Middleware/RateLimiting/src/Microsoft.AspNetCore.RateLimiting.csproj index 72aff185ffda..b8ba30375ac4 100644 --- a/src/Middleware/RateLimiting/src/Microsoft.AspNetCore.RateLimiting.csproj +++ b/src/Middleware/RateLimiting/src/Microsoft.AspNetCore.RateLimiting.csproj @@ -5,7 +5,6 @@ $(DefaultNetCoreTargetFramework) true aspnetcore - enable diff --git a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt index 283f66201bde..14386d09809e 100644 --- a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt @@ -7,5 +7,6 @@ Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.set -> void Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.RateLimitingOptions() -> void Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.RejectionStatusCode.get -> System.Net.HttpStatusCode Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.RejectionStatusCode.set -> void +Microsoft.AspNetCore.RateLimiting.RateLimitingServicesExtensions static Microsoft.AspNetCore.RateLimiting.RateLimitingExtensions.UseRateLimiting(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! -static Microsoft.AspNetCore.RateLimiting.RateLimitingExtensions.UseRateLimiting(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app, Microsoft.AspNetCore.RateLimiting.RateLimitingOptions! options) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +static Microsoft.AspNetCore.RateLimiting.RateLimitingServicesExtensions.ConfigureRateLimiting(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Middleware/RateLimiting/src/RateLimitingExtensions.cs b/src/Middleware/RateLimiting/src/RateLimitingExtensions.cs index c58674d3e3db..7d62ea5e0417 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingExtensions.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingExtensions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.RateLimiting; @@ -25,24 +24,4 @@ public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder app) return app.UseMiddleware(); } - - /// - /// Enables rate limiting for the application with the given options. - /// - /// - /// - /// - public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder app, RateLimitingOptions options) - { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - return app.UseMiddleware(Options.Create(options)); - } } diff --git a/src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs b/src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs new file mode 100644 index 000000000000..2c5ebb7a6120 --- /dev/null +++ b/src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs @@ -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 Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.RateLimiting; + +/// +/// Extension methods for the RateLimiting middleware. +/// +public static class RateLimitingServicesExtensions +{ + /// + /// Configures rate limiting services. + /// + /// The for adding services. + /// A delegate to configure the . + /// + public static IServiceCollection ConfigureRateLimiting(this IServiceCollection services, Action configureOptions) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } + + services.Configure(configureOptions); + return services; + } +} diff --git a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs index 1e164c568128..c3b6e83cf6b1 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs @@ -15,6 +15,7 @@ using Moq; namespace Microsoft.AspNetCore.RateLimiting; + public class RateLimitingMiddlewareTests : LoggedTest { [Fact] @@ -57,7 +58,7 @@ public async Task RequestsCallNextIfAccepted() [Fact] public async Task RequestRejected_CallsOnRejectedAndGives503() { - bool onRejectedInvoked = false; + var onRejectedInvoked = false; var options = CreateOptionsAccessor(); options.Value.Limiter = new TestPartitionedRateLimiter(new TestRateLimiter(false)); options.Value.OnRejected = (httpContext, lease) => diff --git a/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs b/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs index 3e7eefc27ae5..946520c7b0a8 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs @@ -11,13 +11,13 @@ public class RateLimitingOptionsTests public void ThrowsOnNullLimiter() { var options = new RateLimitingOptions(); - var ex = Assert.Throws(() => options.Limiter = null); + Assert.Throws(() => options.Limiter = null); } [Fact] public void ThrowsOnNullOnRejected() { var options = new RateLimitingOptions(); - var ex = Assert.Throws(() => options.OnRejected = null); + Assert.Throws(() => options.OnRejected = null); } } diff --git a/src/Middleware/RateLimiting/test/TestRateLimitLease.cs b/src/Middleware/RateLimiting/test/TestRateLimitLease.cs index 8372e8c9d8dc..afe31aff3171 100644 --- a/src/Middleware/RateLimiting/test/TestRateLimitLease.cs +++ b/src/Middleware/RateLimiting/test/TestRateLimitLease.cs @@ -1,14 +1,10 @@ // 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 TestRateLimitLease : RateLimitLease { internal List _leases; diff --git a/src/Middleware/RateLimiting/test/TestRateLimiter.cs b/src/Middleware/RateLimiting/test/TestRateLimiter.cs index 3b0f64aeba0a..fa8e45e17bde 100644 --- a/src/Middleware/RateLimiting/test/TestRateLimiter.cs +++ b/src/Middleware/RateLimiting/test/TestRateLimiter.cs @@ -1,12 +1,7 @@ // 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 TestRateLimiter : RateLimiter @@ -18,6 +13,8 @@ public TestRateLimiter(bool alwaysAccept) _alwaysAccept = alwaysAccept; } + public override TimeSpan? IdleDuration => throw new NotImplementedException(); + public override int GetAvailablePermits() { throw new NotImplementedException(); From fc92f12194c5616455d3858b54153a7fbc0939b0 Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Tue, 19 Apr 2022 11:07:00 -0700 Subject: [PATCH 25/30] Update API --- .../RateLimiting/src/PublicAPI.Unshipped.txt | 23 +++++---- ...mitingOptions.cs => RateLimiterOptions.cs} | 22 +++++++-- ...ateLimitingApplicationBuilderExtensions.cs | 47 +++++++++++++++++++ .../src/RateLimitingExtensions.cs | 27 ----------- .../src/RateLimitingMiddleware.cs | 13 ++--- .../src/RateLimitingServicesExtensions.cs | 33 ------------- .../test/RateLimitingMiddlewareTests.cs | 38 +++++++++++---- .../test/RateLimitingOptionsTests.cs | 11 ++++- 8 files changed, 122 insertions(+), 92 deletions(-) rename src/Middleware/RateLimiting/src/{RateLimitingOptions.cs => RateLimiterOptions.cs} (62%) create mode 100644 src/Middleware/RateLimiting/src/RateLimitingApplicationBuilderExtensions.cs delete mode 100644 src/Middleware/RateLimiting/src/RateLimitingExtensions.cs delete mode 100644 src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs diff --git a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt index 14386d09809e..b62cec89b1cc 100644 --- a/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt @@ -1,12 +1,11 @@ -Microsoft.AspNetCore.RateLimiting.RateLimitingExtensions -Microsoft.AspNetCore.RateLimiting.RateLimitingOptions -Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.get -> System.Threading.RateLimiting.PartitionedRateLimiter! -Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.set -> void -Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.get -> System.Func! -Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.set -> void -Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.RateLimitingOptions() -> void -Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.RejectionStatusCode.get -> System.Net.HttpStatusCode -Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.RejectionStatusCode.set -> void -Microsoft.AspNetCore.RateLimiting.RateLimitingServicesExtensions -static Microsoft.AspNetCore.RateLimiting.RateLimitingExtensions.UseRateLimiting(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! -static Microsoft.AspNetCore.RateLimiting.RateLimitingServicesExtensions.ConfigureRateLimiting(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +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.RateLimiting.RateLimiterOptions.Limiter.set -> void +Microsoft.AspNetCore.RateLimiting.RateLimiterOptions.OnRejected.get -> System.Func! +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! diff --git a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs b/src/Middleware/RateLimiting/src/RateLimiterOptions.cs similarity index 62% rename from src/Middleware/RateLimiting/src/RateLimitingOptions.cs rename to src/Middleware/RateLimiting/src/RateLimiterOptions.cs index d003118d57e5..ade4404d7555 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingOptions.cs +++ b/src/Middleware/RateLimiting/src/RateLimiterOptions.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.RateLimiting; /// /// Specifies options for the rate limiting middleware. /// -public sealed class RateLimitingOptions +public sealed class RateLimiterOptions { // TODO - Provide a default? private PartitionedRateLimiter _limiter = new NoLimiter(); @@ -18,6 +18,7 @@ public sealed class RateLimitingOptions { return Task.CompletedTask; }; + private int _defaultOnRejectionStatusCode = (int)HttpStatusCode.ServiceUnavailable; /// /// Gets or sets the @@ -38,8 +39,23 @@ public Func OnRejected } /// - /// Gets or sets the to set on the response when a request is rejected. + /// Gets or sets the default to set on the response when a request is rejected. /// Defaults to . /// - public HttpStatusCode RejectionStatusCode { get; set; } = HttpStatusCode.ServiceUnavailable; + /// + /// This status code will be set before is called, so any status code set by + /// will "win" over this default. + /// + public int DefaultRejectionStatusCode + { + get => _defaultOnRejectionStatusCode; + set + { + if (!Enum.IsDefined(typeof(HttpStatusCode), value)) + { + throw new ArgumentException("Value must be a valid HTTP status code", nameof(value)); + } + _defaultOnRejectionStatusCode = value; + } + } } diff --git a/src/Middleware/RateLimiting/src/RateLimitingApplicationBuilderExtensions.cs b/src/Middleware/RateLimiting/src/RateLimitingApplicationBuilderExtensions.cs new file mode 100644 index 000000000000..42d39fe83218 --- /dev/null +++ b/src/Middleware/RateLimiting/src/RateLimitingApplicationBuilderExtensions.cs @@ -0,0 +1,47 @@ +// 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; + +namespace Microsoft.AspNetCore.RateLimiting; + +/// +/// Extension methods for the RateLimiting middleware. +/// +public static class RateLimitingApplicationBuilderExtensions +{ + /// + /// Enables rate limiting for the application. + /// + /// + /// + public static IApplicationBuilder UseRateLimiter(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMiddleware(); + } + + /// + /// Enables rate limiting for the application. + /// + /// + /// + /// + public static IApplicationBuilder UseRateLimiter(this IApplicationBuilder app, RateLimiterOptions options) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + return app.UseMiddleware(); + } +} diff --git a/src/Middleware/RateLimiting/src/RateLimitingExtensions.cs b/src/Middleware/RateLimiting/src/RateLimitingExtensions.cs deleted file mode 100644 index 7d62ea5e0417..000000000000 --- a/src/Middleware/RateLimiting/src/RateLimitingExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -// 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; - -namespace Microsoft.AspNetCore.RateLimiting; - -/// -/// Extension methods for the RateLimiting middleware. -/// -public static class RateLimitingExtensions -{ - /// - /// Enables rate limiting for the application. - /// - /// - /// - public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder app) - { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - return app.UseMiddleware(); - } -} diff --git a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs index 31c65b81feab..5131b5d129ba 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs @@ -1,7 +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.Net; using System.Threading.RateLimiting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -18,7 +17,7 @@ internal sealed partial class RateLimitingMiddleware private readonly Func _onRejected; private readonly ILogger _logger; private readonly PartitionedRateLimiter _limiter; - private readonly HttpStatusCode _rejectionStatusCode; + private readonly int _rejectionStatusCode; /// /// Creates a new . @@ -26,7 +25,7 @@ internal sealed partial class RateLimitingMiddleware /// The representing the next middleware in the pipeline. /// The used for logging. /// The options for the middleware. - public RateLimitingMiddleware(RequestDelegate next, ILogger logger, IOptions options) + public RateLimitingMiddleware(RequestDelegate next, ILogger logger, IOptions options) { _next = next ?? throw new ArgumentNullException(nameof(next)); @@ -38,7 +37,7 @@ public RateLimitingMiddleware(RequestDelegate next, ILogger TryAcquireAsync(HttpContext context) private static partial class RateLimiterLog { - [LoggerMessage(1, LogLevel.Debug, "Rate limits exceeded, rejecting this request with a '503 server not available' error", EventName = "RequestRejectedLimitsExceeded")] + [LoggerMessage(1, LogLevel.Debug, "Rate limits exceeded, rejecting this request", EventName = "RequestRejectedLimitsExceeded")] internal static partial void RequestRejectedLimitsExceeded(ILogger logger); } } diff --git a/src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs b/src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs deleted file mode 100644 index 2c5ebb7a6120..000000000000 --- a/src/Middleware/RateLimiting/src/RateLimitingServicesExtensions.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.DependencyInjection; - -namespace Microsoft.AspNetCore.RateLimiting; - -/// -/// Extension methods for the RateLimiting middleware. -/// -public static class RateLimitingServicesExtensions -{ - /// - /// Configures rate limiting services. - /// - /// The for adding services. - /// A delegate to configure the . - /// - public static IServiceCollection ConfigureRateLimiting(this IServiceCollection services, Action configureOptions) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - if (configureOptions == null) - { - throw new ArgumentNullException(nameof(configureOptions)); - } - - services.Configure(configureOptions); - return services; - } -} diff --git a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs index c3b6e83cf6b1..c4f20dcd22b8 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs @@ -1,12 +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; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.Logging; @@ -80,10 +74,36 @@ public async Task RequestRejected_CallsOnRejectedAndGives503() Assert.Equal(StatusCodes.Status503ServiceUnavailable, context.Response.StatusCode); } - private IOptions CreateOptionsAccessor() + [Fact] + public async Task RequestRejected_WinsOverDefaultStatusCode() + { + var onRejectedInvoked = false; + var options = CreateOptionsAccessor(); + options.Value.Limiter = new TestPartitionedRateLimiter(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(), + options); + + var context = new DefaultHttpContext(); + await middleware.Invoke(context).DefaultTimeout(); + Assert.True(onRejectedInvoked); + Assert.Equal(StatusCodes.Status429TooManyRequests, context.Response.StatusCode); + } + + private IOptions CreateOptionsAccessor() { - var options = new RateLimitingOptions(); - var optionsAccessor = Mock.Of>(o => o.Value == options); + var options = new RateLimiterOptions(); + var optionsAccessor = Mock.Of>(o => o.Value == options); return optionsAccessor; } diff --git a/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs b/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs index 946520c7b0a8..92033b6b9058 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs @@ -10,14 +10,21 @@ public class RateLimitingOptionsTests [Fact] public void ThrowsOnNullLimiter() { - var options = new RateLimitingOptions(); + var options = new RateLimiterOptions(); Assert.Throws(() => options.Limiter = null); } [Fact] public void ThrowsOnNullOnRejected() { - var options = new RateLimitingOptions(); + var options = new RateLimiterOptions(); Assert.Throws(() => options.OnRejected = null); } + + [Fact] + public void ThrowsOnInvalidStatusCode() + { + var options = new RateLimiterOptions(); + Assert.Throws(() => options.DefaultRejectionStatusCode = -1); + } } From 9d3dffb5106d6760883091dc7fe8f1e490b300fa Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Tue, 19 Apr 2022 14:11:17 -0700 Subject: [PATCH 26/30] Some feedback --- ...ateLimitingApplicationBuilderExtensions.cs | 3 +- ...mitingApplicationBuilderExtensionsTests.cs | 39 +++++++++++++++++++ .../test/RateLimitingMiddlewareTests.cs | 8 +--- 3 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 src/Middleware/RateLimiting/test/RateLimitingApplicationBuilderExtensionsTests.cs diff --git a/src/Middleware/RateLimiting/src/RateLimitingApplicationBuilderExtensions.cs b/src/Middleware/RateLimiting/src/RateLimitingApplicationBuilderExtensions.cs index 42d39fe83218..8ab7b8a76c57 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingApplicationBuilderExtensions.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingApplicationBuilderExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.RateLimiting; @@ -42,6 +43,6 @@ public static IApplicationBuilder UseRateLimiter(this IApplicationBuilder app, R throw new ArgumentNullException(nameof(options)); } - return app.UseMiddleware(); + return app.UseMiddleware(Options.Create(options)); } } diff --git a/src/Middleware/RateLimiting/test/RateLimitingApplicationBuilderExtensionsTests.cs b/src/Middleware/RateLimiting/test/RateLimitingApplicationBuilderExtensionsTests.cs new file mode 100644 index 000000000000..8a7234dd1931 --- /dev/null +++ b/src/Middleware/RateLimiting/test/RateLimitingApplicationBuilderExtensionsTests.cs @@ -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.DependencyInjection; + +namespace Microsoft.AspNetCore.RateLimiting; + +public class RateLimitingApplicationBuilderExtensionsTests +{ + [Fact] + public void UseRateLimiter_ThrowsOnNullOptions() + { + var appBuilder = new ApplicationBuilder(new ServiceCollection().BuildServiceProvider()); + Assert.Throws(() => appBuilder.UseRateLimiter(null)); + } + + [Fact] + public void UseRateLimiter_RespectsOptions() + { + // These are the options that should get used + var options = new RateLimiterOptions(); + options.DefaultRejectionStatusCode = 429; + + // These should not get used + var services = new ServiceCollection(); + services.Configure(options => + { + options.DefaultRejectionStatusCode = 404; + }); + var serviceProvider = services.BuildServiceProvider(); + var appBuilder = new ApplicationBuilder(serviceProvider); + + // Act + appBuilder.UseRateLimiter(options); + var configuredOptions = appBuilder.ApplicationServices.GetRequiredService(); + Assert.Equal(429, configuredOptions.DefaultRejectionStatusCode); + } +} diff --git a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs index c4f20dcd22b8..9d7420cf8a89 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; -using Moq; namespace Microsoft.AspNetCore.RateLimiting; @@ -100,11 +99,6 @@ public async Task RequestRejected_WinsOverDefaultStatusCode() Assert.Equal(StatusCodes.Status429TooManyRequests, context.Response.StatusCode); } - private IOptions CreateOptionsAccessor() - { - var options = new RateLimiterOptions(); - var optionsAccessor = Mock.Of>(o => o.Value == options); - return optionsAccessor; - } + private IOptions CreateOptionsAccessor() => Options.Create(new RateLimiterOptions()); } From 385d9394bb28c07e0c8ea8ced6c4c73db301a6ff Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Tue, 19 Apr 2022 14:22:35 -0700 Subject: [PATCH 27/30] Lil more feedback --- .../RateLimiting/src/RateLimiterOptions.cs | 17 +++-------------- .../RateLimitingApplicationBuilderExtensions.cs | 15 +++------------ .../RateLimiting/src/RateLimitingMiddleware.cs | 8 ++------ 3 files changed, 8 insertions(+), 32 deletions(-) diff --git a/src/Middleware/RateLimiting/src/RateLimiterOptions.cs b/src/Middleware/RateLimiting/src/RateLimiterOptions.cs index ade4404d7555..abc585939724 100644 --- a/src/Middleware/RateLimiting/src/RateLimiterOptions.cs +++ b/src/Middleware/RateLimiting/src/RateLimiterOptions.cs @@ -39,23 +39,12 @@ public Func OnRejected } /// - /// Gets or sets the default to set on the response when a request is rejected. - /// Defaults to . + /// Gets or sets the default status code to set on the response when a request is rejected. + /// Defaults to . /// /// /// This status code will be set before is called, so any status code set by /// will "win" over this default. /// - public int DefaultRejectionStatusCode - { - get => _defaultOnRejectionStatusCode; - set - { - if (!Enum.IsDefined(typeof(HttpStatusCode), value)) - { - throw new ArgumentException("Value must be a valid HTTP status code", nameof(value)); - } - _defaultOnRejectionStatusCode = value; - } - } + public int DefaultRejectionStatusCode { get; set; } = StatusCodes.Status503ServiceUnavailable; } diff --git a/src/Middleware/RateLimiting/src/RateLimitingApplicationBuilderExtensions.cs b/src/Middleware/RateLimiting/src/RateLimitingApplicationBuilderExtensions.cs index 8ab7b8a76c57..7cda2ab98c53 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingApplicationBuilderExtensions.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingApplicationBuilderExtensions.cs @@ -18,10 +18,7 @@ public static class RateLimitingApplicationBuilderExtensions /// public static IApplicationBuilder UseRateLimiter(this IApplicationBuilder app) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } + ArgumentNullException.ThrowIfNull(app); return app.UseMiddleware(); } @@ -34,14 +31,8 @@ public static IApplicationBuilder UseRateLimiter(this IApplicationBuilder app) /// public static IApplicationBuilder UseRateLimiter(this IApplicationBuilder app, RateLimiterOptions options) { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } + ArgumentNullException.ThrowIfNull(app, nameof(app)); + ArgumentNullException.ThrowIfNull(options, nameof(options)); return app.UseMiddleware(Options.Create(options)); } diff --git a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs index 5131b5d129ba..a97aa7c9ee83 100644 --- a/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs +++ b/src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs @@ -29,12 +29,8 @@ public RateLimitingMiddleware(RequestDelegate next, ILogger TryAcquireAsync(HttpContext context) private static partial class RateLimiterLog { - [LoggerMessage(1, LogLevel.Debug, "Rate limits exceeded, rejecting this request", EventName = "RequestRejectedLimitsExceeded")] + [LoggerMessage(1, LogLevel.Debug, "Rate limits exceeded, rejecting this request.", EventName = "RequestRejectedLimitsExceeded")] internal static partial void RequestRejectedLimitsExceeded(ILogger logger); } } From 0b9875bd08c0df77ad53186690a4d7e52826790c Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Tue, 19 Apr 2022 14:35:57 -0700 Subject: [PATCH 28/30] Partially fix test --- src/Middleware/RateLimiting/src/RateLimiterOptions.cs | 2 -- .../RateLimitingApplicationBuilderExtensionsTests.cs | 9 +++++++-- .../RateLimiting/test/RateLimitingOptionsTests.cs | 7 ------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Middleware/RateLimiting/src/RateLimiterOptions.cs b/src/Middleware/RateLimiting/src/RateLimiterOptions.cs index abc585939724..bc1ab30b7a37 100644 --- a/src/Middleware/RateLimiting/src/RateLimiterOptions.cs +++ b/src/Middleware/RateLimiting/src/RateLimiterOptions.cs @@ -1,7 +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.Net; using System.Threading.RateLimiting; using Microsoft.AspNetCore.Http; @@ -18,7 +17,6 @@ public sealed class RateLimiterOptions { return Task.CompletedTask; }; - private int _defaultOnRejectionStatusCode = (int)HttpStatusCode.ServiceUnavailable; /// /// Gets or sets the diff --git a/src/Middleware/RateLimiting/test/RateLimitingApplicationBuilderExtensionsTests.cs b/src/Middleware/RateLimiting/test/RateLimitingApplicationBuilderExtensionsTests.cs index 8a7234dd1931..474a020ee67b 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingApplicationBuilderExtensionsTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingApplicationBuilderExtensionsTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.RateLimiting; @@ -21,11 +22,13 @@ public void UseRateLimiter_RespectsOptions() // These are the options that should get used var options = new RateLimiterOptions(); options.DefaultRejectionStatusCode = 429; + options.Limiter = new TestPartitionedRateLimiter(new TestRateLimiter(false)); // These should not get used var services = new ServiceCollection(); services.Configure(options => { + options.Limiter = new TestPartitionedRateLimiter(new TestRateLimiter(false)); options.DefaultRejectionStatusCode = 404; }); var serviceProvider = services.BuildServiceProvider(); @@ -33,7 +36,9 @@ public void UseRateLimiter_RespectsOptions() // Act appBuilder.UseRateLimiter(options); - var configuredOptions = appBuilder.ApplicationServices.GetRequiredService(); - Assert.Equal(429, configuredOptions.DefaultRejectionStatusCode); + var app = appBuilder.Build(); + var context = new DefaultHttpContext(); + app.Invoke(context); + Assert.Equal(429, context.Response.StatusCode); } } diff --git a/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs b/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs index 92033b6b9058..839a9d2da08a 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingOptionsTests.cs @@ -20,11 +20,4 @@ public void ThrowsOnNullOnRejected() var options = new RateLimiterOptions(); Assert.Throws(() => options.OnRejected = null); } - - [Fact] - public void ThrowsOnInvalidStatusCode() - { - var options = new RateLimiterOptions(); - Assert.Throws(() => options.DefaultRejectionStatusCode = -1); - } } From 9d992ca1c740c0cd6c8edaade5d1a876ec401e37 Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Tue, 19 Apr 2022 14:40:49 -0700 Subject: [PATCH 29/30] Fix/Add tests --- .../RateLimitingApplicationBuilderExtensionsTests.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Middleware/RateLimiting/test/RateLimitingApplicationBuilderExtensionsTests.cs b/src/Middleware/RateLimiting/test/RateLimitingApplicationBuilderExtensionsTests.cs index 474a020ee67b..37d6d5bc4c37 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingApplicationBuilderExtensionsTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingApplicationBuilderExtensionsTests.cs @@ -3,12 +3,20 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Testing; using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.RateLimiting; -public class RateLimitingApplicationBuilderExtensionsTests +public class RateLimitingApplicationBuilderExtensionsTests : LoggedTest { + + [Fact] + public void UseRateLimiter_ThrowsOnNullAppBuilder() + { + Assert.Throws(() => RateLimitingApplicationBuilderExtensions.UseRateLimiter(null)); + } + [Fact] public void UseRateLimiter_ThrowsOnNullOptions() { @@ -31,6 +39,7 @@ public void UseRateLimiter_RespectsOptions() options.Limiter = new TestPartitionedRateLimiter(new TestRateLimiter(false)); options.DefaultRejectionStatusCode = 404; }); + services.AddLogging(); var serviceProvider = services.BuildServiceProvider(); var appBuilder = new ApplicationBuilder(serviceProvider); From b90f53404fd6bc607691b6a81a719db09bbe0703 Mon Sep 17 00:00:00 2001 From: Will Godbe Date: Tue, 19 Apr 2022 14:52:26 -0700 Subject: [PATCH 30/30] Add another test --- .../test/RateLimitingMiddlewareTests.cs | 18 ++++++++++++++++++ .../RateLimiting/test/TestRateLimiter.cs | 1 + 2 files changed, 19 insertions(+) diff --git a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs index 9d7420cf8a89..903b225ed41b 100644 --- a/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs +++ b/src/Middleware/RateLimiting/test/RateLimitingMiddlewareTests.cs @@ -99,6 +99,24 @@ public async Task RequestRejected_WinsOverDefaultStatusCode() Assert.Equal(StatusCodes.Status429TooManyRequests, context.Response.StatusCode); } + [Fact] + public async Task RequestAborted_ThrowsTaskCanceledException() + { + var options = CreateOptionsAccessor(); + options.Value.Limiter = new TestPartitionedRateLimiter(new TestRateLimiter(false)); + + var middleware = new RateLimitingMiddleware(c => + { + return Task.CompletedTask; + }, + new NullLoggerFactory().CreateLogger(), + options); + + var context = new DefaultHttpContext(); + context.RequestAborted = new CancellationToken(true); + await Assert.ThrowsAsync(() => middleware.Invoke(context)).DefaultTimeout(); + } + private IOptions CreateOptionsAccessor() => Options.Create(new RateLimiterOptions()); } diff --git a/src/Middleware/RateLimiting/test/TestRateLimiter.cs b/src/Middleware/RateLimiting/test/TestRateLimiter.cs index fa8e45e17bde..037b2fa5250b 100644 --- a/src/Middleware/RateLimiting/test/TestRateLimiter.cs +++ b/src/Middleware/RateLimiting/test/TestRateLimiter.cs @@ -27,6 +27,7 @@ protected override RateLimitLease AcquireCore(int permitCount) protected override ValueTask WaitAsyncCore(int permitCount, CancellationToken cancellationToken) { + cancellationToken.ThrowIfCancellationRequested(); return new ValueTask(new TestRateLimitLease(_alwaysAccept, null)); } }