Skip to content

Implement minimal RateLimitingMiddleware #41008

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 34 commits into from
Apr 19, 2022
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2721e4f
First RateLimiting commit
wtgodbe Feb 25, 2022
caa47a6
More
wtgodbe Feb 25, 2022
d649fca
ctrl-s
wtgodbe Feb 28, 2022
6825683
lil bit
wtgodbe Mar 11, 2022
f7e82a5
Small
wtgodbe Mar 25, 2022
c5f2d94
Check-in launchSettings.json files from Middleware (#40695)
wtgodbe Mar 14, 2022
7782861
Merge main
wtgodbe Mar 30, 2022
2c683fc
sln
wtgodbe Mar 30, 2022
301a4ee
More
wtgodbe Mar 31, 2022
8b1e074
More
wtgodbe Mar 31, 2022
7870d62
More
wtgodbe Apr 1, 2022
16bdcf3
Remove stuff
wtgodbe Apr 1, 2022
4f20092
rm
wtgodbe Apr 1, 2022
a417766
Not SharedFx
wtgodbe Apr 1, 2022
0c43a25
Feedback
wtgodbe Apr 1, 2022
017797d
Internal+IVT
wtgodbe Apr 1, 2022
2b406b0
Feedback
wtgodbe Apr 1, 2022
75aa2cf
Feedback chunk 1
wtgodbe Apr 5, 2022
ade42e0
Feedback chunk 2
wtgodbe Apr 5, 2022
d025eb8
Small feedback
wtgodbe Apr 5, 2022
b7affd6
Merge
wtgodbe Apr 13, 2022
ddddbd4
Fix extension methods
wtgodbe Apr 13, 2022
9abcb07
Small fix
wtgodbe Apr 13, 2022
047ab3d
Func
wtgodbe Apr 13, 2022
cea2af6
Config status code
wtgodbe Apr 13, 2022
4761f1e
Merge remote-tracking branch 'upstream/main' into wtgodbe/RateLimidMi…
wtgodbe Apr 15, 2022
ac0ad26
Feedback, add servicecollection extension
wtgodbe Apr 15, 2022
fc92f12
Update API
wtgodbe Apr 19, 2022
43b16a6
Merge from main
wtgodbe Apr 19, 2022
9d3dffb
Some feedback
wtgodbe Apr 19, 2022
385d939
Lil more feedback
wtgodbe Apr 19, 2022
0b9875b
Partially fix test
wtgodbe Apr 19, 2022
9d992ca
Fix/Add tests
wtgodbe Apr 19, 2022
b90f534
Add another test
wtgodbe Apr 19, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions AspNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -1696,6 +1696,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuildAfterTargetingPack", "
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ResultsOfTGenerator", "src\Http\Http.Results\tools\ResultsOfTGenerator\ResultsOfTGenerator.csproj", "{9716D0D0-2251-44DD-8596-67D253EEF41C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RateLimiting", "src\Middleware\RateLimiting\src\Microsoft.AspNetCore.RateLimiting.csproj", "{9D5C9ED8-5FCD-416F-9977-D7D4D4DCF49D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RateLimiting.Tests", "src\Middleware\RateLimiting\test\Microsoft.AspNetCore.RateLimiting.Tests.csproj", "{E85FB5AE-49AD-4102-88D1-79A224863DF7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -10153,6 +10157,38 @@ Global
{9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|x64.Build.0 = Release|Any CPU
{9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|x86.ActiveCfg = Release|Any CPU
{9716D0D0-2251-44DD-8596-67D253EEF41C}.Release|x86.Build.0 = Release|Any CPU
{9D5C9ED8-5FCD-416F-9977-D7D4D4DCF49D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9D5C9ED8-5FCD-416F-9977-D7D4D4DCF49D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9D5C9ED8-5FCD-416F-9977-D7D4D4DCF49D}.Debug|arm64.ActiveCfg = Debug|Any CPU
{9D5C9ED8-5FCD-416F-9977-D7D4D4DCF49D}.Debug|arm64.Build.0 = Debug|Any CPU
{9D5C9ED8-5FCD-416F-9977-D7D4D4DCF49D}.Debug|x64.ActiveCfg = Debug|Any CPU
{9D5C9ED8-5FCD-416F-9977-D7D4D4DCF49D}.Debug|x64.Build.0 = Debug|Any CPU
{9D5C9ED8-5FCD-416F-9977-D7D4D4DCF49D}.Debug|x86.ActiveCfg = Debug|Any CPU
{9D5C9ED8-5FCD-416F-9977-D7D4D4DCF49D}.Debug|x86.Build.0 = Debug|Any CPU
{9D5C9ED8-5FCD-416F-9977-D7D4D4DCF49D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9D5C9ED8-5FCD-416F-9977-D7D4D4DCF49D}.Release|Any CPU.Build.0 = Release|Any CPU
{9D5C9ED8-5FCD-416F-9977-D7D4D4DCF49D}.Release|arm64.ActiveCfg = Release|Any CPU
{9D5C9ED8-5FCD-416F-9977-D7D4D4DCF49D}.Release|arm64.Build.0 = Release|Any CPU
{9D5C9ED8-5FCD-416F-9977-D7D4D4DCF49D}.Release|x64.ActiveCfg = Release|Any CPU
{9D5C9ED8-5FCD-416F-9977-D7D4D4DCF49D}.Release|x64.Build.0 = Release|Any CPU
{9D5C9ED8-5FCD-416F-9977-D7D4D4DCF49D}.Release|x86.ActiveCfg = Release|Any CPU
{9D5C9ED8-5FCD-416F-9977-D7D4D4DCF49D}.Release|x86.Build.0 = Release|Any CPU
{E85FB5AE-49AD-4102-88D1-79A224863DF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E85FB5AE-49AD-4102-88D1-79A224863DF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E85FB5AE-49AD-4102-88D1-79A224863DF7}.Debug|arm64.ActiveCfg = Debug|Any CPU
{E85FB5AE-49AD-4102-88D1-79A224863DF7}.Debug|arm64.Build.0 = Debug|Any CPU
{E85FB5AE-49AD-4102-88D1-79A224863DF7}.Debug|x64.ActiveCfg = Debug|Any CPU
{E85FB5AE-49AD-4102-88D1-79A224863DF7}.Debug|x64.Build.0 = Debug|Any CPU
{E85FB5AE-49AD-4102-88D1-79A224863DF7}.Debug|x86.ActiveCfg = Debug|Any CPU
{E85FB5AE-49AD-4102-88D1-79A224863DF7}.Debug|x86.Build.0 = Debug|Any CPU
{E85FB5AE-49AD-4102-88D1-79A224863DF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E85FB5AE-49AD-4102-88D1-79A224863DF7}.Release|Any CPU.Build.0 = Release|Any CPU
{E85FB5AE-49AD-4102-88D1-79A224863DF7}.Release|arm64.ActiveCfg = Release|Any CPU
{E85FB5AE-49AD-4102-88D1-79A224863DF7}.Release|arm64.Build.0 = Release|Any CPU
{E85FB5AE-49AD-4102-88D1-79A224863DF7}.Release|x64.ActiveCfg = Release|Any CPU
{E85FB5AE-49AD-4102-88D1-79A224863DF7}.Release|x64.Build.0 = Release|Any CPU
{E85FB5AE-49AD-4102-88D1-79A224863DF7}.Release|x86.ActiveCfg = Release|Any CPU
{E85FB5AE-49AD-4102-88D1-79A224863DF7}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
1 change: 1 addition & 0 deletions eng/ProjectReferences.props
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Localization.Routing" ProjectPath="$(RepoRoot)src\Middleware\Localization.Routing\src\Microsoft.AspNetCore.Localization.Routing.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Localization" ProjectPath="$(RepoRoot)src\Middleware\Localization\src\Microsoft.AspNetCore.Localization.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.MiddlewareAnalysis" ProjectPath="$(RepoRoot)src\Middleware\MiddlewareAnalysis\src\Microsoft.AspNetCore.MiddlewareAnalysis.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.RateLimiting" ProjectPath="$(RepoRoot)src\Middleware\RateLimiting\src\Microsoft.AspNetCore.RateLimiting.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.ResponseCaching.Abstractions" ProjectPath="$(RepoRoot)src\Middleware\ResponseCaching.Abstractions\src\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.ResponseCaching" ProjectPath="$(RepoRoot)src\Middleware\ResponseCaching\src\Microsoft.AspNetCore.ResponseCaching.csproj" />
<ProjectReferenceProvider Include="Microsoft.AspNetCore.ResponseCompression" ProjectPath="$(RepoRoot)src\Middleware\ResponseCompression\src\Microsoft.AspNetCore.ResponseCompression.csproj" />
Expand Down
4 changes: 3 additions & 1 deletion src/Middleware/Middleware.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -115,4 +117,4 @@
"src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj"
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>ASP.NET Core middleware for enforcing rate limiting in an application</Description>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageTags>aspnetcore</PackageTags>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
<Reference Include="Microsoft.Extensions.Logging.Abstractions" />
<Reference Include="Microsoft.Extensions.Options" />
<Reference Include="System.Threading.RateLimiting" />

<Compile Include="$(SharedSourceRoot)ValueStopwatch\*.cs" />
</ItemGroup>

</Project>
36 changes: 36 additions & 0 deletions src/Middleware/RateLimiting/src/NoLimiter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Threading.RateLimiting;

namespace Microsoft.AspNetCore.RateLimiting;
internal class NoLimiter<TResource> : PartitionedRateLimiter<TResource>
{
public override int GetAvailablePermits(TResource resourceID)
{
return 1;
}

protected override RateLimitLease AcquireCore(TResource resourceID, int permitCount)
{
return new NoLimiterLease();
}

protected override ValueTask<RateLimitLease> WaitAsyncCore(TResource resourceID, int permitCount, CancellationToken cancellationToken)
{
return new ValueTask<RateLimitLease>(new NoLimiterLease());
}
}

internal class NoLimiterLease : RateLimitLease
{
public override bool IsAcquired => true;

public override IEnumerable<string> MetadataNames => new List<string>();

public override bool TryGetMetadata(string metadataName, out object? metadata)
{
metadata = null;
return false;
}
}
6 changes: 6 additions & 0 deletions src/Middleware/RateLimiting/src/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Microsoft.AspNetCore.RateLimiting.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
1 change: 1 addition & 0 deletions src/Middleware/RateLimiting/src/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
11 changes: 11 additions & 0 deletions src/Middleware/RateLimiting/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Microsoft.AspNetCore.RateLimiting.RateLimitingExtensions
Microsoft.AspNetCore.RateLimiting.RateLimitingOptions
Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.get -> System.Threading.RateLimiting.PartitionedRateLimiter<Microsoft.AspNetCore.Http.HttpContext!>!
Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.Limiter.set -> void
Microsoft.AspNetCore.RateLimiting.RateLimitingOptions.OnRejected.get -> System.Func<Microsoft.AspNetCore.Http.HttpContext!, System.Threading.RateLimiting.RateLimitLease!, System.Threading.Tasks.Task!>!
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!
48 changes: 48 additions & 0 deletions src/Middleware/RateLimiting/src/RateLimitingExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.RateLimiting;

/// <summary>
/// Extension methods for the RateLimiting middleware.
/// </summary>
public static class RateLimitingExtensions
{
/// <summary>
/// Enables rate limiting for the application.
/// </summary>
/// <param name="app"></param>
/// <returns></returns>
public static IApplicationBuilder UseRateLimiting(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}

return app.UseMiddleware<RateLimitingMiddleware>();
}

/// <summary>
/// Enables rate limiting for the application with the given options.
/// </summary>
/// <param name="app"></param>
/// <param name="options"></param>
/// <returns></returns>
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<RateLimitingMiddleware>(Options.Create(options));
}
}
81 changes: 81 additions & 0 deletions src/Middleware/RateLimiting/src/RateLimitingMiddleware.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.RateLimiting;

/// <summary>
/// Limits the rate of requests allowed in the application, based on limits set by a user-provided <see cref="PartitionedRateLimiter{TResource}"/>.
/// </summary>
internal sealed partial class RateLimitingMiddleware
{
private readonly RequestDelegate _next;
private readonly Func<HttpContext, RateLimitLease, Task> _onRejected;
private readonly ILogger _logger;
private readonly PartitionedRateLimiter<HttpContext> _limiter;
private readonly HttpStatusCode _rejectionStatusCode;

/// <summary>
/// Creates a new <see cref="RateLimitingMiddleware"/>.
/// </summary>
/// <param name="next">The <see cref="RequestDelegate"/> representing the next middleware in the pipeline.</param>
/// <param name="logger">The <see cref="ILogger"/> used for logging.</param>
/// <param name="options">The options for the middleware.</param>
public RateLimitingMiddleware(RequestDelegate next, ILogger<RateLimitingMiddleware> logger, IOptions<RateLimitingOptions> options)
{
_next = next ?? throw new ArgumentNullException(nameof(next));

if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}

_logger = logger;
_limiter = options.Value.Limiter;
_onRejected = options.Value.OnRejected;
_rejectionStatusCode = options.Value.RejectionStatusCode;
}

// TODO - EventSource?
Copy link
Member Author

Choose a reason for hiding this comment

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

Could be valuable to add EventSource logging to this, part of the next pass?

Copy link
Member

Choose a reason for hiding this comment

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

I'm fine with waiting for the next pass. It might be better to add EventSource logging to the rate limiter implemenations, but that might be too low level to be useful if there are many.

Copy link
Member

Choose a reason for hiding this comment

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

Is there an issue tracking this yet?

Copy link
Member Author

Choose a reason for hiding this comment

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

/// <summary>
/// Invokes the logic of the middleware.
/// </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
/// <returns>A <see cref="Task"/> that completes when the request leaves.</returns>
public async Task Invoke(HttpContext context)
{
using var lease = await TryAcquireAsync(context);
if (lease.IsAcquired)
{
await _next(context);
}
else
{
RateLimiterLog.RequestRejectedLimitsExceeded(_logger);
context.Response.StatusCode = (int)_rejectionStatusCode;
await _onRejected(context, lease);
}
}

private ValueTask<RateLimitLease> TryAcquireAsync(HttpContext context)
{
var lease = _limiter.Acquire(context);
if (lease.IsAcquired)
{
return ValueTask.FromResult(lease);
}

return _limiter.WaitAsync(context, cancellationToken: context.RequestAborted);
}

private static partial class RateLimiterLog
{
[LoggerMessage(1, LogLevel.Debug, "Rate limits exceeded, rejecting this request with a '503 server not available' error", EventName = "RequestRejectedLimitsExceeded")]
internal static partial void RequestRejectedLimitsExceeded(ILogger logger);
}
}
45 changes: 45 additions & 0 deletions src/Middleware/RateLimiting/src/RateLimitingOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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;

namespace Microsoft.AspNetCore.RateLimiting;

/// <summary>
/// Specifies options for the rate limiting middleware.
/// </summary>
public sealed class RateLimitingOptions
{
// TODO - Provide a default?
private PartitionedRateLimiter<HttpContext> _limiter = new NoLimiter<HttpContext>();
private Func<HttpContext, RateLimitLease, Task> _onRejected = (context, lease) =>
{
return Task.CompletedTask;
};

/// <summary>
/// Gets or sets the <see cref="PartitionedRateLimiter{TResource}"/>
/// </summary>
public PartitionedRateLimiter<HttpContext> Limiter
{
get => _limiter;
set => _limiter = value ?? throw new ArgumentNullException(nameof(value));
}

/// <summary>
/// Gets or sets a <see cref="Func{HttpContext, RateLimitLease, Task}"/> that handles requests rejected by this middleware.
/// </summary>
public Func<HttpContext, RateLimitLease, Task> OnRejected
{
get => _onRejected;
set => _onRejected = value ?? throw new ArgumentNullException(nameof(value));
}

/// <summary>
/// Gets or sets the <see cref="HttpStatusCode"/> to set on the response when a request is rejected.
/// Defaults to <see cref="HttpStatusCode.ServiceUnavailable"/>.
/// </summary>
public HttpStatusCode RejectionStatusCode { get; set; } = HttpStatusCode.ServiceUnavailable;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Http" />
<Reference Include="Microsoft.AspNetCore.RateLimiting" />
</ItemGroup>
</Project>
Loading