-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Make RateLimitingMiddleware endpoint aware #42417
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 18 commits
ef9b7c4
df78df1
6a4c91c
982cdfb
12ce9d6
a38aee3
858e7d5
54ef30e
b608c2d
e42912c
c15482c
ee4f648
e0a8c70
7dce149
3cc73cf
ec829e6
2fe189a
4e7c3a3
0cdf1fd
6389bc2
0cbbf35
9194a94
8d15768
acb204d
aeaac56
3fb235d
6755538
59839bd
23b1cc9
0664773
5ebe00b
bf4f7ae
e0a277f
1373e87
286b827
17c496d
10173ec
a90c2f2
e1871ee
3c3115b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
// 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; | ||
internal sealed class AspNetKey<TKey>: AspNetKey | ||
{ | ||
private readonly TKey _key; | ||
|
||
public AspNetKey(TKey key) | ||
{ | ||
_key = key; | ||
} | ||
|
||
public override object? GetKey() | ||
{ | ||
return _key; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
internal abstract class AspNetKey | ||
{ | ||
public abstract object? GetKey(); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Diagnostics.CodeAnalysis; | ||
|
||
namespace Microsoft.AspNetCore.RateLimiting; | ||
internal class AspNetKeyEqualityComparer : IEqualityComparer<AspNetKey> | ||
{ | ||
public bool Equals(AspNetKey? x, AspNetKey? y) | ||
{ | ||
if (x == null && y == null) | ||
{ | ||
return true; | ||
} | ||
else if (x == null || y == null) | ||
{ | ||
return false; | ||
} | ||
|
||
var xKey = x.GetKey(); | ||
var yKey = y.GetKey(); | ||
if (xKey == null && yKey == null) | ||
{ | ||
return true; | ||
} | ||
else if (xKey == null || yKey == null) | ||
{ | ||
return false; | ||
} | ||
|
||
return xKey.Equals(yKey); | ||
} | ||
|
||
public int GetHashCode([DisallowNull] AspNetKey obj) | ||
{ | ||
var key = obj.GetKey(); | ||
if (key is not null) | ||
{ | ||
return key.GetHashCode(); | ||
} | ||
// REVIEW - is this reasonable? | ||
return default; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
// 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 sealed class AspNetLease : RateLimitLease | ||
{ | ||
private readonly RateLimitLease? _globalLease; | ||
private readonly RateLimitLease _endpointLease; | ||
private HashSet<string>? _metadataNames; | ||
|
||
public AspNetLease(RateLimitLease? globalLease, RateLimitLease endpointLease) | ||
{ | ||
_globalLease = globalLease; | ||
_endpointLease = endpointLease; | ||
} | ||
|
||
public override bool IsAcquired => true; | ||
|
||
public override IEnumerable<string> MetadataNames | ||
{ | ||
get | ||
{ | ||
if (_metadataNames is null) | ||
{ | ||
_metadataNames = new HashSet<string>(); | ||
if (_globalLease is not null) | ||
{ | ||
foreach (string metadataName in _globalLease.MetadataNames) | ||
{ | ||
_metadataNames.Add(metadataName); | ||
} | ||
} | ||
foreach (string metadataName in _endpointLease.MetadataNames) | ||
{ | ||
_metadataNames.Add(metadataName); | ||
} | ||
} | ||
return _metadataNames; | ||
} | ||
} | ||
|
||
public override bool TryGetMetadata(string metadataName, out object? metadata) | ||
{ | ||
if (_globalLease is not null) | ||
{ | ||
// Use the first metadata item of a given name, ignore duplicates, we can't reliably merge arbitrary metadata | ||
// Creating an object[] if there are multiple of the same metadataName could work, but makes consumption of metadata messy | ||
// and makes MetadataName.Create<T>(...) uses no longer work | ||
if (_globalLease.TryGetMetadata(metadataName, out metadata)) | ||
{ | ||
return true; | ||
} | ||
} | ||
if (_endpointLease.TryGetMetadata(metadataName, out metadata)) | ||
{ | ||
return true; | ||
} | ||
|
||
metadata = null; | ||
return false; | ||
} | ||
|
||
protected override void Dispose(bool disposing) | ||
{ | ||
List<Exception>? exceptions = null; | ||
// Dispose endpoint lease first, then global lease (reverse order of when they were acquired) | ||
// Avoids issues where dispose might unblock a queued acquire and then the acquire fails when acquiring the next limiter. | ||
// When disposing in reverse order there wont be any issues of unblocking an acquire that affects acquires on limiters in the chain after it | ||
|
||
try | ||
{ | ||
_endpointLease.Dispose(); | ||
} | ||
catch (Exception ex) | ||
{ | ||
exceptions ??= new List<Exception>(); | ||
exceptions.Add(ex); | ||
} | ||
|
||
if (_globalLease is not null) | ||
{ | ||
try | ||
{ | ||
_globalLease.Dispose(); | ||
} | ||
catch (Exception ex) | ||
{ | ||
exceptions ??= new List<Exception>(); | ||
exceptions.Add(ex); | ||
} | ||
} | ||
|
||
if (exceptions is not null) | ||
{ | ||
throw new AggregateException(exceptions); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
|
||
using System.Threading.RateLimiting; | ||
using Microsoft.AspNetCore.Http; | ||
|
||
namespace Microsoft.AspNetCore.RateLimiting; | ||
internal sealed class AspNetPolicy : IRateLimiterPolicy<AspNetKey> | ||
{ | ||
private readonly Func<HttpContext, RateLimitPartition<AspNetKey>> _partitioner; | ||
private readonly Func<OnRejectedContext, CancellationToken, ValueTask>? _onRejected; | ||
|
||
public AspNetPolicy(Func<HttpContext, RateLimitPartition<AspNetKey>> partitioner, Func<OnRejectedContext, CancellationToken, ValueTask>? onRejected) | ||
{ | ||
_partitioner = partitioner; | ||
_onRejected = onRejected; | ||
} | ||
|
||
public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected => _onRejected; | ||
|
||
public RateLimitPartition<AspNetKey> GetPartition(HttpContext httpContext) | ||
{ | ||
return _partitioner(httpContext); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// 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; | ||
|
||
/// <summary> | ||
/// An interface which can be used to identify a type which provides metadata needed for enabling request rate limiting support. | ||
/// </summary> | ||
internal interface IRateLimiterMetadata | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The API proposal for the current implementation didn't consider this, though I think it's a good argument for making these public in the next iteration for preview7 (CC @BrennanConroy, @halter73) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We talked about this during preview6 API discussions and decided to leave it out for preview6 and talk about making it public in preview7. |
||
{ | ||
/// <summary> | ||
/// The name of the limiter which needs to be applied. | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// </summary> | ||
string Name { get; } | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 System.Threading.RateLimiting; | ||
using Microsoft.AspNetCore.Http; | ||
|
||
namespace Microsoft.AspNetCore.RateLimiting; | ||
|
||
/// <summary> | ||
/// An interface which is used to represent a RateLimiter policy. | ||
/// </summary> | ||
public interface IRateLimiterPolicy<TPartitionKey> | ||
{ | ||
/// <summary> | ||
/// Gets the <see cref="Func{OnRejectedContext, CancellationToken, ValueTask}"/> that handles requests rejected by this middleware. | ||
/// </summary> | ||
public Func<OnRejectedContext, CancellationToken, ValueTask>? OnRejected { get; } | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// <summary> | ||
/// Gets the <see cref="RateLimitPartition{TPartitionKey}"/> that applies to the given <see cref="HttpContext"/>. | ||
/// </summary> | ||
/// <param name="httpContext">The <see cref="HttpContext"/> to get the partition for.</param> | ||
public RateLimitPartition<TPartitionKey> GetPartition(HttpContext httpContext); | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// 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 struct LeaseContext : IDisposable | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know enough about the subject but does making this struct to readonly ref improve anything!? |
||
{ | ||
public Rejector? Rejector { get; init; } | ||
|
||
public required RateLimitLease Lease { get; init; } | ||
|
||
public void Dispose() | ||
{ | ||
Lease.Dispose(); | ||
} | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
|
||
using System.Threading.RateLimiting; | ||
using Microsoft.AspNetCore.Http; | ||
|
||
namespace Microsoft.AspNetCore.RateLimiting; | ||
|
||
/// <summary> | ||
/// Holds state needed for the OnRejected callback in the RateLimitingMiddleware | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// </summary> | ||
public sealed class OnRejectedContext | ||
{ | ||
/// <summary> | ||
/// Gets or sets the <see cref="HttpContext"/> that the OnRejected callback will have access to | ||
/// </summary> | ||
public required HttpContext HttpContext { get; init; } | ||
|
||
/// <summary> | ||
/// Gets or sets the <see cref="RateLimitLease"/> that the OnRejected callback will have access to | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// </summary> | ||
public required RateLimitLease Lease { get; init; } | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// 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; | ||
internal class PolicyNameKey | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
public required string PolicyName { get; init; } | ||
|
||
public override bool Equals(object? obj) | ||
{ | ||
if (obj is PolicyNameKey key) | ||
{ | ||
return PolicyName.Equals(key.PolicyName); | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
return false; | ||
} | ||
|
||
public override int GetHashCode() | ||
{ | ||
return PolicyName.GetHashCode(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
|
||
using System.Diagnostics.CodeAnalysis; | ||
|
||
namespace Microsoft.AspNetCore.RateLimiting; | ||
internal class PolicyTypeInfo | ||
wtgodbe marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] | ||
public required Type PolicyType { get; init; } | ||
public required Type PartitionKeyType { get; init; } | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is to allow for the
required
keyword I use in many places in this PR