├── src └── ThrottlR │ ├── Endpoints │ ├── IDisableThrottle.cs │ ├── IThrottleMetadata.cs │ ├── IThrottleRulesMetadata.cs │ ├── DisableThrottleAttribute.cs │ ├── DisableThrottle.cs │ ├── EnableThrottle.cs │ ├── EnableThrottleAttribute.cs │ ├── ThrottleAttribute.cs │ └── ThrottlerEndpointConventionBuilderExtensions.cs │ ├── Service │ ├── ISystemClock.cs │ ├── SystemClock.cs │ ├── IThrottlerService.cs │ ├── Counter.cs │ ├── Store │ │ ├── ICounterStore.cs │ │ ├── InMemoryCacheCounterStore.cs │ │ └── DistributedCacheCounterStore.cs │ ├── ThrottlerItem.cs │ └── ThrottlerService.cs │ ├── Policy │ ├── Resolvers │ │ ├── ISafeListResolver.cs │ │ ├── IResolver.cs │ │ ├── NoResolver.cs │ │ ├── AccessTokenResolver.cs │ │ ├── TypeResolver.cs │ │ ├── UsernameResolver.cs │ │ ├── HostResolver.cs │ │ └── IpResolver.cs │ ├── IThrottlePolicyProvider.cs │ ├── QuotaExceededDelegate.cs │ ├── ThrottleRule.cs │ ├── SafeListBuilder.cs │ ├── SafeListBuilderExtensions.cs │ ├── ThrottlePolicy.cs │ ├── SafeListCollection.cs │ ├── DefaultThrottlePolicyProvider.cs │ ├── ThrottlePolicyBuilderExtensions.cs │ ├── ThrottlerOptions.cs │ └── ThrottlePolicyBuilder.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── Middleware │ ├── ThrottlerApplicationBuilderExtensions.cs │ └── ThrottlerMiddleware.cs │ ├── DependencyInjection │ ├── IThrottlerBuilder.cs │ ├── ThrottlerBuilder.cs │ └── ThrottlerServiceCollectionExtensions.cs │ ├── ThrottlR.csproj │ └── Internal │ ├── SubnetMask.cs │ ├── IPAddressExtensions.cs │ └── AsyncKeyLock │ ├── AsyncKeyLock.cs │ └── Doorman.cs ├── test └── ThrottlR.Tests │ ├── AspNetCoreRateLimit.Tests.csproj │ ├── Store │ ├── InMemoryCacheCounterTests.cs │ ├── DistributedCacheCounterStoreTests.cs │ └── CounterStoreTests.cs │ ├── Resolvers │ ├── NoResolverTests.cs │ ├── IpResolverTests.cs │ └── UsernameResolverTests.cs │ ├── Common │ ├── TimeMachine.cs │ └── TestCounterStore.cs │ ├── ThrottlR.Tests.csproj │ ├── Policy │ └── SafeListCollectionTests.cs │ ├── AsyncKeyLock │ └── AsyncKeyLockTests.cs │ ├── Endpoints │ ├── ThrottleAttributeTests.cs │ └── ThrottlerEndpointConventionBuilderExtensionsTests.cs │ ├── ThrottlerServiceTests.cs │ └── ThrottlerMiddlewareTests.cs ├── sample └── MVC │ ├── appsettings.json │ ├── MVC.csproj │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── ApiController.cs │ └── Startup.cs ├── .gitignore ├── benchmark └── ThrottlR.Benchmark │ ├── ThrottlR.Benchmark.csproj │ └── Program.cs ├── .github └── workflows │ └── dotnet-core.yml ├── LICENSE.md ├── .gitattributes ├── ThrottlR.sln ├── README.md └── .editorconfig /src/ThrottlR/Endpoints/IDisableThrottle.cs: -------------------------------------------------------------------------------- 1 | namespace ThrottlR 2 | { 3 | public interface IDisableThrottle 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/ThrottlR/Endpoints/IThrottleMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace ThrottlR 2 | { 3 | public interface IThrottleMetadata 4 | { 5 | string PolicyName { get; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/ThrottlR/Service/ISystemClock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ThrottlR 4 | { 5 | public interface ISystemClock 6 | { 7 | DateTime UtcNow { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/ThrottlR/Policy/Resolvers/ISafeListResolver.cs: -------------------------------------------------------------------------------- 1 | namespace ThrottlR 2 | { 3 | public interface ISafeListResolver : IResolver 4 | { 5 | bool Matches(string scope, string safe); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/ThrottlR/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: InternalsVisibleTo("ThrottlR.Tests")] 6 | -------------------------------------------------------------------------------- /test/ThrottlR.Tests/AspNetCoreRateLimit.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /sample/MVC/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /src/ThrottlR/Policy/IThrottlePolicyProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace ThrottlR 4 | { 5 | public interface IThrottlePolicyProvider 6 | { 7 | Task GetPolicyAsync(string policyName); 8 | 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/ThrottlR/Endpoints/IThrottleRulesMetadata.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ThrottlR 4 | { 5 | public interface IThrottleRulesMetadata : IThrottleMetadata 6 | { 7 | IReadOnlyList GeneralRules { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/ThrottlR/Policy/Resolvers/IResolver.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace ThrottlR 5 | { 6 | public interface IResolver 7 | { 8 | ValueTask ResolveAsync(HttpContext httpContext); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/ThrottlR/Endpoints/DisableThrottleAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ThrottlR 4 | { 5 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] 6 | public class DisableThrottleAttribute : Attribute, IDisableThrottle 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /sample/MVC/MVC.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/ThrottlR/Endpoints/DisableThrottle.cs: -------------------------------------------------------------------------------- 1 | namespace ThrottlR 2 | { 3 | public class DisableThrottle : IDisableThrottle 4 | { 5 | public static IDisableThrottle Instance { get; } = new DisableThrottle(); 6 | 7 | private DisableThrottle() 8 | { 9 | 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ThrottlR/Endpoints/EnableThrottle.cs: -------------------------------------------------------------------------------- 1 | namespace ThrottlR 2 | { 3 | public class EnableThrottle : IThrottleMetadata 4 | { 5 | public EnableThrottle() 6 | { 7 | 8 | } 9 | 10 | public EnableThrottle(string policyName) 11 | { 12 | PolicyName = policyName; 13 | } 14 | 15 | public string PolicyName { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ThrottlR/Middleware/ThrottlerApplicationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using ThrottlR; 2 | 3 | namespace Microsoft.AspNetCore.Builder 4 | { 5 | public static class ThrottlerApplicationBuilderExtensions 6 | { 7 | public static IApplicationBuilder UseThrottler(this IApplicationBuilder builder) 8 | { 9 | return builder.UseMiddleware(); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/ThrottlR/Service/SystemClock.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ThrottlR 4 | { 5 | /// 6 | /// Provides access to the normal system clock. 7 | /// 8 | internal class SystemClock : ISystemClock 9 | { 10 | /// 11 | /// Retrieves the current UTC system time. 12 | /// 13 | public DateTime UtcNow => DateTime.UtcNow; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ThrottlR/DependencyInjection/IThrottlerBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace ThrottlR 4 | { 5 | /// 6 | /// Reverse Proxy builder interface. 7 | /// 8 | public interface IThrottlerBuilder 9 | { 10 | /// 11 | /// Gets the services. 12 | /// 13 | IServiceCollection Services { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Oo]bj/ 2 | [Bb]in/ 3 | TestResults/ 4 | .nuget/ 5 | _ReSharper.*/ 6 | packages/ 7 | artifacts/ 8 | PublishProfiles/ 9 | *.user 10 | *.suo 11 | *.cache 12 | *.docstates 13 | _ReSharper.* 14 | nuget.exe 15 | *net45.csproj 16 | *k10.csproj 17 | *.psess 18 | *.vsp 19 | *.pidb 20 | *.userprefs 21 | *DS_Store 22 | *.ncrunchsolution 23 | *.*sdf 24 | *.ipch 25 | .vs/ 26 | project.lock.json 27 | 28 | bower_components/ 29 | node_modules/ 30 | **/wwwroot/lib -------------------------------------------------------------------------------- /test/ThrottlR.Tests/Store/InMemoryCacheCounterTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Caching.Memory; 2 | using Microsoft.Extensions.Options; 3 | 4 | namespace ThrottlR 5 | { 6 | public class InMemoryCacheCounterTests : CounterStoreTests 7 | { 8 | public override ICounterStore CreateCounterStore() 9 | { 10 | return new InMemoryCacheCounterStore(new MemoryCache(Options.Create(new MemoryCacheOptions()))); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /benchmark/ThrottlR.Benchmark/ThrottlR.Benchmark.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp3.1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/ThrottlR/Service/IThrottlerService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace ThrottlR 6 | { 7 | public interface IThrottlerService 8 | { 9 | IEnumerable GetRules(IReadOnlyList generalRules, IReadOnlyList specificRules); 10 | 11 | Task ProcessRequestAsync(ThrottlerItem throttlerItem, ThrottleRule rule, CancellationToken cancellationToken); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ThrottlR/Policy/Resolvers/NoResolver.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace ThrottlR 5 | { 6 | public class NoResolver : IResolver 7 | { 8 | private const string Identity = "*"; 9 | 10 | public static NoResolver Instance { get; } = new NoResolver(); 11 | 12 | public ValueTask ResolveAsync(HttpContext httpContext) 13 | { 14 | return new ValueTask(Identity); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ThrottlR/Service/Counter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ThrottlR 4 | { 5 | /// 6 | /// Stores the initial access time and the numbers of calls made from that point 7 | /// 8 | public readonly struct Counter 9 | { 10 | public Counter(DateTime timestamp, int count) 11 | { 12 | Timestamp = timestamp; 13 | Count = count; 14 | } 15 | 16 | public DateTime Timestamp { get; } 17 | 18 | public int Count { get; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ThrottlR/Policy/QuotaExceededDelegate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace ThrottlR 6 | { 7 | /// 8 | /// 9 | /// 10 | /// 11 | /// 12 | /// 13 | /// 14 | /// 15 | public delegate Task QuotaExceededDelegate(HttpContext context, ThrottleRule rule, DateTime retryAfter); 16 | } 17 | -------------------------------------------------------------------------------- /test/ThrottlR.Tests/Store/DistributedCacheCounterStoreTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Caching.Distributed; 2 | using Microsoft.Extensions.Caching.Memory; 3 | using Microsoft.Extensions.Options; 4 | 5 | namespace ThrottlR 6 | { 7 | public class DistributedCacheCounterStoreTests : CounterStoreTests 8 | { 9 | public override ICounterStore CreateCounterStore() 10 | { 11 | return new DistributedCacheCounterStore(new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions()))); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/ThrottlR/Endpoints/EnableThrottleAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ThrottlR 4 | { 5 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] 6 | public class EnableThrottleAttribute : Attribute, IThrottleMetadata 7 | { 8 | public EnableThrottleAttribute() 9 | { 10 | 11 | } 12 | 13 | public EnableThrottleAttribute(string policyName) 14 | { 15 | PolicyName = policyName; 16 | } 17 | 18 | public string PolicyName { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ThrottlR/Service/Store/ICounterStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace ThrottlR 6 | { 7 | public interface ICounterStore 8 | { 9 | ValueTask GetAsync(ThrottlerItem throttlerItem, CancellationToken cancellationToken); 10 | 11 | ValueTask RemoveAsync(ThrottlerItem throttlerItem, CancellationToken cancellationToken); 12 | 13 | ValueTask SetAsync(ThrottlerItem throttlerItem, Counter counter, TimeSpan? expirationTime, CancellationToken cancellationToken); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/ThrottlR.Tests/Resolvers/NoResolverTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | using Xunit; 4 | 5 | namespace ThrottlR.Resolvers 6 | { 7 | public class NoResolverTests 8 | { 9 | [Fact] 10 | public async Task NoResolver_Always_Returns_Star() 11 | { 12 | var resolver = new NoResolver(); 13 | 14 | var httpContext = new DefaultHttpContext(); 15 | var identity = await resolver.ResolveAsync(httpContext); 16 | 17 | Assert.Equal("*", identity); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sample/MVC/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace MVC 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | CreateHostBuilder(args).Build().Run(); 11 | } 12 | 13 | public static IHostBuilder CreateHostBuilder(string[] args) => 14 | Host.CreateDefaultBuilder(args) 15 | .ConfigureWebHostDefaults(webBuilder => 16 | { 17 | webBuilder.UseStartup(); 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-core.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: 4 | push: 5 | branches: [ develop ] 6 | pull_request: 7 | branches: [ develop ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET Core 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 3.1.101 20 | - name: Install dependencies 21 | run: dotnet restore 22 | - name: Build 23 | run: dotnet build --configuration Release --no-restore 24 | - name: Test 25 | run: dotnet test --no-restore --verbosity normal 26 | -------------------------------------------------------------------------------- /src/ThrottlR/Policy/ThrottleRule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ThrottlR 4 | { 5 | /// 6 | /// Limit the number of acceptable requests in a given time window. 7 | /// 8 | public class ThrottleRule 9 | { 10 | /// 11 | /// 12 | /// 13 | public TimeSpan TimeWindow { get; set; } 14 | 15 | /// 16 | /// 17 | /// 18 | public double Quota { get; set; } 19 | 20 | public override string ToString() 21 | { 22 | return $"{Quota};w={TimeWindow.TotalSeconds}"; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ThrottlR/Policy/Resolvers/AccessTokenResolver.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Authentication; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace ThrottlR 6 | { 7 | public class AccessTokenResolver : IResolver 8 | { 9 | private AccessTokenResolver() 10 | { 11 | 12 | } 13 | 14 | public static AccessTokenResolver Instance { get; } = new AccessTokenResolver(); 15 | 16 | public ValueTask ResolveAsync(HttpContext httpContext) 17 | { 18 | return new ValueTask(httpContext.GetTokenAsync("access_token")); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/ThrottlR.Tests/Common/TimeMachine.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ThrottlR.Tests 4 | { 5 | public class TimeMachine : ISystemClock 6 | { 7 | public TimeMachine() 8 | { 9 | UtcNow = DateTime.UtcNow; 10 | } 11 | 12 | public DateTime UtcNow { get; set; } 13 | 14 | public void Travel(long seconds) 15 | { 16 | UtcNow += TimeSpan.FromSeconds(seconds); 17 | } 18 | 19 | public void Travel(DateTime dateTime) 20 | { 21 | UtcNow = dateTime; 22 | } 23 | 24 | public void Travel(TimeSpan timeSpan) 25 | { 26 | UtcNow += timeSpan; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ThrottlR/Policy/Resolvers/TypeResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace ThrottlR 7 | { 8 | public class TypeResolver : IResolver where TResolver : IResolver 9 | { 10 | private readonly IServiceProvider _serviceProvider; 11 | 12 | public TypeResolver(IServiceProvider serviceProvider) 13 | { 14 | _serviceProvider = serviceProvider; 15 | } 16 | 17 | public ValueTask ResolveAsync(HttpContext httpContext) 18 | { 19 | return _serviceProvider.GetService().ResolveAsync(httpContext); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ThrottlR/Policy/SafeListBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace ThrottlR 2 | { 3 | public class SafeListBuilder 4 | { 5 | private readonly ThrottlePolicyBuilder _throttlePolicyBuilder; 6 | private readonly SafeListCollection _safeList; 7 | 8 | public SafeListBuilder(ThrottlePolicyBuilder throttlePolicyBuilder, SafeListCollection safeList) 9 | { 10 | _throttlePolicyBuilder = throttlePolicyBuilder; 11 | _safeList = safeList; 12 | } 13 | 14 | public ThrottlePolicyBuilder ForResolver(ISafeListResolver resolver, params string[] safe) 15 | { 16 | _safeList.AddSafeList(resolver, safe); 17 | 18 | return _throttlePolicyBuilder; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sample/MVC/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:25867", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "MVC": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/ThrottlR.Tests/ThrottlR.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | Library 6 | ThrottlR 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/ThrottlR/Policy/SafeListBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ThrottlR 2 | { 3 | public static class SafeListBuilderExtensions 4 | { 5 | public static ThrottlePolicyBuilder IP(this SafeListBuilder builder, params string[] safe) 6 | { 7 | return builder.ForResolver(IpResolver.Instance, safe); 8 | } 9 | 10 | public static ThrottlePolicyBuilder User(this SafeListBuilder builder, params string[] safe) 11 | { 12 | return builder.ForResolver(UsernameResolver.Instance, safe); 13 | } 14 | 15 | public static ThrottlePolicyBuilder Host(this SafeListBuilder builder, params string[] safe) 16 | { 17 | return builder.ForResolver(HostResolver.Instance, safe); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ThrottlR/Policy/ThrottlePolicy.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ThrottlR 4 | { 5 | public class ThrottlePolicy 6 | { 7 | public ThrottlePolicy() 8 | { 9 | GeneralRules = new List(); 10 | SafeList = new SafeListCollection(); 11 | SpecificRules = new Dictionary>(); 12 | Resolver = NoResolver.Instance; 13 | } 14 | 15 | public List GeneralRules { get; set; } 16 | 17 | public SafeListCollection SafeList { get; set; } 18 | 19 | public Dictionary> SpecificRules { get; set; } 20 | 21 | public IResolver Resolver { get; set; } 22 | 23 | public bool ApplyPerEndpoint { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ThrottlR/Policy/SafeListCollection.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ThrottlR 4 | { 5 | public class SafeListCollection : Dictionary> 6 | { 7 | public void AddSafeList(ISafeListResolver resolver, params string[] safeList) 8 | { 9 | if (!TryGetValue(resolver, out var safeScopes)) 10 | { 11 | safeScopes = new List(); 12 | Add(resolver, safeScopes); 13 | } 14 | 15 | for (var i = 0; i < safeList.Length; i++) 16 | { 17 | var item = safeList[i]; 18 | if (!safeScopes.Contains(item)) 19 | { 20 | safeScopes.Add(item); 21 | } 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ThrottlR/DependencyInjection/ThrottlerBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace ThrottlR 5 | { 6 | /// 7 | /// Reverse Proxy builder for DI configuration. 8 | /// 9 | public class ThrottlerBuilder : IThrottlerBuilder 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// Services collection. 15 | public ThrottlerBuilder(IServiceCollection services) 16 | { 17 | Services = services ?? throw new ArgumentNullException(nameof(services)); 18 | } 19 | 20 | /// 21 | /// Gets the services collection. 22 | /// 23 | public IServiceCollection Services { get; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ThrottlR/ThrottlR.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | Throttling middleware for ASP.NET Core 6 | Kahbazi 7 | ThrottlR 8 | ThrottlR 9 | throttlr;aspnetcore;throttle;rate-limit;endpoint 10 | https://github.com/kahbazi/ThrottlR 11 | MIT 12 | git 13 | https://github.com/kahbazi/ThrottlR 14 | 2.0.0 15 | ThrottlR 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /sample/MVC/ApiController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using ThrottlR; 3 | 4 | namespace MVC 5 | { 6 | // Throttle this controller with default policy 7 | [EnableThrottle] 8 | [ApiController] 9 | public class ApiController : ControllerBase 10 | { 11 | [HttpGet("values")] 12 | public string[] Index() 13 | { 14 | return new string[] { "value1", "value2" }; 15 | } 16 | 17 | // Override General Rule for this action with 2 requests per second 18 | [Throttle(PerSecond = 2)] 19 | [HttpGet("custom")] 20 | public string[] CustomRule() 21 | { 22 | return new string[] { "value1", "value2" }; 23 | } 24 | 25 | // Disable throttle for this action 26 | [DisableThrottle] 27 | [HttpGet("greetings")] 28 | public string Greetings() 29 | { 30 | return "Salutation"; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ThrottlR/Policy/Resolvers/UsernameResolver.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace ThrottlR 6 | { 7 | public class UsernameResolver : ISafeListResolver 8 | { 9 | private const string Anonymous = "__Anonymous__"; 10 | 11 | public static UsernameResolver Instance { get; } = new UsernameResolver(); 12 | 13 | public bool Matches(string scope, string safe) 14 | { 15 | return scope.Equals(safe, StringComparison.InvariantCulture); 16 | } 17 | 18 | public ValueTask ResolveAsync(HttpContext httpContext) 19 | { 20 | var identity = httpContext.User?.Identity; 21 | if (identity == null || !identity.IsAuthenticated) 22 | { 23 | return new ValueTask(Anonymous); 24 | } 25 | 26 | return new ValueTask(identity.Name); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ThrottlR/Policy/Resolvers/HostResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace ThrottlR 6 | { 7 | public class HostResolver : ISafeListResolver 8 | { 9 | private const string NoHost = "__NoHost__"; 10 | 11 | private HostResolver() 12 | { 13 | 14 | } 15 | 16 | public static HostResolver Instance { get; } = new HostResolver(); 17 | 18 | public bool Matches(string scope, string safe) 19 | { 20 | return scope.Equals(safe, StringComparison.InvariantCultureIgnoreCase); 21 | } 22 | 23 | public ValueTask ResolveAsync(HttpContext httpContext) 24 | { 25 | if (httpContext.Request.Host.HasValue) 26 | { 27 | return new ValueTask(httpContext.Request.Host.Value); 28 | } 29 | else 30 | { 31 | return new ValueTask(NoHost); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Stefan Prodan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ThrottlR/Policy/DefaultThrottlePolicyProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Extensions.Options; 3 | 4 | namespace ThrottlR 5 | { 6 | public class DefaultThrottlePolicyProvider : IThrottlePolicyProvider 7 | { 8 | private static readonly Task _nullResult = Task.FromResult(null); 9 | private readonly ThrottleOptions _options; 10 | 11 | /// 12 | /// Creates a new instance of . 13 | /// 14 | /// The options configured for the application. 15 | public DefaultThrottlePolicyProvider(IOptions options) 16 | { 17 | _options = options.Value; 18 | } 19 | 20 | /// 21 | public Task GetPolicyAsync(string policyName) 22 | { 23 | policyName ??= _options.DefaultPolicyName; 24 | if (_options.PolicyMap.TryGetValue(policyName, out var result)) 25 | { 26 | return result.policyTask; 27 | } 28 | 29 | return _nullResult; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ThrottlR/Policy/Resolvers/IpResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace ThrottlR 7 | { 8 | public class IpResolver : ISafeListResolver 9 | { 10 | private IpResolver() 11 | { 12 | 13 | } 14 | 15 | public static IpResolver Instance { get; } = new IpResolver(); 16 | 17 | public bool Matches(string scope, string safe) 18 | { 19 | var parts = safe.Split('/'); 20 | if (parts.Length == 2) 21 | { 22 | var safeMask = SubnetMask.CreateByNetBitLength(int.Parse(parts[1])); 23 | var safeIp = IPAddress.Parse(parts[0]); 24 | 25 | var scopeIp = IPAddress.Parse(scope); 26 | 27 | return scopeIp.IsInSameSubnet(safeIp, safeMask); 28 | } 29 | 30 | return scope.Equals(safe, StringComparison.InvariantCultureIgnoreCase); 31 | } 32 | 33 | public ValueTask ResolveAsync(HttpContext httpContext) 34 | { 35 | return new ValueTask(httpContext.Connection.RemoteIpAddress?.ToString() ?? string.Empty); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/ThrottlR.Tests/Resolvers/IpResolverTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace ThrottlR 4 | { 5 | public class IpResolverTests 6 | { 7 | [Theory] 8 | [InlineData("10.20.10.47", "10.20.10.0/24")] 9 | [InlineData("10.20.10.47", "10.20.0.0/16")] 10 | [InlineData("10.20.10.47", "10.0.0.0/8")] 11 | [InlineData("10.20.10.47", "10.20.10.47")] 12 | public void IpResolver_Matches_True(string scope, string safe) 13 | { 14 | // Arrange 15 | var resolver = IpResolver.Instance; 16 | 17 | // Act 18 | var result = resolver.Matches(scope, safe); 19 | 20 | // Assert 21 | Assert.True(result); 22 | } 23 | 24 | [Theory] 25 | [InlineData("10.20.10.47", "10.20.80.0/24")] 26 | [InlineData("10.20.10.47", "10.80.0.0/16")] 27 | [InlineData("10.20.10.47", "80.0.0.0/8")] 28 | [InlineData("10.20.10.47", "10.20.10.80")] 29 | public void IpResolver_Matches_False(string scope, string safe) 30 | { 31 | // Arrange 32 | var resolver = IpResolver.Instance; 33 | 34 | // Act 35 | var result = resolver.Matches(scope, safe); 36 | 37 | // Assert 38 | Assert.False(result); 39 | 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ThrottlR/Service/ThrottlerItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | namespace ThrottlR 5 | { 6 | public struct ThrottlerItem 7 | { 8 | public ThrottlerItem(ThrottleRule rule, string policyName, string scope, string endpointName) 9 | { 10 | Rule = rule ?? throw new ArgumentNullException(nameof(rule)); 11 | PolicyName = policyName; 12 | Scope = scope; 13 | EndpointName = endpointName; 14 | } 15 | 16 | public ThrottleRule Rule { get; } 17 | 18 | public string PolicyName { get; } 19 | 20 | public string Scope { get; } 21 | 22 | public string EndpointName { get; } 23 | 24 | public string GenerateCounterKey(string prefix = "") 25 | { 26 | var builder = new StringBuilder(); 27 | 28 | if (!string.IsNullOrEmpty(prefix)) 29 | { 30 | builder.Append(prefix); 31 | builder.Append(':'); 32 | } 33 | 34 | builder.Append(EndpointName); 35 | builder.Append(':'); 36 | 37 | builder.Append(PolicyName); 38 | builder.Append(':'); 39 | 40 | builder.Append(Rule.TimeWindow); 41 | builder.Append(':'); 42 | 43 | builder.Append(Scope); 44 | builder.Append(':'); 45 | 46 | return builder.ToString(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/ThrottlR.Tests/Common/TestCounterStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace ThrottlR.Tests 7 | { 8 | public class TestCounterStore : ICounterStore 9 | { 10 | private readonly Dictionary _cache = new Dictionary(); 11 | 12 | public ValueTask GetAsync(ThrottlerItem throttlerItem, CancellationToken cancellationToken) 13 | { 14 | var key = throttlerItem.GenerateCounterKey(); 15 | if (_cache.TryGetValue(key, out var counter)) 16 | { 17 | return new ValueTask(counter); 18 | } 19 | else 20 | { 21 | return new ValueTask(default(Counter?)); 22 | } 23 | } 24 | 25 | public ValueTask RemoveAsync(ThrottlerItem throttlerItem, CancellationToken cancellationToken) 26 | { 27 | var key = throttlerItem.GenerateCounterKey(); 28 | _cache.Remove(key); 29 | return new ValueTask(); 30 | } 31 | 32 | public ValueTask SetAsync(ThrottlerItem throttlerItem, Counter counter, TimeSpan? expirationTime, CancellationToken cancellationToken) 33 | { 34 | var key = throttlerItem.GenerateCounterKey(); 35 | _cache[key] = counter; 36 | return new ValueTask(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ThrottlR/Policy/ThrottlePolicyBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ThrottlR 4 | { 5 | public static class ThrottlePolicyBuilderExtensions 6 | { 7 | public static ThrottlePolicyBuilder WithUsernameResolver(this ThrottlePolicyBuilder builder) 8 | { 9 | return builder.WithResolver(UsernameResolver.Instance); 10 | } 11 | 12 | public static ThrottlePolicyBuilder WithIpResolver(this ThrottlePolicyBuilder builder) 13 | { 14 | return builder.WithResolver(IpResolver.Instance); 15 | } 16 | 17 | public static ThrottlePolicyBuilder WithNoResolver(this ThrottlePolicyBuilder builder) 18 | { 19 | return builder.WithResolver(NoResolver.Instance); 20 | } 21 | 22 | public static ThrottlePolicyBuilder WithHostResolver(this ThrottlePolicyBuilder builder) 23 | { 24 | return builder.WithResolver(HostResolver.Instance); 25 | } 26 | 27 | public static ThrottlePolicyBuilder WithAccessTokenResolver(this ThrottlePolicyBuilder builder) 28 | { 29 | return builder.WithResolver(AccessTokenResolver.Instance); 30 | } 31 | 32 | public static ThrottlePolicyBuilder WithResolver(this ThrottlePolicyBuilder builder, IServiceProvider serviceProvider) where TResolver : IResolver 33 | { 34 | return builder.WithResolver(new TypeResolver(serviceProvider)); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/ThrottlR.Tests/Policy/SafeListCollectionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Xunit; 5 | 6 | namespace ThrottlR.Policy 7 | { 8 | public class SafeListCollectionTests 9 | { 10 | 11 | [Fact] 12 | public void SafeListCollection_Add_Multiple_With_Same_Resolver() 13 | { 14 | // Arrange 15 | var safeList = new SafeListCollection(); 16 | 17 | // Act 18 | safeList.AddSafeList(UsernameResolver.Instance, new string[] { "Admin" }); 19 | safeList.AddSafeList(UsernameResolver.Instance, new string[] { "Manager" }); 20 | 21 | // Assert 22 | var kvp = Assert.Single(safeList); 23 | Assert.Equal(UsernameResolver.Instance, kvp.Key); 24 | Assert.Equal(2, kvp.Value.Count); 25 | Assert.Equal(new string[] { "Admin", "Manager" }, kvp.Value); 26 | } 27 | 28 | [Fact] 29 | public void SafeListCollection_Add_Handle_Duplicates() 30 | { 31 | // Arrange 32 | var safeList = new SafeListCollection(); 33 | 34 | // Act 35 | safeList.AddSafeList(UsernameResolver.Instance, new string[] { "Admin" }); 36 | safeList.AddSafeList(UsernameResolver.Instance, new string[] { "Admin", "Manager" }); 37 | safeList.AddSafeList(UsernameResolver.Instance, new string[] { "Manager" }); 38 | 39 | // Assert 40 | var kvp = Assert.Single(safeList); 41 | Assert.Equal(UsernameResolver.Instance, kvp.Key); 42 | Assert.Equal(2, kvp.Value.Count); 43 | Assert.Equal(new string[] { "Admin", "Manager" }, kvp.Value); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ThrottlR/Service/Store/InMemoryCacheCounterStore.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Caching.Memory; 2 | using System; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace ThrottlR 7 | { 8 | public class InMemoryCacheCounterStore : ICounterStore 9 | { 10 | private readonly IMemoryCache _cache; 11 | 12 | public InMemoryCacheCounterStore(IMemoryCache cache) 13 | { 14 | _cache = cache; 15 | } 16 | 17 | public ValueTask GetAsync(ThrottlerItem throttlerItem, CancellationToken cancellationToken) 18 | { 19 | var key = GenerateThrottlerItemKey(throttlerItem); 20 | if (_cache.TryGetValue(key, out Counter stored)) 21 | { 22 | return new ValueTask(stored); 23 | } 24 | 25 | return new ValueTask(default(Counter?)); 26 | } 27 | 28 | public ValueTask RemoveAsync(ThrottlerItem throttlerItem, CancellationToken cancellationToken) 29 | { 30 | var key = GenerateThrottlerItemKey(throttlerItem); 31 | _cache.Remove(key); 32 | 33 | return new ValueTask(); 34 | } 35 | 36 | public ValueTask SetAsync(ThrottlerItem throttlerItem, Counter counter, TimeSpan? expirationTime, CancellationToken cancellationToken) 37 | { 38 | var options = new MemoryCacheEntryOptions 39 | { 40 | Priority = CacheItemPriority.NeverRemove 41 | }; 42 | 43 | if (expirationTime.HasValue) 44 | { 45 | options.SetAbsoluteExpiration(expirationTime.Value); 46 | } 47 | 48 | var key = GenerateThrottlerItemKey(throttlerItem); 49 | _cache.Set(key, counter, options); 50 | 51 | return new ValueTask(); 52 | } 53 | 54 | public virtual string GenerateThrottlerItemKey(ThrottlerItem throttlerItem) 55 | { 56 | return throttlerItem.GenerateCounterKey("Throttler"); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/ThrottlR.Tests/Resolvers/UsernameResolverTests.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Xunit; 5 | 6 | namespace ThrottlR.Resolvers 7 | { 8 | public class UsernameResolverTests 9 | { 10 | [Fact] 11 | public async Task UsernameResolver_Anonymous_When_User_Is_Null() 12 | { 13 | var resolver = new UsernameResolver(); 14 | 15 | var httpContext = new DefaultHttpContext(); 16 | var identity = await resolver.ResolveAsync(httpContext); 17 | 18 | Assert.Equal("__Anonymous__", identity); 19 | } 20 | 21 | [Fact] 22 | public async Task UsernameResolver_Anonymous_When_Identity_Is_Null() 23 | { 24 | var resolver = new UsernameResolver(); 25 | 26 | var httpContext = new DefaultHttpContext(); 27 | httpContext.User = new ClaimsPrincipal(); 28 | var identity = await resolver.ResolveAsync(httpContext); 29 | 30 | Assert.Equal("__Anonymous__", identity); 31 | } 32 | 33 | [Fact] 34 | public async Task UsernameResolver_Anonymous_When_IsAuthenticated_Is_False() 35 | { 36 | var resolver = new UsernameResolver(); 37 | 38 | var httpContext = new DefaultHttpContext(); 39 | httpContext.User = new ClaimsPrincipal(new[] { new ClaimsIdentity() }); 40 | var identity = await resolver.ResolveAsync(httpContext); 41 | 42 | Assert.Equal("__Anonymous__", identity); 43 | } 44 | 45 | [Fact] 46 | public async Task UsernameResolver_Username() 47 | { 48 | var resolver = new UsernameResolver(); 49 | 50 | var httpContext = new DefaultHttpContext(); 51 | httpContext.User = new ClaimsPrincipal(new[] { new ClaimsIdentity(new[] { new Claim("user", "admin") }, "test", "user", "role") }); 52 | var identity = await resolver.ResolveAsync(httpContext); 53 | 54 | Assert.Equal("admin", identity); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/ThrottlR/Internal/SubnetMask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace ThrottlR 5 | { 6 | internal static class SubnetMask 7 | { 8 | public static readonly IPAddress ClassA = IPAddress.Parse("255.0.0.0"); 9 | public static readonly IPAddress ClassB = IPAddress.Parse("255.255.0.0"); 10 | public static readonly IPAddress ClassC = IPAddress.Parse("255.255.255.0"); 11 | 12 | public static IPAddress CreateByHostBitLength(int hostpartLength) 13 | { 14 | var hostPartLength = hostpartLength; 15 | var netPartLength = 32 - hostPartLength; 16 | 17 | if (netPartLength < 2) 18 | { 19 | throw new ArgumentException("Number of hosts is to large for IPv4"); 20 | } 21 | 22 | var binaryMask = new byte[4]; 23 | 24 | for (var i = 0; i < 4; i++) 25 | { 26 | if (i * 8 + 8 <= netPartLength) 27 | { 28 | binaryMask[i] = (byte)255; 29 | } 30 | else if (i * 8 > netPartLength) 31 | { 32 | binaryMask[i] = (byte)0; 33 | } 34 | else 35 | { 36 | var oneLength = netPartLength - i * 8; 37 | var binaryDigit = string.Empty.PadLeft(oneLength, '1').PadRight(8, '0'); 38 | binaryMask[i] = Convert.ToByte(binaryDigit, 2); 39 | } 40 | } 41 | return new IPAddress(binaryMask); 42 | } 43 | 44 | public static IPAddress CreateByNetBitLength(int netpartLength) 45 | { 46 | var hostPartLength = 32 - netpartLength; 47 | return CreateByHostBitLength(hostPartLength); 48 | } 49 | 50 | public static IPAddress CreateByHostNumber(int numberOfHosts) 51 | { 52 | var maxNumber = numberOfHosts + 1; 53 | 54 | var b = Convert.ToString(maxNumber, 2); 55 | 56 | return CreateByHostBitLength(b.Length); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ThrottlR/Internal/IPAddressExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace ThrottlR 5 | { 6 | internal static class IPAddressExtensions 7 | { 8 | public static IPAddress GetBroadcastAddress(this IPAddress address, IPAddress subnetMask) 9 | { 10 | var ipAdressBytes = address.GetAddressBytes(); 11 | var subnetMaskBytes = subnetMask.GetAddressBytes(); 12 | 13 | if (ipAdressBytes.Length != subnetMaskBytes.Length) 14 | { 15 | throw new ArgumentException("Lengths of IP address and subnet mask do not match."); 16 | } 17 | 18 | var broadcastAddress = new byte[ipAdressBytes.Length]; 19 | for (var i = 0; i < broadcastAddress.Length; i++) 20 | { 21 | broadcastAddress[i] = (byte)(ipAdressBytes[i] | (subnetMaskBytes[i] ^ 255)); 22 | } 23 | return new IPAddress(broadcastAddress); 24 | } 25 | 26 | public static IPAddress GetNetworkAddress(this IPAddress address, IPAddress subnetMask) 27 | { 28 | var ipAdressBytes = address.GetAddressBytes(); 29 | var subnetMaskBytes = subnetMask.GetAddressBytes(); 30 | 31 | if (ipAdressBytes.Length != subnetMaskBytes.Length) 32 | { 33 | throw new ArgumentException("Lengths of IP address and subnet mask do not match."); 34 | } 35 | 36 | var broadcastAddress = new byte[ipAdressBytes.Length]; 37 | for (var i = 0; i < broadcastAddress.Length; i++) 38 | { 39 | broadcastAddress[i] = (byte)(ipAdressBytes[i] & (subnetMaskBytes[i])); 40 | } 41 | return new IPAddress(broadcastAddress); 42 | } 43 | 44 | public static bool IsInSameSubnet(this IPAddress address2, IPAddress address, IPAddress subnetMask) 45 | { 46 | var network1 = address.GetNetworkAddress(subnetMask); 47 | var network2 = address2.GetNetworkAddress(subnetMask); 48 | 49 | return network1.Equals(network2); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ThrottlR/Endpoints/ThrottleAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ThrottlR 5 | { 6 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] 7 | public class ThrottleAttribute : Attribute, IThrottleRulesMetadata 8 | { 9 | private static readonly TimeSpan _oneSecond = TimeSpan.FromSeconds(1); 10 | private static readonly TimeSpan _oneMinute = TimeSpan.FromMinutes(1); 11 | private static readonly TimeSpan _oneHour = TimeSpan.FromHours(1); 12 | private static readonly TimeSpan _oneDay = TimeSpan.FromDays(1); 13 | 14 | private readonly object _lockObject = new object(); 15 | 16 | private List _generalRules; 17 | 18 | IReadOnlyList IThrottleRulesMetadata.GeneralRules 19 | { 20 | get 21 | { 22 | if (_generalRules == null) 23 | { 24 | lock (_lockObject) 25 | { 26 | var generalRules = new List(); 27 | 28 | if (PerDay > 0) 29 | { 30 | generalRules.Add(new ThrottleRule { Quota = PerDay, TimeWindow = _oneDay }); 31 | } 32 | 33 | if (PerHour > 0) 34 | { 35 | generalRules.Add(new ThrottleRule { Quota = PerHour, TimeWindow = _oneHour }); 36 | } 37 | 38 | if (PerMinute > 0) 39 | { 40 | generalRules.Add(new ThrottleRule { Quota = PerMinute, TimeWindow = _oneMinute }); 41 | } 42 | 43 | if (PerSecond > 0) 44 | { 45 | generalRules.Add(new ThrottleRule { Quota = PerSecond, TimeWindow = _oneSecond }); 46 | } 47 | 48 | _generalRules = generalRules; 49 | } 50 | } 51 | 52 | return _generalRules; 53 | } 54 | } 55 | 56 | public long PerSecond { get; set; } 57 | 58 | public long PerMinute { get; set; } 59 | 60 | public long PerHour { get; set; } 61 | 62 | public long PerDay { get; set; } 63 | 64 | public string PolicyName { get; set; } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/ThrottlR/Endpoints/ThrottlerEndpointConventionBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using ThrottlR; 2 | 3 | namespace Microsoft.AspNetCore.Builder 4 | { 5 | public static class ThrottlerEndpointConventionBuilderExtensions 6 | { 7 | private static readonly EnableThrottle _throttleMetadata = new EnableThrottle(); 8 | 9 | public static TBuilder Throttle(this TBuilder builder, string policy) where TBuilder : IEndpointConventionBuilder 10 | { 11 | builder.Add(endpointBuilder => 12 | { 13 | endpointBuilder.Metadata.Add(new EnableThrottle(policy)); 14 | }); 15 | return builder; 16 | } 17 | 18 | /// 19 | /// The default policy would be applied 20 | /// 21 | /// 22 | /// 23 | /// 24 | public static TBuilder Throttle(this TBuilder builder) where TBuilder : IEndpointConventionBuilder 25 | { 26 | builder.Add(endpointBuilder => 27 | { 28 | endpointBuilder.Metadata.Add(_throttleMetadata); 29 | }); 30 | return builder; 31 | } 32 | 33 | /// 34 | /// 35 | /// 36 | /// 37 | /// 38 | /// 39 | /// 40 | /// 41 | /// 42 | /// 43 | /// 44 | public static TBuilder Throttle(this TBuilder builder, long? perSecond = null, long? perMinute = null, long? perHour = null, long? perDay = null, string policyName = null) where TBuilder : IEndpointConventionBuilder 45 | { 46 | builder.Add(endpointBuilder => 47 | { 48 | var throttleAttribute = new ThrottleAttribute 49 | { 50 | PerSecond = perSecond ?? 0, 51 | PerMinute = perMinute ?? 0, 52 | PerHour = perHour ?? 0, 53 | PerDay = perDay ?? 0, 54 | PolicyName = policyName 55 | }; 56 | 57 | endpointBuilder.Metadata.Add(throttleAttribute); 58 | }); 59 | return builder; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/ThrottlR.Tests/AsyncKeyLock/AsyncKeyLockTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Six Labors and contributors. 2 | // Licensed under the Apache License, Version 2.0. 3 | // Thanks to https://github.com/SixLabors/ImageSharp.Web/ 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | namespace ThrottlR.Tests 9 | { 10 | public class AsyncKeyLockTests 11 | { 12 | private static readonly AsyncKeyLock _asyncLock = new AsyncKeyLock(); 13 | 14 | private const string AsyncKey = "ASYNC_KEY"; 15 | private const string AsyncKey1 = "ASYNC_KEY1"; 16 | private const string AsyncKey2 = "ASYNC_KEY2"; 17 | 18 | [Fact] 19 | public async Task AsyncLockCanLockByKeyAsync() 20 | { 21 | var zeroEntered = false; 22 | var entered = false; 23 | var index = 0; 24 | var tasks = Enumerable.Range(0, 5).Select(i => Task.Run(async () => 25 | { 26 | using (await _asyncLock.WriterLockAsync(AsyncKey)) 27 | { 28 | if (i == 0) 29 | { 30 | entered = true; 31 | zeroEntered = true; 32 | await Task.Delay(3000); 33 | entered = false; 34 | } 35 | else if (zeroEntered) 36 | { 37 | Assert.False(entered); 38 | } 39 | 40 | index++; 41 | } 42 | })).ToArray(); 43 | 44 | await Task.WhenAll(tasks); 45 | Assert.Equal(5, index); 46 | } 47 | 48 | [Fact] 49 | public async Task AsyncLockAllowsDifferentKeysToRunAsync() 50 | { 51 | var zeroEntered = false; 52 | var entered = false; 53 | var index = 0; 54 | var tasks = Enumerable.Range(0, 5).Select(i => Task.Run(async () => 55 | { 56 | using (await _asyncLock.WriterLockAsync(i > 0 ? AsyncKey2 : AsyncKey1)) 57 | { 58 | if (i == 0) 59 | { 60 | entered = true; 61 | zeroEntered = true; 62 | await Task.Delay(2000); 63 | entered = false; 64 | } 65 | else if (zeroEntered) 66 | { 67 | Assert.True(entered); 68 | } 69 | 70 | index++; 71 | } 72 | })).ToArray(); 73 | 74 | await Task.WhenAll(tasks); 75 | Assert.Equal(5, index); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/ThrottlR/DependencyInjection/ThrottlerServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.Extensions.DependencyInjection.Extensions; 5 | using ThrottlR; 6 | 7 | namespace Microsoft.Extensions.DependencyInjection 8 | { 9 | public static class ThrottlerServiceCollectionExtensions 10 | { 11 | public static IThrottlerBuilder AddThrottlR(this IServiceCollection services, Action configure) 12 | { 13 | var builder = new ThrottlerBuilder(services); 14 | 15 | services.TryAddSingleton(); 16 | services.TryAddSingleton(); 17 | services.TryAddSingleton(); 18 | 19 | services.AddOptions() 20 | .Configure(configure) 21 | .PostConfigure(options => 22 | { 23 | foreach (var policy in options.PolicyMap.Select(kvp => kvp.Value.policy)) 24 | { 25 | foreach (var kvp in policy.SafeList) 26 | { 27 | policy.SafeList[kvp.Key] = kvp.Value 28 | .Distinct() 29 | .ToList(); 30 | } 31 | 32 | 33 | policy.GeneralRules = TidyUp(policy.GeneralRules); 34 | 35 | policy.SpecificRules = policy.SpecificRules 36 | .ToDictionary(kvp => kvp.Key, kvp => TidyUp(kvp.Value)); 37 | } 38 | }); 39 | 40 | services.AddMemoryCache(); 41 | 42 | return builder; 43 | 44 | static List TidyUp(IEnumerable rules) 45 | { 46 | return rules.GroupBy(kvp => kvp.TimeWindow) 47 | .Select(l => l.OrderBy(x => x.Quota)) 48 | .Select(l => l.First()) 49 | .OrderByDescending(x => x.TimeWindow) 50 | .ToList(); 51 | } 52 | } 53 | 54 | public static IThrottlerBuilder AddInMemoryCounterStore(this IThrottlerBuilder builder) 55 | { 56 | builder.Services.TryAddSingleton(); 57 | 58 | return builder; 59 | } 60 | 61 | public static IThrottlerBuilder AddDistributedCounterStore(this IThrottlerBuilder builder) 62 | { 63 | builder.Services.TryAddSingleton(); 64 | 65 | return builder; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/ThrottlR/Service/Store/DistributedCacheCounterStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Caching.Distributed; 5 | 6 | namespace ThrottlR 7 | { 8 | public class DistributedCacheCounterStore : ICounterStore 9 | { 10 | private readonly IDistributedCache _cache; 11 | 12 | public DistributedCacheCounterStore(IDistributedCache cache) 13 | { 14 | _cache = cache; 15 | } 16 | 17 | public async ValueTask SetAsync(ThrottlerItem throttlerItem, Counter counter, TimeSpan? expirationTime, CancellationToken cancellationToken) 18 | { 19 | var options = new DistributedCacheEntryOptions(); 20 | 21 | if (expirationTime.HasValue) 22 | { 23 | options.SetAbsoluteExpiration(expirationTime.Value); 24 | } 25 | 26 | var key = GenerateThrottlerItemKey(throttlerItem); 27 | await _cache.SetAsync(key, Serialize(counter), options, cancellationToken); 28 | } 29 | 30 | public async ValueTask GetAsync(ThrottlerItem throttlerItem, CancellationToken cancellationToken) 31 | { 32 | var key = GenerateThrottlerItemKey(throttlerItem); 33 | var stored = await _cache.GetAsync(key, cancellationToken); 34 | 35 | if (stored != null) 36 | { 37 | return Deserialize(stored); 38 | } 39 | 40 | return default; 41 | } 42 | 43 | public async ValueTask RemoveAsync(ThrottlerItem throttlerItem, CancellationToken cancellationToken) 44 | { 45 | var key = GenerateThrottlerItemKey(throttlerItem); 46 | await _cache.RemoveAsync(key, cancellationToken); 47 | } 48 | 49 | public virtual string GenerateThrottlerItemKey(ThrottlerItem throttlerItem) 50 | { 51 | return throttlerItem.GenerateCounterKey("Throttler"); 52 | } 53 | 54 | private static byte[] Serialize(Counter counter) 55 | { 56 | var data = new byte[12]; 57 | 58 | // no need to check for success, it only fails on size mismatch which shouldn't happen 59 | BitConverter.TryWriteBytes(data.AsSpan(0, 4), counter.Count); 60 | BitConverter.TryWriteBytes(data.AsSpan(4, 8), counter.Timestamp.Ticks); 61 | return data; 62 | } 63 | 64 | private static Counter? Deserialize(byte[] counterBinaryData) 65 | { 66 | try 67 | { 68 | var count = BitConverter.ToInt32(counterBinaryData, 0); 69 | var timestamp = new DateTime(BitConverter.ToInt64(counterBinaryData, 4)); 70 | return new Counter(timestamp, count); 71 | } 72 | catch 73 | { 74 | return default; 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /sample/MVC/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using ThrottlR; 6 | 7 | namespace MVC 8 | { 9 | public class Startup 10 | { 11 | public void ConfigureServices(IServiceCollection services) 12 | { 13 | services.AddControllers(); 14 | 15 | // Adds throttlR services to service collection 16 | services.AddThrottlR(options => 17 | { 18 | // Configures the default policy 19 | options.AddDefaultPolicy(policy => 20 | { 21 | // throttling is based on request ip 22 | policy.WithIpResolver() 23 | // add general rules for all ips 24 | .WithGeneralRule(TimeSpan.FromSeconds(10), 3) // 3 requests could be called every 10 seconds 25 | .WithGeneralRule(TimeSpan.FromMinutes(1), 30) // 30 requests could be called every 1 minute 26 | .WithGeneralRule(TimeSpan.FromHours(1), 500) // 500 requests could be called every 1 hour 27 | 28 | // override general rules for "10.20.10.47" with new rules 29 | .WithSpecificRule("10.20.10.47", TimeSpan.FromSeconds(10), 60) 30 | .WithSpecificRule("10.20.10.47", TimeSpan.FromMinutes(1), 600) 31 | .WithSpecificRule("10.20.10.47", TimeSpan.FromHours(1), 1000) 32 | 33 | // throttling skips requests coming from IP : "127.0.0.1" or "::1" 34 | .SafeList.IP("127.0.0.1", "::1") 35 | // throttling skips requests for User "Admin" 36 | .SafeList.User("Admin") 37 | // throttling skips requests with Host header "myApi.local" 38 | .SafeList.Host("myApi.local"); 39 | }); 40 | }) 41 | .AddInMemoryCounterStore(); 42 | } 43 | 44 | public void Configure(IApplicationBuilder app) 45 | { 46 | app.UseRouting(); 47 | 48 | // Adds Throttler middleware to the pipeline 49 | app.UseThrottler(); 50 | 51 | app.UseEndpoints(endpoints => 52 | { 53 | endpoints.MapControllers(); 54 | 55 | endpoints.MapGet("/hello", context => 56 | { 57 | return context.Response.WriteAsync("Hello"); 58 | }) 59 | // Throttle "/hello" endpoint with default policy 60 | .Throttle(); 61 | 62 | endpoints.MapGet("/farewell", context => 63 | { 64 | return context.Response.WriteAsync("Farewell"); 65 | }) 66 | // Override general rules for default policy 67 | .Throttle(perSecond: 4); 68 | }); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /benchmark/ThrottlR.Benchmark/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.AspNetCore.Builder; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Http; 6 | using BenchmarkDotNet.Attributes; 7 | using BenchmarkDotNet.Jobs; 8 | using BenchmarkDotNet.Running; 9 | using System.Diagnostics; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | using System.Net; 13 | using CommandLine; 14 | 15 | namespace ThrottlR.Benchmark 16 | { 17 | internal class Program 18 | { 19 | private static void Main() 20 | { 21 | BenchmarkRunner.Run(); 22 | } 23 | } 24 | 25 | [SimpleJob(RuntimeMoniker.NetCoreApp31)] 26 | [MemoryDiagnoser] 27 | [RPlotExporter] 28 | public class ThrottlerOverheadBenchmark 29 | { 30 | private readonly Random _random = new Random(); 31 | 32 | private RequestDelegate _app; 33 | private List _ipList; 34 | 35 | [GlobalSetup] 36 | public void Setup() 37 | { 38 | _ipList = Enumerable.Range(1000, 2000).Select(x => (long)x).ToList(); 39 | 40 | var serviceCollection = new ServiceCollection(); 41 | serviceCollection.AddRouting(); 42 | serviceCollection.AddLogging(); 43 | serviceCollection.AddSingleton(new DiagnosticListener(string.Empty)); 44 | 45 | serviceCollection.AddThrottlR(options => 46 | { 47 | options.AddDefaultPolicy(policy => 48 | { 49 | policy.WithIpResolver() 50 | .WithGeneralRule(TimeSpan.FromDays(1), int.MaxValue); 51 | }); 52 | }).AddInMemoryCounterStore(); 53 | 54 | var serviceProvider = serviceCollection.BuildServiceProvider(); 55 | var app = new ApplicationBuilder(serviceProvider); 56 | app.UseRouting(); 57 | app.UseThrottler(); 58 | app.UseEndpoints(builder => 59 | { 60 | builder.Map("/WithThrottle", Greetings).Throttle(); 61 | builder.Map("/NoThrottle", Greetings); 62 | }); 63 | 64 | _app = app.Build(); 65 | 66 | static async Task Greetings(HttpContext context) 67 | { 68 | await Task.Delay(2000); 69 | await context.Response.WriteAsync("Hello World!"); 70 | } 71 | } 72 | 73 | [Benchmark] 74 | public Task WithThrottle() 75 | { 76 | return Run("/WithThrottle"); 77 | } 78 | 79 | [Benchmark] 80 | public Task NoThrottle() 81 | { 82 | return Run("/NoThrottle"); 83 | } 84 | 85 | private async Task Run(string path) 86 | { 87 | var httpContext = new DefaultHttpContext(); 88 | httpContext.Connection.RemoteIpAddress = new IPAddress(_ipList[_random.Next(_ipList.Count - 1)]); 89 | httpContext.Request.Path = new PathString(path); 90 | await _app(httpContext); 91 | return httpContext.Response.StatusCode; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /ThrottlR.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 16 3 | VisualStudioVersion = 16.0.30225.117 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{909648ED-E1F4-4DAE-A274-AEC47BE826F3}" 6 | EndProject 7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C3997747-D4C9-4E9F-866D-F5B6D10CD40F}" 8 | ProjectSection(SolutionItems) = preProject 9 | .editorconfig = .editorconfig 10 | README.md = README.md 11 | EndProjectSection 12 | EndProject 13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{83B05009-1BC7-4E56-8D4B-0E107CA4D45B}" 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThrottlR", "src\ThrottlR\ThrottlR.csproj", "{E9557798-3499-4726-BCB7-5EB7CB4B33E7}" 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThrottlR.Tests", "test\ThrottlR.Tests\ThrottlR.Tests.csproj", "{0A7C0247-AE46-4068-AE42-F955AE2A1D0E}" 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{760F0930-725B-404A-96D7-129BBC4E8E04}" 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MVC", "sample\MVC\MVC.csproj", "{B6C5CCEA-01B4-4B7F-A855-CBF500B6E742}" 22 | EndProject 23 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmark", "benchmark", "{E02305BA-6EF5-4025-8DB1-A5747B24BFE5}" 24 | EndProject 25 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ThrottlR.Benchmark", "benchmark\ThrottlR.Benchmark\ThrottlR.Benchmark.csproj", "{7C5F45DC-4AF9-4600-8BC0-65700266937B}" 26 | EndProject 27 | Global 28 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 29 | Debug|Any CPU = Debug|Any CPU 30 | Release|Any CPU = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 33 | {E9557798-3499-4726-BCB7-5EB7CB4B33E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {E9557798-3499-4726-BCB7-5EB7CB4B33E7}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {E9557798-3499-4726-BCB7-5EB7CB4B33E7}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {E9557798-3499-4726-BCB7-5EB7CB4B33E7}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {0A7C0247-AE46-4068-AE42-F955AE2A1D0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {0A7C0247-AE46-4068-AE42-F955AE2A1D0E}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {0A7C0247-AE46-4068-AE42-F955AE2A1D0E}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {0A7C0247-AE46-4068-AE42-F955AE2A1D0E}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {B6C5CCEA-01B4-4B7F-A855-CBF500B6E742}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {B6C5CCEA-01B4-4B7F-A855-CBF500B6E742}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {B6C5CCEA-01B4-4B7F-A855-CBF500B6E742}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {B6C5CCEA-01B4-4B7F-A855-CBF500B6E742}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {7C5F45DC-4AF9-4600-8BC0-65700266937B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {7C5F45DC-4AF9-4600-8BC0-65700266937B}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {7C5F45DC-4AF9-4600-8BC0-65700266937B}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {7C5F45DC-4AF9-4600-8BC0-65700266937B}.Release|Any CPU.Build.0 = Release|Any CPU 49 | EndGlobalSection 50 | GlobalSection(SolutionProperties) = preSolution 51 | HideSolutionNode = FALSE 52 | EndGlobalSection 53 | GlobalSection(NestedProjects) = preSolution 54 | {E9557798-3499-4726-BCB7-5EB7CB4B33E7} = {909648ED-E1F4-4DAE-A274-AEC47BE826F3} 55 | {0A7C0247-AE46-4068-AE42-F955AE2A1D0E} = {83B05009-1BC7-4E56-8D4B-0E107CA4D45B} 56 | {B6C5CCEA-01B4-4B7F-A855-CBF500B6E742} = {760F0930-725B-404A-96D7-129BBC4E8E04} 57 | {7C5F45DC-4AF9-4600-8BC0-65700266937B} = {E02305BA-6EF5-4025-8DB1-A5747B24BFE5} 58 | EndGlobalSection 59 | GlobalSection(ExtensibilityGlobals) = postSolution 60 | SolutionGuid = {2FE0A4A1-855C-4770-A84F-F1CAE18E62EF} 61 | EndGlobalSection 62 | EndGlobal 63 | -------------------------------------------------------------------------------- /src/ThrottlR/Service/ThrottlerService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace ThrottlR 6 | { 7 | public class ThrottlerService : IThrottlerService 8 | { 9 | /// The key-lock used for limiting requests. 10 | private static readonly AsyncKeyLock _asyncLock = new AsyncKeyLock(); 11 | 12 | private readonly ICounterStore _counterStore; 13 | private readonly ISystemClock _systemClock; 14 | 15 | public ThrottlerService(ICounterStore counterStore, ISystemClock systemClock) 16 | { 17 | _counterStore = counterStore; 18 | _systemClock = systemClock; 19 | } 20 | 21 | public IEnumerable GetRules(IReadOnlyList generalRules, IReadOnlyList specificRules) 22 | { 23 | var g = 0; 24 | var s = 0; 25 | 26 | while (true) 27 | { 28 | if (s == specificRules.Count && g == generalRules.Count) 29 | { 30 | break; 31 | } 32 | else if (s == specificRules.Count) 33 | { 34 | for (; g < generalRules.Count; g++) 35 | { 36 | yield return generalRules[g]; 37 | } 38 | break; 39 | } 40 | else if (g == generalRules.Count) 41 | { 42 | for (; s < specificRules.Count; s++) 43 | { 44 | yield return specificRules[s]; 45 | } 46 | break; 47 | } 48 | 49 | var generalRule = generalRules[g]; 50 | var speceficRule = specificRules[s]; 51 | 52 | if (speceficRule.TimeWindow > generalRule.TimeWindow) 53 | { 54 | yield return speceficRule; 55 | s++; 56 | } 57 | else if (speceficRule.TimeWindow < generalRule.TimeWindow) 58 | { 59 | yield return generalRule; 60 | g++; 61 | } 62 | else 63 | { 64 | yield return speceficRule; 65 | s++; 66 | g++; 67 | } 68 | } 69 | } 70 | 71 | public async Task ProcessRequestAsync(ThrottlerItem throttlerItem, ThrottleRule rule, CancellationToken cancellationToken) 72 | { 73 | Counter counter; 74 | 75 | using (await _asyncLock.WriterLockAsync(throttlerItem.GenerateCounterKey())) 76 | { 77 | var entry = await _counterStore.GetAsync(throttlerItem, cancellationToken); 78 | 79 | if (entry.HasValue) 80 | { 81 | // entry has not expired 82 | if (entry.Value.Timestamp + rule.TimeWindow >= _systemClock.UtcNow) 83 | { 84 | // increment request count 85 | var totalCount = entry.Value.Count + 1; 86 | 87 | counter = new Counter(entry.Value.Timestamp, totalCount); 88 | } 89 | else 90 | { 91 | counter = new Counter(_systemClock.UtcNow, 1); 92 | } 93 | } 94 | else 95 | { 96 | counter = new Counter(_systemClock.UtcNow, 1); 97 | } 98 | 99 | await _counterStore.SetAsync(throttlerItem, counter, rule.TimeWindow, cancellationToken); 100 | } 101 | 102 | return counter; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ThrottlR 2 | 3 | [![NuGet](https://img.shields.io/nuget/vpre/ThrottlR.svg)](https://www.nuget.org/packages/ThrottlR) 4 | 5 | A Throttling middleware for ASP.NET Core. 6 | 7 | #### Getting Started 8 | 9 | Install [ThrottlR](https://www.nuget.org/packages/ThrottlR) nuget package: 10 | 11 | ``` 12 | dotnet add package ThrottlR 13 | ``` 14 | 15 | Since ThrottlR is implemented on top of Endpoint, ThrottlR middleware needs to be added after `UseRouting()` and before `UseEndpoints()`. 16 | 17 | ```csharp 18 | public void Configure(IApplicationBuilder app) 19 | { 20 | app.UseRouting(); 21 | // Adds Throttler middleware to the pipeline 22 | app.UseThrottler(); 23 | app.UseEndpoints(...); 24 | } 25 | ``` 26 | 27 | Also add ThrottlR to `IServiceCollection` 28 | 29 | ```csharp 30 | public void ConfigureServices(IServiceCollection services) 31 | { 32 | // Adds throttlR services to service collection 33 | services.AddThrottlR(options => 34 | { 35 | // Configures the default policy 36 | options.AddDefaultPolicy(policy => 37 | { 38 | // throttling is based on request ip 39 | policy.WithIpResolver() 40 | // add general rules for all ips 41 | .WithGeneralRule(TimeSpan.FromSeconds(10), 3) // 3 requests could be called every 10 seconds 42 | .WithGeneralRule(TimeSpan.FromMinutes(1), 30) // 30 requests could be called every 1 minute 43 | .WithGeneralRule(TimeSpan.FromHours(1), 500) // 500 requests could be called every 1 hour 44 | 45 | // override general rules for "10.20.10.47" with new rules 46 | .WithSpecificRule("10.20.10.47", TimeSpan.FromSeconds(10), 60) 47 | .WithSpecificRule("10.20.10.47", TimeSpan.FromMinutes(1), 600) 48 | .WithSpecificRule("10.20.10.47", TimeSpan.FromHours(1), 1000) 49 | 50 | // throttling skips requests coming from IP : "127.0.0.1" or "::1" 51 | .SafeList.IP("127.0.0.1", "::1") 52 | // throttling skips requests for User "Admin" 53 | .SafeList.User("Admin") 54 | // throttling skips requests with Host header "myApi.local" 55 | .SafeList.Host("myApi.local"); 56 | }); 57 | }) 58 | .AddInMemoryCounterStore(); 59 | } 60 | ``` 61 | 62 | #### How to add ThrottlR on Controller 63 | Add `[Throttle]` Attribute to Controller class or Action. You can add `[DisableThrottle]` for Action that doesn't need throttling. 64 | 65 | ```csharp 66 | // Throttle this controller with default policy 67 | [Throttle] 68 | [ApiController] 69 | public class ApiController : ControllerBase 70 | { 71 | [HttpGet("values")] 72 | public string[] GetValues() 73 | { 74 | return new string[] { "value1", "value2" }; 75 | } 76 | 77 | // Override General Rule for this action with 2 requests per second 78 | [Throttle(PerSecond = 2)] 79 | [HttpGet("custom")] 80 | public string[] CustomRule() 81 | { 82 | return new string[] { "value1", "value2" }; 83 | } 84 | 85 | // Disable throttle for this action 86 | [DisableThrottle] 87 | [HttpGet("other")] 88 | public string[] GetOtherValues() 89 | { 90 | return new string[] { "value1", "value2" }; 91 | } 92 | } 93 | ``` 94 | 95 | #### How to add ThrottlR on Endpoint 96 | 97 | Use `Throttle()` extensions method 98 | 99 | ```csharp 100 | app.UseEndpoints(endpoints => 101 | { 102 | endpoints.MapGet("/greetings", context => 103 | { 104 | return context.Response.WriteAsync("Greetings"); 105 | }) 106 | // Throttle "/greetings" endpoint with default policy 107 | .Throttle(); 108 | 109 | endpoints.MapGet("/farewell", context => 110 | { 111 | return context.Response.WriteAsync("Farewell"); 112 | }) 113 | // Throttle "/farewell" endpoint and override general rules for default policy 114 | .Throttle(perSecond: 4); 115 | }); 116 | ``` 117 | -------------------------------------------------------------------------------- /test/ThrottlR.Tests/Endpoints/ThrottleAttributeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Xunit; 4 | 5 | namespace ThrottlR 6 | { 7 | public class ThrottleAttributeTests 8 | { 9 | [Fact] 10 | public void ThrottleAttribute_Constructor_Order() 11 | { 12 | // Arrange 13 | var throttleAttribute = new ThrottleAttribute 14 | { 15 | PerSecond = 1, 16 | PerMinute = 10, 17 | PerHour = 100, 18 | PerDay = 1000 19 | }; 20 | 21 | // Act 22 | IThrottleRulesMetadata throttleRules = throttleAttribute; 23 | var rules = throttleRules.GeneralRules; 24 | 25 | // Assert 26 | Assert.Equal(4, rules.Count); 27 | 28 | var rule1 = rules[0]; 29 | Assert.Equal(1000, rule1.Quota); 30 | Assert.Equal(TimeSpan.FromDays(1), rule1.TimeWindow); 31 | 32 | var rule2 = rules[1]; 33 | Assert.Equal(100, rule2.Quota); 34 | Assert.Equal(TimeSpan.FromHours(1), rule2.TimeWindow); 35 | 36 | var rule3 = rules[2]; 37 | Assert.Equal(10, rule3.Quota); 38 | Assert.Equal(TimeSpan.FromMinutes(1), rule3.TimeWindow); 39 | 40 | var rule4 = rules[3]; 41 | Assert.Equal(1, rule4.Quota); 42 | Assert.Equal(TimeSpan.FromSeconds(1), rule4.TimeWindow); 43 | } 44 | 45 | 46 | [Theory] 47 | [InlineData(0, 0, 0, 0)] 48 | [InlineData(0, 0, 0, 1)] 49 | [InlineData(0, 0, 2, 0)] 50 | [InlineData(0, 0, 2, 1)] 51 | [InlineData(0, 3, 0, 0)] 52 | [InlineData(0, 3, 0, 1)] 53 | [InlineData(0, 3, 2, 0)] 54 | [InlineData(0, 3, 2, 1)] 55 | [InlineData(4, 0, 0, 0)] 56 | [InlineData(4, 0, 0, 1)] 57 | [InlineData(4, 0, 2, 0)] 58 | [InlineData(4, 0, 2, 1)] 59 | [InlineData(4, 3, 0, 0)] 60 | [InlineData(4, 3, 0, 1)] 61 | [InlineData(4, 3, 2, 0)] 62 | [InlineData(4, 3, 2, 1)] 63 | public void ThrottleAttribute_Constructor_Rules(long perSecond, long perMinute, long perHour, long perDay) 64 | { 65 | // Arrange 66 | var throttleAttribute = new ThrottleAttribute 67 | { 68 | PerSecond = perSecond, 69 | PerMinute = perMinute, 70 | PerHour = perHour, 71 | PerDay = perDay 72 | }; 73 | 74 | // Act 75 | IThrottleRulesMetadata throttleRules = throttleAttribute; 76 | var rules = throttleRules.GeneralRules; 77 | 78 | // Assert 79 | Assert.NotNull(rules); 80 | 81 | if (perSecond > 0) 82 | { 83 | var rule = Assert.Single(rules.Where(x => x.TimeWindow == TimeSpan.FromSeconds(1))); 84 | Assert.Equal(perSecond, rule.Quota); 85 | } 86 | 87 | if (perMinute > 0) 88 | { 89 | var rule = Assert.Single(rules.Where(x => x.TimeWindow == TimeSpan.FromMinutes(1))); 90 | Assert.Equal(perMinute, rule.Quota); 91 | } 92 | 93 | if (perHour > 0) 94 | { 95 | var rule = Assert.Single(rules.Where(x => x.TimeWindow == TimeSpan.FromHours(1))); 96 | Assert.Equal(perHour, rule.Quota); 97 | } 98 | 99 | if (perDay > 0) 100 | { 101 | var rule = Assert.Single(rules.Where(x => x.TimeWindow == TimeSpan.FromDays(1))); 102 | Assert.Equal(perDay, rule.Quota); 103 | } 104 | 105 | var rulesCount = (perSecond > 0 ? 1 : 0) 106 | + (perMinute > 0 ? 1 : 0) 107 | + (perHour > 0 ? 1 : 0) 108 | + (perDay > 0 ? 1 : 0); 109 | 110 | Assert.Equal(rulesCount, rules.Count); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/ThrottlR/Policy/ThrottlerOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Http; 6 | 7 | namespace ThrottlR 8 | { 9 | public class ThrottleOptions 10 | { 11 | private static readonly byte[] _exceededQuoataMessage = Encoding.UTF8.GetBytes("You have exceeded your quota."); 12 | 13 | private string _defaultPolicyName = "__DefaultThrottlerPolicy"; 14 | 15 | internal IDictionary policyTask)> PolicyMap { get; } 16 | = new Dictionary)>(StringComparer.Ordinal); 17 | 18 | public string DefaultPolicyName 19 | { 20 | get => _defaultPolicyName; 21 | set 22 | { 23 | _defaultPolicyName = value ?? throw new ArgumentNullException(nameof(value)); 24 | } 25 | } 26 | 27 | /// 28 | /// Adds a new policy and sets it as the default. 29 | /// 30 | /// The policy to be added. 31 | public void AddDefaultPolicy(ThrottlePolicy policy) 32 | { 33 | _ = policy ?? throw new ArgumentNullException(nameof(policy)); 34 | 35 | AddPolicy(DefaultPolicyName, policy); 36 | } 37 | 38 | /// 39 | /// Adds a new policy and sets it as the default. 40 | /// 41 | /// A delegate which can use a policy builder to build a policy. 42 | public void AddDefaultPolicy(Action configurePolicy) 43 | { 44 | _ = configurePolicy ?? throw new ArgumentNullException(nameof(configurePolicy)); 45 | 46 | AddPolicy(DefaultPolicyName, configurePolicy); 47 | } 48 | 49 | /// 50 | /// Adds a new policy. 51 | /// 52 | /// The name of the policy. 53 | /// The policy to be added. 54 | public void AddPolicy(string name, ThrottlePolicy policy) 55 | { 56 | _ = name ?? throw new ArgumentNullException(nameof(name)); 57 | _ = policy ?? throw new ArgumentNullException(nameof(policy)); 58 | 59 | PolicyMap[name] = (policy, Task.FromResult(policy)); 60 | } 61 | 62 | /// 63 | /// Adds a new policy. 64 | /// 65 | /// The name of the policy. 66 | /// A delegate which can use a policy builder to build a policy. 67 | public void AddPolicy(string name, Action configurePolicy) 68 | { 69 | _ = name ?? throw new ArgumentNullException(nameof(name)); 70 | _ = configurePolicy ?? throw new ArgumentNullException(nameof(configurePolicy)); 71 | 72 | var policyBuilder = new ThrottlePolicyBuilder(); 73 | configurePolicy(policyBuilder); 74 | var policy = policyBuilder.Build(); 75 | 76 | PolicyMap[name] = (policy, Task.FromResult(policy)); 77 | } 78 | 79 | /// 80 | /// Gets the policy based on the 81 | /// 82 | /// The name of the policy to lookup. 83 | /// The if the policy was added.null otherwise. 84 | public ThrottlePolicy GetPolicy(string name) 85 | { 86 | _ = name ?? throw new ArgumentNullException(nameof(name)); 87 | 88 | if (PolicyMap.TryGetValue(name, out var result)) 89 | { 90 | return result.policy; 91 | } 92 | 93 | return null; 94 | } 95 | 96 | public QuotaExceededDelegate OnQuotaExceeded { get; set; } = DefaultQuotaExceededDelegate; 97 | 98 | private static Task DefaultQuotaExceededDelegate(HttpContext httpContext, ThrottleRule rule, DateTime retryAfter) 99 | { 100 | httpContext.Response.ContentType = "text/plain"; 101 | return httpContext.Response.Body.WriteAsync(_exceededQuoataMessage, 0, _exceededQuoataMessage.Length); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/ThrottlR/Policy/ThrottlePolicyBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace ThrottlR 5 | { 6 | /// 7 | /// Exposes methods to build a policy. 8 | /// 9 | public class ThrottlePolicyBuilder 10 | { 11 | private readonly ThrottlePolicy _policy; 12 | 13 | 14 | public ThrottlePolicyBuilder() 15 | { 16 | _policy = new ThrottlePolicy(); 17 | 18 | SafeList = new SafeListBuilder(this, _policy.SafeList); 19 | } 20 | 21 | /// 22 | /// 23 | /// 24 | public SafeListBuilder SafeList { get; } 25 | 26 | /// 27 | /// Adds the specified to the policy. 28 | /// 29 | /// The rules which the request has. 30 | /// The current policy builder. 31 | public ThrottlePolicyBuilder WithGeneralRule(TimeSpan timeWindow, double quota) 32 | { 33 | if (quota <= 0) 34 | { 35 | throw new ArgumentNullException(nameof(quota)); 36 | } 37 | 38 | _policy.GeneralRules.Add(new ThrottleRule 39 | { 40 | TimeWindow = timeWindow, 41 | Quota = quota 42 | }); 43 | 44 | return this; 45 | } 46 | 47 | /// 48 | /// Adds the specified to the policy. 49 | /// 50 | /// The rules which the request has. 51 | /// The current policy builder. 52 | public ThrottlePolicyBuilder WithGeneralRule(params ThrottleRule[] rules) 53 | { 54 | _ = rules ?? throw new ArgumentNullException(nameof(rules)); 55 | 56 | for (var i = 0; i < rules.Length; i++) 57 | { 58 | _policy.GeneralRules.Add(rules[i]); 59 | } 60 | 61 | return this; 62 | } 63 | 64 | /// 65 | /// Adds the specified to the policy. 66 | /// 67 | /// The rules which the request has. 68 | /// The current policy builder. 69 | public ThrottlePolicyBuilder WithSpecificRule(string identity, TimeSpan timeWindow, double quota) 70 | { 71 | WithSpecificRule(identity, new ThrottleRule { TimeWindow = timeWindow, Quota = quota }); 72 | 73 | return this; 74 | } 75 | 76 | /// 77 | /// Adds the specified to the policy. 78 | /// 79 | /// The rules which the request has. 80 | /// The current policy builder. 81 | public ThrottlePolicyBuilder WithSpecificRule(string identity, params ThrottleRule[] rules) 82 | { 83 | _ = rules ?? throw new ArgumentNullException(nameof(rules)); 84 | 85 | if (string.IsNullOrEmpty(identity)) 86 | { 87 | throw new ArgumentNullException(nameof(identity)); 88 | } 89 | 90 | if (!_policy.SpecificRules.TryGetValue(identity, out var ruleList)) 91 | { 92 | ruleList = new List(); 93 | _policy.SpecificRules.Add(identity, ruleList); 94 | } 95 | 96 | for (var i = 0; i < rules.Length; i++) 97 | { 98 | ruleList.Add(rules[i]); 99 | } 100 | 101 | return this; 102 | } 103 | 104 | /// 105 | /// Adds the specified to the policy. 106 | /// 107 | /// The methods which need to be added to the policy. 108 | /// The current policy builder. 109 | public ThrottlePolicyBuilder WithResolver(IResolver resolver) 110 | { 111 | _ = resolver ?? throw new ArgumentNullException(nameof(resolver)); 112 | 113 | _policy.Resolver = resolver; 114 | 115 | return this; 116 | } 117 | 118 | /// 119 | /// 120 | /// 121 | /// The current policy builder. 122 | public ThrottlePolicyBuilder ApplyPerEndpoint() 123 | { 124 | _policy.ApplyPerEndpoint = true; 125 | 126 | return this; 127 | } 128 | 129 | /// 130 | /// Builds a new using the entries added. 131 | /// 132 | /// The constructed . 133 | public ThrottlePolicy Build() 134 | { 135 | if (_policy.Resolver == null) 136 | { 137 | throw new InvalidOperationException("Resolver unspecified."); 138 | } 139 | 140 | return _policy; 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /test/ThrottlR.Tests/Store/CounterStoreTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace ThrottlR 7 | { 8 | public abstract class CounterStoreTests 9 | { 10 | [Fact] 11 | public async Task Get() 12 | { 13 | var store = CreateCounterStore(); 14 | 15 | var timestamp = DateTime.Now; 16 | 17 | var throttleRule = new ThrottleRule { Quota = 10, TimeWindow = TimeSpan.FromSeconds(10) }; 18 | var throttlerItem = new ThrottlerItem(throttleRule, "policy", "scope", "endpoint"); 19 | 20 | await store.SetAsync(throttlerItem, new Counter(timestamp, 12), null, CancellationToken.None); 21 | 22 | var counter = await store.GetAsync(throttlerItem, CancellationToken.None); 23 | 24 | Assert.True(counter.HasValue); 25 | Assert.Equal(12, counter.Value.Count); 26 | Assert.Equal(timestamp, counter.Value.Timestamp); 27 | } 28 | 29 | [Fact] 30 | public async Task Get_Null() 31 | { 32 | var store = CreateCounterStore(); 33 | 34 | var throttleRule = new ThrottleRule { Quota = 10, TimeWindow = TimeSpan.FromSeconds(10) }; 35 | var throttlerItem = new ThrottlerItem(throttleRule, "policy", "scope", "endpoint"); 36 | 37 | var counter = await store.GetAsync(throttlerItem, CancellationToken.None); 38 | 39 | Assert.False(counter.HasValue); 40 | } 41 | 42 | [Fact] 43 | public async Task Remove() 44 | { 45 | var store = CreateCounterStore(); 46 | 47 | var timestamp = DateTime.Now; 48 | 49 | var throttleRule = new ThrottleRule { Quota = 10, TimeWindow = TimeSpan.FromSeconds(10) }; 50 | var throttlerItem = new ThrottlerItem(throttleRule, "policy", "scope", "endpoint"); 51 | 52 | await store.SetAsync(throttlerItem, new Counter(timestamp, 12), null, CancellationToken.None); 53 | 54 | await store.RemoveAsync(throttlerItem, CancellationToken.None); 55 | 56 | var counter = await store.GetAsync(throttlerItem, CancellationToken.None); 57 | 58 | Assert.False(counter.HasValue); 59 | } 60 | 61 | [Fact] 62 | public async Task Set_Multiple_Get_Each() 63 | { 64 | var store = CreateCounterStore(); 65 | 66 | var timestamp1 = DateTime.Now; 67 | var count1 = 11; 68 | 69 | var throttleRule = new ThrottleRule { Quota = 10, TimeWindow = TimeSpan.FromSeconds(10) }; 70 | var throttlerItem1 = new ThrottlerItem(throttleRule, "policy", "scope1", "endpoint"); 71 | var throttlerItem2 = new ThrottlerItem(throttleRule, "policy", "scope2", "endpoint"); 72 | 73 | await store.SetAsync(throttlerItem1, new Counter(timestamp1, count1), null, CancellationToken.None); 74 | 75 | var timestamp2 = DateTime.Now; 76 | var count2 = 12; 77 | await store.SetAsync(throttlerItem2, new Counter(timestamp2, count2), null, CancellationToken.None); 78 | 79 | var counter1 = await store.GetAsync(throttlerItem1, CancellationToken.None); 80 | var counter2 = await store.GetAsync(throttlerItem2, CancellationToken.None); 81 | 82 | Assert.True(counter1.HasValue); 83 | Assert.Equal(count1, counter1.Value.Count); 84 | Assert.Equal(timestamp1, counter1.Value.Timestamp); 85 | 86 | Assert.True(counter2.HasValue); 87 | Assert.Equal(count2, counter2.Value.Count); 88 | Assert.Equal(timestamp2, counter2.Value.Timestamp); 89 | } 90 | 91 | [Fact] 92 | public async Task Set_Multiple_Remove_One() 93 | { 94 | var store = CreateCounterStore(); 95 | 96 | var timestamp1 = DateTime.Now; 97 | var count1 = 11; 98 | 99 | var throttleRule = new ThrottleRule { Quota = 10, TimeWindow = TimeSpan.FromSeconds(10) }; 100 | var throttlerItem1 = new ThrottlerItem(throttleRule, "policy", "scope1", "endpoint"); 101 | var throttlerItem2 = new ThrottlerItem(throttleRule, "policy", "scope2", "endpoint"); 102 | 103 | await store.SetAsync(throttlerItem1, new Counter(timestamp1, count1), null, CancellationToken.None); 104 | 105 | var timestamp2 = DateTime.Now; 106 | var count2 = 12; 107 | await store.SetAsync(throttlerItem2, new Counter(timestamp2, count2), null, CancellationToken.None); 108 | 109 | await store.RemoveAsync(throttlerItem1, CancellationToken.None); 110 | 111 | var counter1 = await store.GetAsync(throttlerItem1, CancellationToken.None); 112 | var counter2 = await store.GetAsync(throttlerItem2, CancellationToken.None); 113 | 114 | Assert.False(counter1.HasValue); 115 | 116 | Assert.True(counter2.HasValue); 117 | Assert.Equal(count2, counter2.Value.Count); 118 | Assert.Equal(timestamp2, counter2.Value.Timestamp); 119 | } 120 | 121 | public abstract ICounterStore CreateCounterStore(); 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/ThrottlR/Internal/AsyncKeyLock/AsyncKeyLock.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Six Labors and contributors. 2 | // Licensed under the Apache License, Version 2.0. 3 | // Thanks to https://github.com/SixLabors/ImageSharp.Web/ 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Runtime.CompilerServices; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace ThrottlR 11 | { 12 | /// 13 | /// The async key lock prevents multiple asynchronous threads acting upon the same object with the given key at the same time. 14 | /// It is designed so that it does not block unique requests allowing a high throughput. 15 | /// 16 | internal sealed class AsyncKeyLock 17 | { 18 | /// 19 | /// A collection of doorman counters used for tracking references to the same key. 20 | /// 21 | private static readonly Dictionary _keys = new Dictionary(); 22 | 23 | /// 24 | /// A pool of unused doorman counters that can be re-used to avoid allocations. 25 | /// 26 | private static readonly Stack _pool = new Stack(MaxPoolSize); 27 | 28 | /// 29 | /// SpinLock used to protect access to the Keys and Pool collections. 30 | /// 31 | private static SpinLock _spinLock = new SpinLock(false); 32 | 33 | /// 34 | /// Maximum size of the doorman pool. If the pool is already full when releasing 35 | /// a doorman, it is simply left for garbage collection. 36 | /// 37 | private const int MaxPoolSize = 20; 38 | 39 | /// 40 | /// Locks the current thread in read mode asynchronously. 41 | /// 42 | /// The key identifying the specific object to lock against. 43 | /// 44 | /// The that will release the lock. 45 | /// 46 | public async Task ReaderLockAsync(string key) 47 | { 48 | var doorman = GetDoorman(key); 49 | 50 | return await doorman.ReaderLockAsync(); 51 | } 52 | 53 | /// 54 | /// Locks the current thread in write mode asynchronously. 55 | /// 56 | /// The key identifying the specific object to lock against. 57 | /// 58 | /// The that will release the lock. 59 | /// 60 | public async Task WriterLockAsync(string key) 61 | { 62 | var doorman = GetDoorman(key); 63 | 64 | return await doorman.WriterLockAsync(); 65 | } 66 | 67 | /// 68 | /// Gets the doorman for the specified key. If no such doorman exists, an unused doorman 69 | /// is obtained from the pool (or a new one is allocated if the pool is empty), and it's 70 | /// assigned to the requested key. 71 | /// 72 | /// The key for the desired doorman. 73 | /// The . 74 | private static Doorman GetDoorman(string key) 75 | { 76 | Doorman doorman; 77 | var lockTaken = false; 78 | try 79 | { 80 | _spinLock.Enter(ref lockTaken); 81 | 82 | if (!_keys.TryGetValue(key, out doorman)) 83 | { 84 | doorman = (_pool.Count > 0) ? _pool.Pop() : new Doorman(ReleaseDoorman); 85 | doorman.Key = key; 86 | _keys.Add(key, doorman); 87 | } 88 | 89 | doorman.RefCount++; 90 | } 91 | finally 92 | { 93 | if (lockTaken) 94 | { 95 | _spinLock.Exit(); 96 | } 97 | } 98 | 99 | return doorman; 100 | } 101 | 102 | /// 103 | /// Releases a reference to a doorman. If the ref-count hits zero, then the doorman is 104 | /// returned to the pool (or is simply left for the garbage collector to cleanup if the 105 | /// pool is already full). 106 | /// 107 | /// The . 108 | private static void ReleaseDoorman(Doorman doorman) 109 | { 110 | var lockTaken = false; 111 | try 112 | { 113 | _spinLock.Enter(ref lockTaken); 114 | 115 | if (--doorman.RefCount == 0) 116 | { 117 | _keys.Remove(doorman.Key); 118 | if (_pool.Count < MaxPoolSize) 119 | { 120 | doorman.Key = null; 121 | _pool.Push(doorman); 122 | } 123 | } 124 | } 125 | finally 126 | { 127 | if (lockTaken) 128 | { 129 | _spinLock.Exit(); 130 | } 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /test/ThrottlR.Tests/Endpoints/ThrottlerEndpointConventionBuilderExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Routing; 8 | using Xunit; 9 | 10 | namespace ThrottlR.Endpoints 11 | { 12 | public class ThrottlerEndpointConventionBuilderExtensionsTests 13 | { 14 | [Fact] 15 | public void EndpointConventionBuilderExtensions_Throttle() 16 | { 17 | // Arrange 18 | var conventionBuilder = new TestEndpointConventionBuilder(); 19 | 20 | // Act 21 | conventionBuilder.Throttle(); 22 | 23 | var endpointBuilder = new TestEndpointBuilder(); 24 | 25 | for (var i = 0; i < conventionBuilder.Actions.Count; i++) 26 | { 27 | conventionBuilder.Actions[i](endpointBuilder); 28 | } 29 | 30 | var endpoint = endpointBuilder.Build(); 31 | 32 | var throttleMetadata = endpoint.Metadata.GetMetadata(); 33 | Assert.NotNull(throttleMetadata); 34 | Assert.Null(throttleMetadata.PolicyName); 35 | } 36 | 37 | [Fact] 38 | public void EndpointConventionBuilderExtensions_Throttle_Policy() 39 | { 40 | // Arrange 41 | var conventionBuilder = new TestEndpointConventionBuilder(); 42 | 43 | // Act 44 | conventionBuilder.Throttle("policy-1"); 45 | 46 | var endpointBuilder = new TestEndpointBuilder(); 47 | 48 | for (var i = 0; i < conventionBuilder.Actions.Count; i++) 49 | { 50 | conventionBuilder.Actions[i](endpointBuilder); 51 | } 52 | 53 | var endpoint = endpointBuilder.Build(); 54 | 55 | var throttleMetadata = endpoint.Metadata.GetMetadata(); 56 | Assert.NotNull(throttleMetadata); 57 | Assert.Equal("policy-1", throttleMetadata.PolicyName); 58 | } 59 | 60 | [Theory] 61 | [InlineData(null, null, null, null)] 62 | [InlineData(null, null, null, 5)] 63 | [InlineData(null, null, 5, null)] 64 | [InlineData(null, null, 5, 5)] 65 | [InlineData(null, 5, null, null)] 66 | [InlineData(null, 5, null, 5)] 67 | [InlineData(null, 5, 5, null)] 68 | [InlineData(null, 5, 5, 5)] 69 | [InlineData(5, null, null, null)] 70 | [InlineData(5, null, null, 5)] 71 | [InlineData(5, null, 5, null)] 72 | [InlineData(5, null, 5, 5)] 73 | [InlineData(5, 5, null, null)] 74 | [InlineData(5, 5, null, 5)] 75 | [InlineData(5, 5, 5, null)] 76 | [InlineData(5, 5, 5, 5)] 77 | public void EndpointConventionBuilderExtensions_Throttle_Rule(long? perSecond, long? perMinute, long? perHour, long? perDay) 78 | { 79 | // Arrange 80 | var conventionBuilder = new TestEndpointConventionBuilder(); 81 | 82 | // Act 83 | conventionBuilder.Throttle(perSecond, perMinute, perHour, perDay); 84 | 85 | var endpointBuilder = new TestEndpointBuilder(); 86 | 87 | for (var i = 0; i < conventionBuilder.Actions.Count; i++) 88 | { 89 | conventionBuilder.Actions[i](endpointBuilder); 90 | } 91 | 92 | var endpoint = endpointBuilder.Build(); 93 | 94 | var throttleRulesMetadata = endpoint.Metadata.GetMetadata(); 95 | Assert.NotNull(throttleRulesMetadata); 96 | Assert.Null(throttleRulesMetadata.PolicyName); 97 | 98 | var rules = throttleRulesMetadata.GeneralRules; 99 | Assert.NotNull(rules); 100 | 101 | if (perSecond.HasValue) 102 | { 103 | var rule = Assert.Single(rules.Where(x => x.TimeWindow == TimeSpan.FromSeconds(1))); 104 | Assert.Equal(perSecond.Value, rule.Quota); 105 | } 106 | 107 | if (perMinute.HasValue) 108 | { 109 | var rule = Assert.Single(rules.Where(x => x.TimeWindow == TimeSpan.FromMinutes(1))); 110 | Assert.Equal(perMinute.Value, rule.Quota); 111 | } 112 | 113 | if (perHour.HasValue) 114 | { 115 | var rule = Assert.Single(rules.Where(x => x.TimeWindow == TimeSpan.FromHours(1))); 116 | Assert.Equal(perHour.Value, rule.Quota); 117 | } 118 | 119 | if (perDay.HasValue) 120 | { 121 | var rule = Assert.Single(rules.Where(x => x.TimeWindow == TimeSpan.FromDays(1))); 122 | Assert.Equal(perDay.Value, rule.Quota); 123 | } 124 | 125 | var rulesCount = (perSecond.HasValue ? 1 : 0) 126 | + (perMinute.HasValue ? 1 : 0) 127 | + (perHour.HasValue ? 1 : 0) 128 | + (perDay.HasValue ? 1 : 0); 129 | 130 | Assert.Equal(rulesCount, rules.Count); 131 | } 132 | } 133 | 134 | public class TestEndpointConventionBuilder : IEndpointConventionBuilder 135 | { 136 | public List> Actions { get; } = new List>(); 137 | 138 | public void Add(Action convention) 139 | { 140 | Actions.Add(convention); 141 | } 142 | } 143 | 144 | public class TestEndpointBuilder : EndpointBuilder 145 | { 146 | public override Endpoint Build() 147 | { 148 | return new Endpoint(RequestDelegate, new EndpointMetadataCollection(Metadata), DisplayName); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/ThrottlR/Internal/AsyncKeyLock/Doorman.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Six Labors and contributors. 2 | // Licensed under the Apache License, Version 2.0. 3 | // Thanks to https://github.com/SixLabors/ImageSharp.Web/ 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | 8 | namespace ThrottlR 9 | { 10 | /// 11 | /// An asynchronous locker that provides read and write locking policies. 12 | /// 13 | internal sealed class Doorman 14 | { 15 | private readonly Queue> _waitingWriters; 16 | private readonly Task _readerReleaser; 17 | private readonly Task _writerReleaser; 18 | private readonly Action _reset; 19 | private TaskCompletionSource _waitingReader; 20 | private int _readersWaiting; 21 | private int _status; 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// The reset action. 27 | public Doorman(Action reset) 28 | { 29 | _waitingWriters = new Queue>(); 30 | _waitingReader = new TaskCompletionSource(); 31 | _status = 0; 32 | 33 | _readerReleaser = Task.FromResult(new Releaser(this, false)); 34 | _writerReleaser = Task.FromResult(new Releaser(this, true)); 35 | _reset = reset; 36 | } 37 | 38 | /// 39 | /// Gets or sets the key that this doorman is mapped to. 40 | /// 41 | public string Key { get; set; } 42 | 43 | /// 44 | /// Gets or sets the current reference count on this doorman. 45 | /// 46 | public int RefCount { get; set; } 47 | 48 | /// 49 | /// Locks the current thread in read mode asynchronously. 50 | /// 51 | /// The . 52 | public Task ReaderLockAsync() 53 | { 54 | lock (_waitingWriters) 55 | { 56 | if (_status >= 0 && _waitingWriters.Count == 0) 57 | { 58 | ++_status; 59 | return _readerReleaser; 60 | } 61 | else 62 | { 63 | ++_readersWaiting; 64 | return _waitingReader.Task.ContinueWith(t => t.Result); 65 | } 66 | } 67 | } 68 | 69 | /// 70 | /// Locks the current thread in write mode asynchronously. 71 | /// 72 | /// The . 73 | public Task WriterLockAsync() 74 | { 75 | lock (_waitingWriters) 76 | { 77 | if (_status == 0) 78 | { 79 | _status = -1; 80 | return _writerReleaser; 81 | } 82 | else 83 | { 84 | var waiter = new TaskCompletionSource(); 85 | _waitingWriters.Enqueue(waiter); 86 | return waiter.Task; 87 | } 88 | } 89 | } 90 | 91 | private void ReaderRelease() 92 | { 93 | TaskCompletionSource toWake = null; 94 | 95 | lock (_waitingWriters) 96 | { 97 | --_status; 98 | 99 | if (_status == 0) 100 | { 101 | if (_waitingWriters.Count > 0) 102 | { 103 | _status = -1; 104 | toWake = _waitingWriters.Dequeue(); 105 | } 106 | } 107 | } 108 | 109 | _reset(this); 110 | 111 | toWake?.SetResult(new Releaser(this, true)); 112 | } 113 | 114 | private void WriterRelease() 115 | { 116 | TaskCompletionSource toWake = null; 117 | var toWakeIsWriter = false; 118 | 119 | lock (_waitingWriters) 120 | { 121 | if (_waitingWriters.Count > 0) 122 | { 123 | toWake = _waitingWriters.Dequeue(); 124 | toWakeIsWriter = true; 125 | } 126 | else if (_readersWaiting > 0) 127 | { 128 | toWake = _waitingReader; 129 | _status = _readersWaiting; 130 | _readersWaiting = 0; 131 | _waitingReader = new TaskCompletionSource(); 132 | } 133 | else 134 | { 135 | _status = 0; 136 | } 137 | } 138 | 139 | _reset(this); 140 | 141 | toWake?.SetResult(new Releaser(this, toWakeIsWriter)); 142 | } 143 | 144 | public readonly struct Releaser : IDisposable 145 | { 146 | private readonly Doorman _toRelease; 147 | private readonly bool _writer; 148 | 149 | internal Releaser(Doorman toRelease, bool writer) 150 | { 151 | _toRelease = toRelease; 152 | _writer = writer; 153 | } 154 | 155 | public void Dispose() 156 | { 157 | if (_toRelease != null) 158 | { 159 | if (_writer) 160 | { 161 | _toRelease.WriterRelease(); 162 | } 163 | else 164 | { 165 | _toRelease.ReaderRelease(); 166 | } 167 | } 168 | } 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/ThrottlR/Middleware/ThrottlerMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.Logging; 3 | using Microsoft.Extensions.Options; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | 8 | namespace ThrottlR 9 | { 10 | public class ThrottlerMiddleware 11 | { 12 | private static readonly IReadOnlyList _emptyRules = Array.Empty(); 13 | private static readonly Func _onResponseStartingDelegate = OnResponseStarting; 14 | 15 | private readonly RequestDelegate _next; 16 | private readonly IThrottlerService _throttlerService; 17 | private readonly IThrottlePolicyProvider _throttlePolicyProvider; 18 | private readonly ThrottleOptions _options; 19 | private readonly ISystemClock _systemClock; 20 | private readonly ILogger _logger; 21 | 22 | public ThrottlerMiddleware(RequestDelegate next, 23 | IThrottlerService throttlerService, 24 | IThrottlePolicyProvider throttlePolicyProvider, 25 | IOptions options, 26 | ISystemClock systemClock, 27 | ILogger logger) 28 | { 29 | _next = next; 30 | _throttlerService = throttlerService; 31 | _throttlePolicyProvider = throttlePolicyProvider; 32 | _options = options.Value; 33 | _systemClock = systemClock; 34 | _logger = logger; 35 | } 36 | 37 | public async Task Invoke(HttpContext context) 38 | { 39 | var endpoint = context.GetEndpoint(); 40 | 41 | var throttleMetadata = endpoint?.Metadata.GetMetadata(); 42 | if (throttleMetadata == null) 43 | { 44 | await _next.Invoke(context); 45 | return; 46 | } 47 | 48 | var disableThrottle = endpoint?.Metadata.GetMetadata(); 49 | if (disableThrottle != null) 50 | { 51 | await _next.Invoke(context); 52 | return; 53 | } 54 | 55 | var policy = await _throttlePolicyProvider.GetPolicyAsync(throttleMetadata.PolicyName); 56 | if (policy == null) 57 | { 58 | context.Response.StatusCode = StatusCodes.Status500InternalServerError; 59 | return; 60 | } 61 | 62 | if (policy.Resolver == null) 63 | { 64 | context.Response.StatusCode = StatusCodes.Status500InternalServerError; 65 | return; 66 | } 67 | 68 | var scope = await policy.Resolver.ResolveAsync(context); 69 | var isSafe = await CheckSafeScopes(context, policy, scope); 70 | if (isSafe) 71 | { 72 | await _next.Invoke(context); 73 | return; 74 | } 75 | 76 | IReadOnlyList generalRules; 77 | if (throttleMetadata is IThrottleRulesMetadata throttle) 78 | { 79 | // ThrottleRuleMetadata overrides the general rule 80 | generalRules = throttle.GeneralRules; 81 | } 82 | else 83 | { 84 | generalRules = policy.GeneralRules; 85 | } 86 | 87 | IReadOnlyList specificRules; 88 | if (policy.SpecificRules.TryGetValue(scope, out var specificRulesList)) 89 | { 90 | specificRules = specificRulesList; 91 | } 92 | else 93 | { 94 | specificRules = _emptyRules; 95 | } 96 | 97 | var rules = _throttlerService.GetRules(generalRules, specificRules); 98 | 99 | (ThrottleRule rule, Counter counter, bool hasBeenSet) longestRule = (default, default, false); 100 | 101 | foreach (var rule in rules) 102 | { 103 | var throttlerItem = new ThrottlerItem(rule, throttleMetadata.PolicyName, scope, endpoint.DisplayName); 104 | 105 | // increment counter 106 | var counter = await _throttlerService.ProcessRequestAsync(throttlerItem, rule, context.RequestAborted); 107 | 108 | if (rule.Quota > 0) 109 | { 110 | // check if key expired 111 | if (counter.Timestamp + rule.TimeWindow < _systemClock.UtcNow) 112 | { 113 | continue; 114 | } 115 | 116 | // check if limit is reached 117 | if (counter.Count > rule.Quota) 118 | { 119 | LogBlockRequest(throttleMetadata, scope, rule, counter); 120 | 121 | await ReturnQuotaExceededResponse(context, rule, counter); 122 | 123 | return; 124 | } 125 | } 126 | 127 | longestRule = (rule, counter, true); 128 | } 129 | 130 | // set RateLimit headers for the longest period 131 | if (longestRule.hasBeenSet) 132 | { 133 | context.Response.OnStarting(_onResponseStartingDelegate, (longestRule.rule, longestRule.counter, context)); 134 | } 135 | 136 | await _next.Invoke(context); 137 | } 138 | 139 | private static async Task CheckSafeScopes(HttpContext context, ThrottlePolicy policy, string scope) 140 | { 141 | // check safe list 142 | foreach (var kvp in policy.SafeList) 143 | { 144 | var safeList = kvp.Value; 145 | if (safeList.Count == 0) 146 | { 147 | continue; 148 | } 149 | 150 | var resolver = kvp.Key; 151 | string safeScope; 152 | if (policy.Resolver == resolver) 153 | { 154 | safeScope = scope; 155 | } 156 | else 157 | { 158 | safeScope = await resolver.ResolveAsync(context); 159 | } 160 | 161 | for (var i = 0; i < safeList.Count; i++) 162 | { 163 | var safe = safeList[i]; 164 | if (resolver.Matches(safeScope, safe)) 165 | { 166 | return true; 167 | } 168 | } 169 | } 170 | 171 | return false; 172 | } 173 | 174 | private Task ReturnQuotaExceededResponse(HttpContext httpContext, ThrottleRule rule, Counter counter) 175 | { 176 | var retryAfter = counter.Timestamp + rule.TimeWindow; 177 | 178 | httpContext.Response.Headers["Retry-After"] = retryAfter.ToString("R"); 179 | 180 | SetRateLimitHeaders(rule, counter, httpContext); 181 | 182 | httpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; 183 | return _options.OnQuotaExceeded(httpContext, rule, retryAfter); 184 | } 185 | 186 | private void LogBlockRequest(IThrottleMetadata rateLimitMetadata, string identity, ThrottleRule rule, Counter rateLimitCounter) 187 | { 188 | _logger.LogInformation($"Request with identity `{identity}` has been blocked by policy `{rateLimitMetadata.PolicyName}`, quota `{rule.Quota}/{rule.TimeWindow}` exceeded by `{rateLimitCounter.Count}`."); 189 | } 190 | 191 | private static Task OnResponseStarting(object state) 192 | { 193 | var (rule, counter, context) = ((ThrottleRule, Counter, HttpContext))state; 194 | 195 | SetRateLimitHeaders(rule, counter, context); 196 | 197 | return Task.CompletedTask; 198 | } 199 | 200 | private static void SetRateLimitHeaders(ThrottleRule rule, Counter counter, HttpContext context) 201 | { 202 | var remaining = rule.Quota - counter.Count; 203 | context.Response.Headers["RateLimit-Remaining"] = remaining.ToString(); 204 | 205 | var limit = $"{counter.Count}, {rule}"; 206 | context.Response.Headers["RateLimit-Limit"] = limit; 207 | 208 | var reset = counter.Timestamp + rule.TimeWindow; 209 | context.Response.Headers["RateLimit-Reset"] = reset.ToString(); 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /test/ThrottlR.Tests/ThrottlerServiceTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Xunit; 6 | 7 | namespace ThrottlR.Tests 8 | { 9 | public class ThrottlerServiceTests 10 | { 11 | 12 | [Fact] 13 | public void ThrottlerService_GetRules_OnlyGeneralRules() 14 | { 15 | // Arrange 16 | var throttlerService = CreateThrottlerService(); 17 | 18 | var generalRules = new List 19 | { 20 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(8), Quota = 8 }, 21 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(5), Quota = 5 }, 22 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(2), Quota = 2 }, 23 | }; 24 | 25 | // Act 26 | var rules = throttlerService.GetRules(generalRules, Array.Empty()).ToList(); 27 | 28 | // Assert 29 | var rule1 = rules[0]; 30 | Assert.NotNull(rule1); 31 | Assert.Equal(TimeSpan.FromSeconds(8), rule1.TimeWindow); 32 | Assert.Equal(8, rule1.Quota); 33 | 34 | var rule2 = rules[1]; 35 | Assert.NotNull(rule2); 36 | Assert.Equal(TimeSpan.FromSeconds(5), rule2.TimeWindow); 37 | Assert.Equal(5, rule2.Quota); 38 | 39 | var rule3 = rules[2]; 40 | Assert.NotNull(rule3); 41 | Assert.Equal(TimeSpan.FromSeconds(2), rule3.TimeWindow); 42 | Assert.Equal(2, rule3.Quota); 43 | } 44 | 45 | [Fact] 46 | public void ThrottlerService_GetRules_OnlySpeceficRules() 47 | { 48 | // Arrange 49 | var throttlerService = CreateThrottlerService(); 50 | 51 | var specificRules = new List 52 | { 53 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(8), Quota = 8 }, 54 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(5), Quota = 5 }, 55 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(2), Quota = 2 }, 56 | }; 57 | 58 | // Act 59 | var rules = throttlerService.GetRules(Array.Empty(), specificRules).ToList(); 60 | 61 | // Assert 62 | var rule1 = rules[0]; 63 | Assert.NotNull(rule1); 64 | Assert.Equal(TimeSpan.FromSeconds(8), rule1.TimeWindow); 65 | Assert.Equal(8, rule1.Quota); 66 | 67 | var rule2 = rules[1]; 68 | Assert.NotNull(rule2); 69 | Assert.Equal(TimeSpan.FromSeconds(5), rule2.TimeWindow); 70 | Assert.Equal(5, rule2.Quota); 71 | 72 | var rule3 = rules[2]; 73 | Assert.NotNull(rule3); 74 | Assert.Equal(TimeSpan.FromSeconds(2), rule3.TimeWindow); 75 | Assert.Equal(2, rule3.Quota); 76 | } 77 | 78 | [Fact] 79 | public void ThrottlerService_GetRules_Returns_Specefic_Rules_When_Overlap_With_Genral_Rules() 80 | { 81 | // Arrange 82 | var throttlerService = CreateThrottlerService(); 83 | 84 | var generalRules = new List 85 | { 86 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(8), Quota = 88 }, 87 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(5), Quota = 55 }, 88 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(2), Quota = 22 }, 89 | }; 90 | 91 | var specificRules = new List 92 | { 93 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(8), Quota = 8 }, 94 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(5), Quota = 5 }, 95 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(2), Quota = 2 }, 96 | }; 97 | 98 | 99 | // Act 100 | var rules = throttlerService.GetRules(generalRules, specificRules).ToList(); 101 | 102 | // Assert 103 | var rule1 = rules[0]; 104 | Assert.NotNull(rule1); 105 | Assert.Equal(TimeSpan.FromSeconds(8), rule1.TimeWindow); 106 | Assert.Equal(8, rule1.Quota); 107 | 108 | var rule2 = rules[1]; 109 | Assert.NotNull(rule2); 110 | Assert.Equal(TimeSpan.FromSeconds(5), rule2.TimeWindow); 111 | Assert.Equal(5, rule2.Quota); 112 | 113 | var rule3 = rules[2]; 114 | Assert.NotNull(rule3); 115 | Assert.Equal(TimeSpan.FromSeconds(2), rule3.TimeWindow); 116 | Assert.Equal(2, rule3.Quota); 117 | } 118 | 119 | [Fact] 120 | public void ThrottlerService_GetRules_Returns_Both_Specefic_And_Genral_Rules() 121 | { 122 | // Arrange 123 | var throttlerService = CreateThrottlerService(); 124 | 125 | var generalRules = new List 126 | { 127 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(9), Quota = 9 }, 128 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(7), Quota = 7 }, 129 | }; 130 | 131 | var specificRules = new List 132 | { 133 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(8), Quota = 8 }, 134 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(6), Quota = 6 }, 135 | }; 136 | 137 | // Act 138 | var rules = throttlerService.GetRules(generalRules, specificRules).ToList(); 139 | 140 | // Assert 141 | var rule1 = rules[0]; 142 | Assert.NotNull(rule1); 143 | Assert.Equal(TimeSpan.FromSeconds(9), rule1.TimeWindow); 144 | Assert.Equal(9, rule1.Quota); 145 | 146 | var rule2 = rules[1]; 147 | Assert.NotNull(rule2); 148 | Assert.Equal(TimeSpan.FromSeconds(8), rule2.TimeWindow); 149 | Assert.Equal(8, rule2.Quota); 150 | 151 | var rule3 = rules[2]; 152 | Assert.NotNull(rule3); 153 | Assert.Equal(TimeSpan.FromSeconds(7), rule3.TimeWindow); 154 | Assert.Equal(7, rule3.Quota); 155 | 156 | var rule4 = rules[3]; 157 | Assert.NotNull(rule4); 158 | Assert.Equal(TimeSpan.FromSeconds(6), rule4.TimeWindow); 159 | Assert.Equal(6, rule4.Quota); 160 | } 161 | 162 | [Fact] 163 | public void ThrottlerService_GetRules_Returns_More_Specefic_Than_Genral_Rules() 164 | { 165 | // Arrange 166 | var throttlerService = CreateThrottlerService(); 167 | 168 | var generalRules = new List 169 | { 170 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(9), Quota = 9 }, 171 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(7), Quota = 7 }, 172 | }; 173 | 174 | var specificRules = new List 175 | { 176 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(8), Quota = 8 }, 177 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(6), Quota = 6 }, 178 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(5), Quota = 5 }, 179 | }; 180 | 181 | // Act 182 | var rules = throttlerService.GetRules(generalRules, specificRules).ToList(); 183 | 184 | // Assert 185 | var rule1 = rules[0]; 186 | Assert.NotNull(rule1); 187 | Assert.Equal(TimeSpan.FromSeconds(9), rule1.TimeWindow); 188 | Assert.Equal(9, rule1.Quota); 189 | 190 | var rule2 = rules[1]; 191 | Assert.NotNull(rule2); 192 | Assert.Equal(TimeSpan.FromSeconds(8), rule2.TimeWindow); 193 | Assert.Equal(8, rule2.Quota); 194 | 195 | var rule3 = rules[2]; 196 | Assert.NotNull(rule3); 197 | Assert.Equal(TimeSpan.FromSeconds(7), rule3.TimeWindow); 198 | Assert.Equal(7, rule3.Quota); 199 | 200 | var rule4 = rules[3]; 201 | Assert.NotNull(rule4); 202 | Assert.Equal(TimeSpan.FromSeconds(6), rule4.TimeWindow); 203 | Assert.Equal(6, rule4.Quota); 204 | 205 | var rule5 = rules[4]; 206 | Assert.NotNull(rule5); 207 | Assert.Equal(TimeSpan.FromSeconds(5), rule5.TimeWindow); 208 | Assert.Equal(5, rule5.Quota); 209 | } 210 | 211 | [Fact] 212 | public void ThrottlerService_GetRules_Returns_More_Genral_Than_Specefic_Rules() 213 | { 214 | // Arrange 215 | var throttlerService = CreateThrottlerService(); 216 | 217 | var generalRules = new List 218 | { 219 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(9), Quota = 9 }, 220 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(7), Quota = 7 }, 221 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(5), Quota = 5 }, 222 | }; 223 | 224 | var specificRules = new List 225 | { 226 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(8), Quota = 8 }, 227 | new ThrottleRule { TimeWindow = TimeSpan.FromSeconds(6), Quota = 6 }, 228 | }; 229 | 230 | // Act 231 | var rules = throttlerService.GetRules(generalRules, specificRules).ToList(); 232 | 233 | // Assert 234 | var rule1 = rules[0]; 235 | Assert.NotNull(rule1); 236 | Assert.Equal(TimeSpan.FromSeconds(9), rule1.TimeWindow); 237 | Assert.Equal(9, rule1.Quota); 238 | 239 | var rule2 = rules[1]; 240 | Assert.NotNull(rule2); 241 | Assert.Equal(TimeSpan.FromSeconds(8), rule2.TimeWindow); 242 | Assert.Equal(8, rule2.Quota); 243 | 244 | var rule3 = rules[2]; 245 | Assert.NotNull(rule3); 246 | Assert.Equal(TimeSpan.FromSeconds(7), rule3.TimeWindow); 247 | Assert.Equal(7, rule3.Quota); 248 | 249 | var rule4 = rules[3]; 250 | Assert.NotNull(rule4); 251 | Assert.Equal(TimeSpan.FromSeconds(6), rule4.TimeWindow); 252 | Assert.Equal(6, rule4.Quota); 253 | 254 | var rule5 = rules[4]; 255 | Assert.NotNull(rule5); 256 | Assert.Equal(TimeSpan.FromSeconds(5), rule5.TimeWindow); 257 | Assert.Equal(5, rule5.Quota); 258 | } 259 | 260 | private static ThrottlerService CreateThrottlerService() 261 | { 262 | return new ThrottlerService(new TestCounterStore(), new TimeMachine()); 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /test/ThrottlR.Tests/ThrottlerMiddlewareTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.Logging.Abstractions; 7 | using Microsoft.Extensions.Options; 8 | using ThrottlR.Tests; 9 | using Xunit; 10 | 11 | namespace ThrottlR 12 | { 13 | public class ThrottlerMiddlewareTests 14 | { 15 | [Fact] 16 | public async Task Skips_When_There_Is_No_Endpoint() 17 | { 18 | // Arrange 19 | var (next, _, _, middleware, context) = Create(); 20 | 21 | // Act 22 | await middleware.Invoke(context); 23 | 24 | 25 | // Assert 26 | Assert.True(next.Called); 27 | } 28 | 29 | [Fact] 30 | public async Task Skips_When_There_Is_No_ThrottleMetadata() 31 | { 32 | // Arrange 33 | var (next, _, _, middleware, context) = Create(); 34 | 35 | var endpoint = CreateEndpoint(); 36 | context.SetEndpoint(endpoint); 37 | 38 | // Act 39 | await middleware.Invoke(context); 40 | 41 | 42 | // Assert 43 | Assert.True(next.Called); 44 | } 45 | 46 | [Fact] 47 | public async Task Returns_500_When_There_Is_No_Policy() 48 | { 49 | // Arrange 50 | var (next, _, _, middleware, context) = Create(); 51 | 52 | var endpoint = CreateEndpoint(new EnableThrottle()); 53 | context.SetEndpoint(endpoint); 54 | 55 | // Act 56 | await middleware.Invoke(context); 57 | 58 | 59 | // Assert 60 | Assert.False(next.Called); 61 | Assert.Equal(StatusCodes.Status500InternalServerError, context.Response.StatusCode); 62 | } 63 | 64 | [Fact] 65 | public async Task Returns_500_When_There_Is_No_Resolver() 66 | { 67 | // Arrange 68 | var (next, _, throttleOptions, middleware, context) = Create(); 69 | 70 | var endpoint = CreateEndpoint(new EnableThrottle()); 71 | context.SetEndpoint(endpoint); 72 | 73 | throttleOptions.AddDefaultPolicy(new ThrottlePolicy { Resolver = null }); 74 | 75 | // Act 76 | await middleware.Invoke(context); 77 | 78 | 79 | // Assert 80 | Assert.False(next.Called); 81 | Assert.Equal(StatusCodes.Status500InternalServerError, context.Response.StatusCode); 82 | } 83 | 84 | [Fact] 85 | public async Task Skips_When_Identity_Is_In_SafeList() 86 | { 87 | // Arrange 88 | var (next, timeMachine, throttleOptions, middleware, context) = Create(); 89 | 90 | var endpoint = CreateEndpoint(new EnableThrottle()); 91 | context.SetEndpoint(endpoint); 92 | context.Connection.RemoteIpAddress = new IPAddress(new byte[] { 10, 20, 10, 47 }); 93 | 94 | throttleOptions.AddDefaultPolicy(x => x.SafeList.IP("10.20.10.47") 95 | .WithGeneralRule(TimeSpan.FromSeconds(1), 1)); 96 | 97 | // Act 98 | await middleware.Invoke(context); 99 | 100 | // Assert 101 | Assert.True(next.Called); 102 | } 103 | 104 | [Fact] 105 | public async Task Quota_Exceeds_In_A_Time_Window() 106 | { 107 | // Arrange 108 | var (next, timeMachine, throttleOptions, middleware, context) = Create(); 109 | 110 | var endpoint = CreateEndpoint(new EnableThrottle()); 111 | context.SetEndpoint(endpoint); 112 | 113 | throttleOptions.AddDefaultPolicy(x => x.WithGeneralRule(TimeSpan.FromSeconds(1), 1)); 114 | 115 | await middleware.Invoke(context); 116 | 117 | Assert.True(next.Called); 118 | 119 | next.Called = false; 120 | 121 | 122 | await middleware.Invoke(context); 123 | 124 | Assert.False(next.Called); 125 | Assert.Equal(StatusCodes.Status429TooManyRequests, context.Response.StatusCode); 126 | Assert.Equal("text/plain", context.Response.ContentType); 127 | } 128 | 129 | [Fact] 130 | public async Task Custom_Quota_Exceeds_Delegate() 131 | { 132 | // Arrange 133 | var (next, timeMachine, throttleOptions, middleware, context) = Create(); 134 | 135 | var endpoint = CreateEndpoint(new EnableThrottle()); 136 | context.SetEndpoint(endpoint); 137 | 138 | var cuotaExceededDelegateCalled = false; 139 | throttleOptions.AddDefaultPolicy(x => x.WithGeneralRule(TimeSpan.FromSeconds(1), 1)); 140 | throttleOptions.OnQuotaExceeded = (HttpContext httpContext, ThrottleRule rule, DateTime retryAfter) => 141 | { 142 | cuotaExceededDelegateCalled = true; 143 | return Task.CompletedTask; 144 | }; 145 | 146 | await middleware.Invoke(context); 147 | 148 | Assert.True(next.Called); 149 | 150 | next.Called = false; 151 | 152 | 153 | await middleware.Invoke(context); 154 | 155 | Assert.False(next.Called); 156 | Assert.True(cuotaExceededDelegateCalled); 157 | Assert.Equal(StatusCodes.Status429TooManyRequests, context.Response.StatusCode); 158 | } 159 | 160 | [Fact] 161 | public async Task Quota_Exceeds_In_A_Time_Windows_Skips_In_The_Next_Window() 162 | { 163 | // Arrange 164 | var (next, timeMachine, throttleOptions, middleware, context) = Create(); 165 | 166 | var endpoint = CreateEndpoint(new EnableThrottle()); 167 | context.SetEndpoint(endpoint); 168 | 169 | throttleOptions.AddDefaultPolicy(x => x.WithGeneralRule(TimeSpan.FromSeconds(1), 1)); 170 | 171 | // 00:00:00.0 172 | await middleware.Invoke(context); 173 | 174 | Assert.True(next.Called); 175 | 176 | 177 | // 00:00:00.0 178 | next.Called = false; 179 | await middleware.Invoke(context); 180 | 181 | Assert.False(next.Called); 182 | Assert.Equal(StatusCodes.Status429TooManyRequests, context.Response.StatusCode); 183 | 184 | 185 | // 00:00:01.1 186 | timeMachine.Travel(TimeSpan.FromMilliseconds(1001)); 187 | next.Called = false; 188 | await middleware.Invoke(context); 189 | 190 | Assert.True(next.Called); 191 | } 192 | 193 | [Fact] 194 | public async Task Skips_When_There_Is_ThrottleMetadata_And_DisableThrottle() 195 | { 196 | // Arrange 197 | var (next, _, _, middleware, context) = Create(); 198 | 199 | var endpoint = CreateEndpoint(new EnableThrottle(), DisableThrottle.Instance); 200 | context.SetEndpoint(endpoint); 201 | 202 | await middleware.Invoke(context); 203 | 204 | // Assert 205 | Assert.True(next.Called); 206 | } 207 | 208 | [Fact] 209 | public async Task PerEndpointPolicy() 210 | { 211 | // Arrange 212 | var (next, timeMachine, throttleOptions, middleware, context) = Create(); 213 | 214 | throttleOptions.AddDefaultPolicy(x => x.WithGeneralRule(TimeSpan.FromSeconds(1), 1) 215 | .ApplyPerEndpoint()); 216 | 217 | // 00:00:00.0 218 | var endpoint1 = CreateEndpoint("Endpoint-1", new EnableThrottle()); 219 | context.SetEndpoint(endpoint1); 220 | 221 | await middleware.Invoke(context); 222 | 223 | Assert.True(next.Called); 224 | 225 | 226 | // 00:00:00.0 227 | next.Called = false; 228 | await middleware.Invoke(context); 229 | 230 | Assert.False(next.Called); 231 | Assert.Equal(StatusCodes.Status429TooManyRequests, context.Response.StatusCode); 232 | 233 | 234 | // 00:00:00.0 235 | var endpoint2 = CreateEndpoint("Endpoint-2", new EnableThrottle()); 236 | context.SetEndpoint(endpoint2); 237 | 238 | await middleware.Invoke(context); 239 | 240 | Assert.True(next.Called); 241 | 242 | 243 | // 00:00:00.0 244 | next.Called = false; 245 | await middleware.Invoke(context); 246 | 247 | Assert.False(next.Called); 248 | Assert.Equal(StatusCodes.Status429TooManyRequests, context.Response.StatusCode); 249 | } 250 | 251 | [Fact] 252 | public async Task ThrottleAttribute_Override_GeneralRule() 253 | { 254 | // Arrange 255 | var (next, timeMachine, throttleOptions, middleware, context) = Create(); 256 | 257 | var endpoint = CreateEndpoint(new ThrottleAttribute { PerSecond = 2 }); 258 | context.SetEndpoint(endpoint); 259 | 260 | throttleOptions.AddDefaultPolicy(x => x.WithGeneralRule(TimeSpan.FromSeconds(1), 1)); 261 | 262 | // 00:00:00.0 263 | await middleware.Invoke(context); 264 | 265 | Assert.True(next.Called); 266 | 267 | 268 | // 00:00:00.0 269 | next.Called = false; 270 | await middleware.Invoke(context); 271 | 272 | Assert.True(next.Called); 273 | } 274 | 275 | [Fact] 276 | public async Task ThrottleAttribute_With_Policy() 277 | { 278 | // Arrange 279 | var (next, timeMachine, throttleOptions, middleware, context) = Create(); 280 | 281 | var endpoint = CreateEndpoint(new ThrottleAttribute { PerSecond = 1, PolicyName = "policy-1" }); 282 | context.SetEndpoint(endpoint); 283 | 284 | throttleOptions.AddPolicy("policy-1", builder => { }); 285 | 286 | // 00:00:00.0 287 | await middleware.Invoke(context); 288 | 289 | Assert.True(next.Called); 290 | 291 | 292 | // 00:00:00.0 293 | next.Called = false; 294 | await middleware.Invoke(context); 295 | 296 | Assert.False(next.Called); 297 | Assert.Equal(StatusCodes.Status429TooManyRequests, context.Response.StatusCode); 298 | } 299 | 300 | private (Result next, TimeMachine timeMachine, ThrottleOptions throttleOptions, ThrottlerMiddleware middleware, HttpContext context) Create() 301 | { 302 | var result = new Result { Called = false }; 303 | Task Next(HttpContext context) 304 | { 305 | result.Called = true; 306 | return Task.CompletedTask; 307 | } 308 | 309 | var timeMachine = new TimeMachine(); 310 | var throttleOptions = new ThrottleOptions(); 311 | 312 | var middleware = CreateMiddleware(throttleOptions, timeMachine, Next); 313 | 314 | var context = new DefaultHttpContext(); 315 | var endpoint = CreateEndpoint(); 316 | context.SetEndpoint(endpoint); 317 | 318 | return (result, timeMachine, throttleOptions, middleware, context); 319 | } 320 | 321 | public class Result 322 | { 323 | public bool Called { get; set; } 324 | } 325 | 326 | private Endpoint CreateEndpoint(params object[] throttleMetadata) 327 | { 328 | return CreateEndpoint(string.Empty, throttleMetadata); 329 | } 330 | 331 | private Endpoint CreateEndpoint(string endpointName = "", params object[] throttleMetadata) 332 | { 333 | return new Endpoint(context => Task.CompletedTask, new EndpointMetadataCollection((IEnumerable)throttleMetadata), endpointName); 334 | } 335 | 336 | public ThrottlerMiddleware CreateMiddleware(ThrottleOptions throttleOptions, TimeMachine timeMachine, RequestDelegate next) 337 | { 338 | var options = Options.Create(throttleOptions); 339 | var throttlerService = CreateThrottleService(timeMachine); 340 | var throttlePolicyProvider = new DefaultThrottlePolicyProvider(options); 341 | var middleware = new ThrottlerMiddleware(next, throttlerService, throttlePolicyProvider, options, timeMachine, NullLogger.Instance); 342 | 343 | return middleware; 344 | } 345 | 346 | private static ThrottlerService CreateThrottleService(TimeMachine timeMachine) 347 | { 348 | var store = new TestCounterStore(); 349 | var throttlerService = new ThrottlerService(store, timeMachine); 350 | 351 | return throttlerService; 352 | } 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; EditorConfig to support per-solution formatting. 2 | ; Use the EditorConfig VS add-in to make this work. 3 | ; http://editorconfig.org/ 4 | ; 5 | ; Here are some resources for what's supported for .NET/C# 6 | ; https://kent-boogaart.com/blog/editorconfig-reference-for-c-developers 7 | ; https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference?view=vs-2017 8 | ; 9 | ; Be **careful** editing this because some of the rules don't support adding a severity level 10 | ; For instance if you change to `dotnet_sort_system_directives_first = true:warning` (adding `:warning`) 11 | ; then the rule will be silently ignored. 12 | 13 | ; This is the default for the codeline. 14 | root = true 15 | 16 | [*] 17 | indent_style = space 18 | charset = utf-8 19 | trim_trailing_whitespace = true 20 | insert_final_newline = true 21 | 22 | [*.cs] 23 | indent_size = 4 24 | 25 | [*.{xml,config,*proj,nuspec,props,resx,targets,yml,tasks}] 26 | indent_size = 2 27 | 28 | [*.json] 29 | indent_size = 2 30 | 31 | [*.sh] 32 | indent_size = 4 33 | end_of_line = lf 34 | 35 | 36 | ## The .NET Style 37 | ## Things that are commented out are available to configure but we generally don't have a preference. 38 | [*.{cs, vb}] 39 | 40 | # Organize using directives 41 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-formatting-conventions?view=vs-2019#organize-using-directives 42 | dotnet_sort_system_directives_first = true 43 | # dotnet_separate_import_directive_groups = false 44 | 45 | ## TODO: Swap things back to suggestion from error before merging. Just doing this to find the violations quickly and fix them. 46 | 47 | # Naming rules 48 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-naming-conventions?view=vs-2019 49 | 50 | # Inspired by, but modified from, the Roslyn style: https://github.com/dotnet/roslyn/blob/75fcec13fdaa6f0f38f8ef1d7238d947df55cf5e/.editorconfig#L59 51 | 52 | # Non-private static fields are PascalCase 53 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion 54 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields 55 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style 56 | 57 | dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field 58 | dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected 59 | dotnet_naming_symbols.non_private_static_fields.required_modifiers = static 60 | 61 | dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case 62 | 63 | # Non-private readonly fields are PascalCase 64 | dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion 65 | dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields 66 | dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style 67 | 68 | dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field 69 | dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected 70 | dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly 71 | 72 | dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case 73 | 74 | # Constants are PascalCase 75 | dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion 76 | dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants 77 | dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style 78 | 79 | dotnet_naming_symbols.constants.applicable_kinds = field, local 80 | dotnet_naming_symbols.constants.required_modifiers = const 81 | 82 | dotnet_naming_style.constant_style.capitalization = pascal_case 83 | 84 | # Instance fields are camelCase and start with _ 85 | dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion 86 | dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields 87 | dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style 88 | 89 | dotnet_naming_symbols.instance_fields.applicable_kinds = field 90 | 91 | dotnet_naming_style.instance_field_style.capitalization = camel_case 92 | dotnet_naming_style.instance_field_style.required_prefix = _ 93 | 94 | # Locals and parameters are camelCase 95 | dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion 96 | dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters 97 | dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style 98 | 99 | dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local 100 | 101 | dotnet_naming_style.camel_case_style.capitalization = camel_case 102 | 103 | # Local functions are PascalCase 104 | dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion 105 | dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions 106 | dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style 107 | 108 | dotnet_naming_symbols.local_functions.applicable_kinds = local_function 109 | 110 | dotnet_naming_style.local_function_style.capitalization = pascal_case 111 | 112 | # By default, name items with PascalCase 113 | dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion 114 | dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members 115 | dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style 116 | 117 | dotnet_naming_symbols.all_members.applicable_kinds = * 118 | 119 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 120 | 121 | # Don't use "this." anywhere. 122 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#this-and-me 123 | dotnet_style_qualification_for_field = false:error 124 | dotnet_style_qualification_for_property = false:error 125 | dotnet_style_qualification_for_method = false:error 126 | dotnet_style_qualification_for_event = false:error 127 | 128 | # Use 'string' instead of 'String', 'int' instead of 'Int32', etc. 129 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#language-keywords 130 | dotnet_style_predefined_type_for_locals_parameters_members = true:error 131 | dotnet_style_predefined_type_for_member_access = true:error 132 | 133 | # Explicitly specify modifiers and always mark fields that can be 'readonly' 134 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#normalize-modifiers 135 | dotnet_style_require_accessibility_modifiers = always:error 136 | dotnet_style_readonly_field = true:error 137 | 138 | # We generally don't have rules about parentheses. 139 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#parentheses-preferences 140 | # dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 141 | # dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 142 | # dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 143 | # dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 144 | 145 | # Expression preferences 146 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#expression-level-preferences 147 | dotnet_style_prefer_auto_properties = true:error 148 | # dotnet_style_object_initializer = true:suggestion 149 | # dotnet_style_collection_initializer = true:suggestion 150 | # dotnet_style_explicit_tuple_names = true:suggestion 151 | # dotnet_style_prefer_inferred_tuple_names = true:suggestion 152 | # dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 153 | # dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion 154 | # dotnet_style_prefer_conditional_expression_over_return = true:suggestion 155 | # dotnet_style_prefer_compound_assignment = true:suggestion 156 | 157 | # Null-checking preferences 158 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#null-checking-preferences 159 | # dotnet_style_coalesce_expression = true:suggestion 160 | # dotnet_style_null_propagation = true:suggestion 161 | 162 | # Parameter preferences 163 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#parameter-preferences 164 | dotnet_code_quality_unused_parameters = all:error 165 | 166 | ## C#-specific style 167 | [*.cs] 168 | 169 | # Preferred order of modfiers. 170 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#normalize-modifiers 171 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:error 172 | 173 | # Implicit and explicit types 174 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#implicit-and-explicit-types 175 | csharp_style_var_for_built_in_types = true:error 176 | csharp_style_var_when_type_is_apparent = true:error 177 | csharp_style_var_elsewhere = true:error 178 | 179 | # Expression-bodied members 180 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#expression-bodied-members 181 | # csharp_style_expression_bodied_methods = false:silent 182 | # csharp_style_expression_bodied_constructors = false:silent 183 | # csharp_style_expression_bodied_operators = false:silent 184 | # csharp_style_expression_bodied_properties = true:suggestion 185 | # csharp_style_expression_bodied_indexers = true:suggestion 186 | # csharp_style_expression_bodied_accessors = true:suggestion 187 | # csharp_style_expression_bodied_lambdas = true:silent 188 | # csharp_style_expression_bodied_local_functions = false:silent 189 | 190 | # Pattern matching 191 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#pattern-matching 192 | # csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 193 | # csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 194 | 195 | # Inlined variable declarations (out var i) 196 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#inlined-variable-declarations 197 | csharp_style_inlined_variable_declaration = true:error 198 | 199 | # Expression preferences ('default' vs 'default(T)') 200 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#c-expression-level-preferences 201 | csharp_prefer_simple_default_expression = true:error 202 | 203 | # Null-checking preferences 204 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#c-null-checking-preferences 205 | # csharp_style_throw_expression = true:suggestion 206 | csharp_style_conditional_delegate_call = true:error 207 | 208 | # Code block preferences 209 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#code-block-preferences 210 | csharp_prefer_braces = true:error 211 | 212 | # Unused value preferences 213 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#unused-value-preferences 214 | # This one is S P I C Y. Enabling either option *forces* you to put something on the left-side when you call a method that returns a value. 215 | # csharp_style_unused_value_expression_statement_preference = discard_variable:silent 216 | # csharp_style_unused_value_assignment_preference = discard_variable:suggestion 217 | 218 | # Index and Range preferences 219 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#index-and-range-preferences 220 | # csharp_style_prefer_index_operator = true:suggestion 221 | # csharp_style_prefer_range_operator = true:suggestion 222 | 223 | # Miscellaneous preferences 224 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-language-conventions?view=vs-2019#miscellaneous-preferences 225 | csharp_using_directive_placement = outside_namespace:error 226 | # csharp_style_deconstructed_variable_declaration = true:suggestion 227 | # csharp_style_pattern_local_over_anonymous_function = true:suggestion 228 | # csharp_prefer_static_local_function = true:suggestion 229 | # csharp_prefer_simple_using_statement = true:suggestion 230 | # csharp_style_prefer_switch_expression = true:suggestion 231 | 232 | # New-line options 233 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-formatting-conventions?view=vs-2019#new-line-options 234 | csharp_new_line_before_open_brace = methods, properties, control_blocks, types, anonymous_methods, lambdas, object_collection_array_initializers 235 | csharp_new_line_before_else = true 236 | csharp_new_line_before_catch = true 237 | csharp_new_line_before_finally = true 238 | csharp_new_line_before_members_in_object_initializers = true 239 | csharp_new_line_before_members_in_anonymous_types = true 240 | csharp_new_line_between_query_expression_clauses = true 241 | 242 | # Indentation options 243 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-formatting-conventions?view=vs-2019#indentation-options 244 | csharp_indent_case_contents = true 245 | csharp_indent_switch_labels = true 246 | csharp_indent_labels = one_less_than_current 247 | csharp_indent_block_contents = true 248 | csharp_indent_braces = false 249 | csharp_indent_case_contents_when_block = true 250 | 251 | # Spacing options 252 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-formatting-conventions?view=vs-2019#spacing-options 253 | csharp_space_after_cast = false 254 | csharp_space_after_keywords_in_control_flow_statements = true 255 | csharp_space_between_parentheses = false 256 | csharp_space_before_colon_in_inheritance_clause = true 257 | csharp_space_after_colon_in_inheritance_clause = true 258 | csharp_space_around_binary_operators = before_and_after 259 | csharp_space_between_method_declaration_parameter_list_parentheses = false 260 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 261 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 262 | csharp_space_between_method_call_parameter_list_parentheses = false 263 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 264 | csharp_space_between_method_call_name_and_opening_parenthesis = false 265 | csharp_space_after_comma = true 266 | csharp_space_before_comma = false 267 | csharp_space_after_dot = false 268 | csharp_space_before_dot = false 269 | csharp_space_after_semicolon_in_for_statement = true 270 | csharp_space_before_semicolon_in_for_statement = false 271 | csharp_space_around_declaration_statements = false 272 | csharp_space_before_open_square_brackets = false 273 | csharp_space_between_empty_square_brackets = false 274 | csharp_space_between_square_brackets = false 275 | 276 | # Wrap options 277 | # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-formatting-conventions?view=vs-2019#wrap-options 278 | csharp_preserve_single_line_statements = true 279 | csharp_preserve_single_line_blocks = true 280 | --------------------------------------------------------------------------------