├── .github
└── FUNDING.yml
├── .gitignore
├── LICENSE.md
├── README.md
├── ThrottledWebApi.ClientDemo
├── Program.cs
├── README.md
├── ThrottledHttpClient.cs
└── ThrottledWebApi.ClientDemo.csproj
├── ThrottledWebApi.IntegrationTests
├── IpRateLimitOptionsTests.cs
├── ThrottledWebApi.IntegrationTests.csproj
└── ValuesControllerTests.cs
├── ThrottledWebApi.sln
└── ThrottledWebApi
├── Controllers
└── ValuesController.cs
├── Program.cs
├── Properties
└── launchSettings.json
├── Startup.cs
├── ThrottledWebApi.csproj
├── appsettings.Development.json
└── appsettings.json
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: changhuixu
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Aa][Rr][Mm]/
27 | [Aa][Rr][Mm]64/
28 | bld/
29 | [Bb]in/
30 | [Oo]bj/
31 | [Ll]og/
32 |
33 | # Visual Studio 2015/2017 cache/options directory
34 | .vs/
35 | # Uncomment if you have tasks that create the project's static files in wwwroot
36 | #wwwroot/
37 |
38 | # Visual Studio 2017 auto generated files
39 | Generated\ Files/
40 |
41 | # MSTest test Results
42 | [Tt]est[Rr]esult*/
43 | [Bb]uild[Ll]og.*
44 |
45 | # NUnit
46 | *.VisualState.xml
47 | TestResult.xml
48 | nunit-*.xml
49 |
50 | # Build Results of an ATL Project
51 | [Dd]ebugPS/
52 | [Rr]eleasePS/
53 | dlldata.c
54 |
55 | # Benchmark Results
56 | BenchmarkDotNet.Artifacts/
57 |
58 | # .NET Core
59 | project.lock.json
60 | project.fragment.lock.json
61 | artifacts/
62 |
63 | # StyleCop
64 | StyleCopReport.xml
65 |
66 | # Files built by Visual Studio
67 | *_i.c
68 | *_p.c
69 | *_h.h
70 | *.ilk
71 | *.meta
72 | *.obj
73 | *.iobj
74 | *.pch
75 | *.pdb
76 | *.ipdb
77 | *.pgc
78 | *.pgd
79 | *.rsp
80 | *.sbr
81 | *.tlb
82 | *.tli
83 | *.tlh
84 | *.tmp
85 | *.tmp_proj
86 | *_wpftmp.csproj
87 | *.log
88 | *.vspscc
89 | *.vssscc
90 | .builds
91 | *.pidb
92 | *.svclog
93 | *.scc
94 |
95 | # Chutzpah Test files
96 | _Chutzpah*
97 |
98 | # Visual C++ cache files
99 | ipch/
100 | *.aps
101 | *.ncb
102 | *.opendb
103 | *.opensdf
104 | *.sdf
105 | *.cachefile
106 | *.VC.db
107 | *.VC.VC.opendb
108 |
109 | # Visual Studio profiler
110 | *.psess
111 | *.vsp
112 | *.vspx
113 | *.sap
114 |
115 | # Visual Studio Trace Files
116 | *.e2e
117 |
118 | # TFS 2012 Local Workspace
119 | $tf/
120 |
121 | # Guidance Automation Toolkit
122 | *.gpState
123 |
124 | # ReSharper is a .NET coding add-in
125 | _ReSharper*/
126 | *.[Rr]e[Ss]harper
127 | *.DotSettings.user
128 |
129 | # JustCode is a .NET coding add-in
130 | .JustCode
131 |
132 | # TeamCity is a build add-in
133 | _TeamCity*
134 |
135 | # DotCover is a Code Coverage Tool
136 | *.dotCover
137 |
138 | # AxoCover is a Code Coverage Tool
139 | .axoCover/*
140 | !.axoCover/settings.json
141 |
142 | # Visual Studio code coverage results
143 | *.coverage
144 | *.coveragexml
145 |
146 | # NCrunch
147 | _NCrunch_*
148 | .*crunch*.local.xml
149 | nCrunchTemp_*
150 |
151 | # MightyMoose
152 | *.mm.*
153 | AutoTest.Net/
154 |
155 | # Web workbench (sass)
156 | .sass-cache/
157 |
158 | # Installshield output folder
159 | [Ee]xpress/
160 |
161 | # DocProject is a documentation generator add-in
162 | DocProject/buildhelp/
163 | DocProject/Help/*.HxT
164 | DocProject/Help/*.HxC
165 | DocProject/Help/*.hhc
166 | DocProject/Help/*.hhk
167 | DocProject/Help/*.hhp
168 | DocProject/Help/Html2
169 | DocProject/Help/html
170 |
171 | # Click-Once directory
172 | publish/
173 |
174 | # Publish Web Output
175 | *.[Pp]ublish.xml
176 | *.azurePubxml
177 | # Note: Comment the next line if you want to checkin your web deploy settings,
178 | # but database connection strings (with potential passwords) will be unencrypted
179 | *.pubxml
180 | *.publishproj
181 |
182 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
183 | # checkin your Azure Web App publish settings, but sensitive information contained
184 | # in these scripts will be unencrypted
185 | PublishScripts/
186 |
187 | # NuGet Packages
188 | *.nupkg
189 | # NuGet Symbol Packages
190 | *.snupkg
191 | # The packages folder can be ignored because of Package Restore
192 | **/[Pp]ackages/*
193 | # except build/, which is used as an MSBuild target.
194 | !**/[Pp]ackages/build/
195 | # Uncomment if necessary however generally it will be regenerated when needed
196 | #!**/[Pp]ackages/repositories.config
197 | # NuGet v3's project.json files produces more ignorable files
198 | *.nuget.props
199 | *.nuget.targets
200 |
201 | # Microsoft Azure Build Output
202 | csx/
203 | *.build.csdef
204 |
205 | # Microsoft Azure Emulator
206 | ecf/
207 | rcf/
208 |
209 | # Windows Store app package directories and files
210 | AppPackages/
211 | BundleArtifacts/
212 | Package.StoreAssociation.xml
213 | _pkginfo.txt
214 | *.appx
215 | *.appxbundle
216 | *.appxupload
217 |
218 | # Visual Studio cache files
219 | # files ending in .cache can be ignored
220 | *.[Cc]ache
221 | # but keep track of directories ending in .cache
222 | !?*.[Cc]ache/
223 |
224 | # Others
225 | ClientBin/
226 | ~$*
227 | *~
228 | *.dbmdl
229 | *.dbproj.schemaview
230 | *.jfm
231 | *.pfx
232 | *.publishsettings
233 | orleans.codegen.cs
234 |
235 | # Including strong name files can present a security risk
236 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
237 | #*.snk
238 |
239 | # Since there are multiple workflows, uncomment next line to ignore bower_components
240 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
241 | #bower_components/
242 |
243 | # RIA/Silverlight projects
244 | Generated_Code/
245 |
246 | # Backup & report files from converting an old project file
247 | # to a newer Visual Studio version. Backup files are not needed,
248 | # because we have git ;-)
249 | _UpgradeReport_Files/
250 | Backup*/
251 | UpgradeLog*.XML
252 | UpgradeLog*.htm
253 | ServiceFabricBackup/
254 | *.rptproj.bak
255 |
256 | # SQL Server files
257 | *.mdf
258 | *.ldf
259 | *.ndf
260 |
261 | # Business Intelligence projects
262 | *.rdl.data
263 | *.bim.layout
264 | *.bim_*.settings
265 | *.rptproj.rsuser
266 | *- [Bb]ackup.rdl
267 | *- [Bb]ackup ([0-9]).rdl
268 | *- [Bb]ackup ([0-9][0-9]).rdl
269 |
270 | # Microsoft Fakes
271 | FakesAssemblies/
272 |
273 | # GhostDoc plugin setting file
274 | *.GhostDoc.xml
275 |
276 | # Node.js Tools for Visual Studio
277 | .ntvs_analysis.dat
278 | node_modules/
279 |
280 | # Visual Studio 6 build log
281 | *.plg
282 |
283 | # Visual Studio 6 workspace options file
284 | *.opt
285 |
286 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
287 | *.vbw
288 |
289 | # Visual Studio LightSwitch build output
290 | **/*.HTMLClient/GeneratedArtifacts
291 | **/*.DesktopClient/GeneratedArtifacts
292 | **/*.DesktopClient/ModelManifest.xml
293 | **/*.Server/GeneratedArtifacts
294 | **/*.Server/ModelManifest.xml
295 | _Pvt_Extensions
296 |
297 | # Paket dependency manager
298 | .paket/paket.exe
299 | paket-files/
300 |
301 | # FAKE - F# Make
302 | .fake/
303 |
304 | # CodeRush personal settings
305 | .cr/personal
306 |
307 | # Python Tools for Visual Studio (PTVS)
308 | __pycache__/
309 | *.pyc
310 |
311 | # Cake - Uncomment if you are using it
312 | # tools/**
313 | # !tools/packages.config
314 |
315 | # Tabs Studio
316 | *.tss
317 |
318 | # Telerik's JustMock configuration file
319 | *.jmconfig
320 |
321 | # BizTalk build output
322 | *.btp.cs
323 | *.btm.cs
324 | *.odx.cs
325 | *.xsd.cs
326 |
327 | # OpenCover UI analysis results
328 | OpenCover/
329 |
330 | # Azure Stream Analytics local run output
331 | ASALocalRun/
332 |
333 | # MSBuild Binary and Structured Log
334 | *.binlog
335 |
336 | # NVidia Nsight GPU debugger configuration file
337 | *.nvuser
338 |
339 | # MFractors (Xamarin productivity tool) working folder
340 | .mfractor/
341 |
342 | # Local History for Visual Studio
343 | .localhistory/
344 |
345 | # BeatPulse healthcheck temp database
346 | healthchecksdb
347 |
348 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
349 | MigrationBackup/
350 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 |
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2021 Changhui Xu
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Rate Limiting API Endpoints in ASP.NET Core
2 |
3 |
4 |
5 | ## [Medium Post 1: Rate Limiting API Endpoints in ASP.NET Core](https://medium.com/@changhuixu/rate-limiting-api-endpoints-in-asp-net-core-926e31428017)
6 |
7 | This post shows (1) an ASP.NET Core Web API demo project which limits inbound HTTP requests from the internet and (2) an integration test project which tests the Web API rate limit using a `TestServer` and an `HttpClient`. One of the integration tests shows an example approach to send concurrent API requests using a `Semaphore` in order to comply with the rate limit in the Web API application.
8 |
9 | ## [Medium Post 2: Throttling Concurrent Outgoing HTTP Requests in .NET Core](https://medium.com/@changhuixu/throttling-concurrent-outgoing-http-requests-in-net-core-404b5acd987b)
10 |
11 | This post will go over how to make concurrent outgoing HTTP requests _on the client side_. The goal is to let the HTTP Client send concurrent requests at the maximum allowed rate which is set by the server, for example, at a maximum rate of 2 requests per second.
12 |
13 | ## Solution Structure
14 |
15 | This solution contains 3 projects.
16 |
17 | 1 `ThrottledWebApi`
18 |
19 | - An ASP.NET Core Web API project
20 | - Contains one API endpoint: `/api/values/isPrime?number={number}`
21 | - The API endpoint is enforced with rate limit
22 |
23 | 2 `ThrottledWebApi.IntegrationTests`
24 |
25 | - An integration test project with an in-memory test server
26 | - An HTTP client is used to test against the API endpoint
27 | - The rate limiting effect in different scenarios are tested
28 |
29 | 3 `ThrottledWebApi.ClientDemo`
30 |
31 | - A .NET Core Console app with Dependency Injection, HttpClient
32 | - Throttling concurrent outgoing HTTP requests using a `semaphore`
33 |
34 | ## License
35 |
36 | Feel free to use the code in this repository as it is under MIT license.
37 |
38 |
39 |
--------------------------------------------------------------------------------
/ThrottledWebApi.ClientDemo/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading.Tasks;
4 | using Microsoft.Extensions.DependencyInjection;
5 |
6 | namespace ThrottledWebApi.ClientDemo
7 | {
8 | internal class Program
9 | {
10 | private static async Task Main()
11 | {
12 | var services = new ServiceCollection().AddHttpClient();
13 | services.AddHttpClient();
14 | var serviceProvider = services.BuildServiceProvider();
15 |
16 | var client = serviceProvider.GetService();
17 | var numbers = new List { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0, 11, 13, 17, 19, 23, 29, 31, 41, 43, 1763 };
18 | var results = await client.GetPrimeNumberResults(numbers);
19 | foreach (var result in results)
20 | {
21 | Console.WriteLine($"{result.Number} is a prime number? \t {result.IsPrime}.");
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/ThrottledWebApi.ClientDemo/README.md:
--------------------------------------------------------------------------------
1 | # Throttling Concurrent Outgoing HTTP Requests in .NET Core
2 |
3 | - A .NET Core Console app with Dependency Injection, HttpClient
4 | - Throttling concurrent outgoing HTTP requests using a `semaphore`
5 |
6 | ## [Medium Post](https://medium.com/@changhuixu/throttling-concurrent-outgoing-http-requests-in-net-core-404b5acd987b)
7 |
8 | ## How to run the project
9 |
10 | 1. Go up one directory to `ThrottledWebApi` project. In a terminal, issue command `dotnet run` to start the Web API application.
11 |
12 | 2. In this folder, open a new terminal, run command `dotnet run` to check the Console ouput. ThrottledWebApi.ClientDemo
13 |
--------------------------------------------------------------------------------
/ThrottledWebApi.ClientDemo/ThrottledHttpClient.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Net.Http;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 |
8 | namespace ThrottledWebApi.ClientDemo
9 | {
10 | public interface IThrottledHttpClient
11 | {
12 | ///
13 | ///
14 | ///
15 | /// A list of integers
16 | /// Max number of concurrent requests
17 | /// per second or per n seconds
18 | ///
19 | Task GetPrimeNumberResults(List numbers, int requestLimit = 2, int limitingPeriodInSeconds = 1);
20 | }
21 |
22 | public class ThrottledHttpClient : IThrottledHttpClient
23 | {
24 | private readonly HttpClient _httpClient;
25 | private readonly string _baseUrl = @"http://localhost:5000/api";
26 |
27 | public ThrottledHttpClient(HttpClient httpClient)
28 | {
29 | _httpClient = httpClient;
30 | }
31 |
32 | ///
33 | ///
34 | ///
35 | /// A list of integers
36 | /// Max number of concurrent requests
37 | /// per second or per n seconds
38 | ///
39 | public async Task GetPrimeNumberResults(List numbers, int requestLimit = 2, int limitingPeriodInSeconds = 1)
40 | {
41 | var throttler = new SemaphoreSlim(requestLimit);
42 | var tasks = numbers.Select(async n =>
43 | {
44 | await throttler.WaitAsync();
45 |
46 | var task = _httpClient.GetStringAsync($"{_baseUrl}/values/isPrime?number={n}");
47 | _ = task.ContinueWith(async s =>
48 | {
49 | await Task.Delay(1000 * limitingPeriodInSeconds);
50 | Console.WriteLine($"\t\t {n} waiting");
51 | throttler.Release();
52 | });
53 | try
54 | {
55 | var isPrime = await task;
56 | Console.WriteLine($"{n}");
57 | return new PrimeNumberResult(n, isPrime);
58 | }
59 | catch (HttpRequestException)
60 | {
61 | Console.WriteLine($"\t\t\t {n} error out");
62 | return new PrimeNumberResult(n, "NA");
63 | }
64 | });
65 | return await Task.WhenAll(tasks);
66 | }
67 | }
68 |
69 | public class PrimeNumberResult
70 | {
71 | public long Number { get; set; }
72 | public string IsPrime { get; set; }
73 |
74 | public PrimeNumberResult()
75 | {
76 | }
77 |
78 | public PrimeNumberResult(long n, string isPrime)
79 | {
80 | Number = n;
81 | IsPrime = isPrime;
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/ThrottledWebApi.ClientDemo/ThrottledWebApi.ClientDemo.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net5.0
6 | 7.3
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/ThrottledWebApi.IntegrationTests/IpRateLimitOptionsTests.cs:
--------------------------------------------------------------------------------
1 | using AspNetCoreRateLimit;
2 | using Microsoft.AspNetCore.Mvc.Testing;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Microsoft.Extensions.Options;
5 | using Microsoft.VisualStudio.TestTools.UnitTesting;
6 |
7 | namespace ThrottledWebApi.IntegrationTests
8 | {
9 | [TestClass]
10 | public class IpRateLimitOptionsTests
11 | {
12 | [TestMethod]
13 | public void CheckIpRateLimitOptions()
14 | {
15 | using var factory = new WebApplicationFactory();
16 | var options = factory.Services.GetRequiredService>();
17 | Assert.AreEqual(1, options.Value.GeneralRules.Count);
18 | var generalRule = options.Value.GeneralRules[0];
19 | Assert.AreEqual("*:/api/*", generalRule.Endpoint);
20 | Assert.AreEqual("1s", generalRule.Period);
21 | Assert.AreEqual(2, generalRule.Limit);
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/ThrottledWebApi.IntegrationTests/ThrottledWebApi.IntegrationTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net5.0
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | all
16 | runtime; build; native; contentfiles; analyzers; buildtransitive
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/ThrottledWebApi.IntegrationTests/ValuesControllerTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Net.Http;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using Microsoft.AspNetCore.Mvc.Testing;
8 | using Microsoft.VisualStudio.TestTools.UnitTesting;
9 |
10 | namespace ThrottledWebApi.IntegrationTests
11 | {
12 | [TestClass]
13 | public class ValuesControllerTests
14 | {
15 | [DataTestMethod]
16 | [DataRow(1, "false")]
17 | [DataRow(2, "true")]
18 | [DataRow(11, "true")]
19 | [DataRow(12, "false")]
20 | public async Task TestWithDataSource(int n, string isPrime)
21 | {
22 | using var factory = new WebApplicationFactory();
23 | var httpClient = factory.CreateClient();
24 | var response = await httpClient.GetAsync($"api/values/isPrime?number={n}");
25 | response.EnsureSuccessStatusCode();
26 | var result = await response.Content.ReadAsStringAsync();
27 | Assert.AreEqual(isPrime, result);
28 | }
29 |
30 | [TestMethod]
31 | public async Task ExpectExceptionWhenExceedRateLimit()
32 | {
33 | using var factory = new WebApplicationFactory();
34 | var httpClient = factory.CreateClient();
35 | var numbers = new List { 1, 12, 11 };
36 | var allTasks = numbers.Select(n => Task.Run(async () =>
37 | {
38 | var result = await httpClient.GetStringAsync($"api/values/isPrime?number={n}");
39 | Console.WriteLine($"{n} is a prime number? {result}");
40 | })).ToList();
41 | async Task ConcurrentApiRequests() => await Task.WhenAll(allTasks);
42 | var e = await Assert.ThrowsExceptionAsync(ConcurrentApiRequests);
43 | Assert.AreEqual("Response status code does not indicate success: 429 (Too Many Requests).", e.Message);
44 | }
45 |
46 | [TestMethod]
47 | public async Task ExpectHttpHeadersWhenExceedRateLimit()
48 | {
49 | using var factory = new WebApplicationFactory();
50 | var httpClient = factory.CreateClient();
51 | httpClient.DefaultRequestHeaders.Clear();
52 | var numbers = new List { 1, 12, 11 };
53 | var allTasks = numbers.Select(n => Task.Run(async () =>
54 | {
55 | var response = await httpClient.GetAsync($"api/values/isPrime?number={n}");
56 | return new
57 | {
58 | Number = n,
59 | Headers = response.Headers.ToList()
60 | };
61 | })).ToList();
62 |
63 | var results = await Task.WhenAll(allTasks);
64 |
65 | // assert
66 | var retryHeaders = results.SelectMany(x => x.Headers).Where(x => x.Key == "Retry-After").ToList();
67 | Assert.AreEqual(1, retryHeaders.Count);
68 | Assert.AreEqual("1", string.Join(", ", retryHeaders[0].Value));
69 | var xRateLimitHeaders = results.SelectMany(x => x.Headers).Where(x => x.Key.StartsWith("X-Rate-Limit")).ToList();
70 | Assert.AreEqual(6, xRateLimitHeaders.Count);
71 |
72 | // auxiliary method to print out all headers.
73 | foreach (var result in results)
74 | {
75 | Console.WriteLine($"\r\nHTTP Response Headers for number = {result.Number}:");
76 | foreach (var (key, value) in result.Headers)
77 | {
78 | Console.WriteLine($"\t{key}: {string.Join(", ", value)}");
79 | }
80 | }
81 | }
82 | /* Test Output
83 |
84 | TEST Server Started.
85 |
86 | HTTP Response Headers for number = 1:
87 | Retry-After: 1
88 |
89 | HTTP Response Headers for number = 12:
90 | X-Rate-Limit-Limit: 1s
91 | X-Rate-Limit-Remaining: 1
92 | X-Rate-Limit-Reset: 2019-09-17T18:51:02.4401731Z
93 |
94 | HTTP Response Headers for number = 11:
95 | X-Rate-Limit-Limit: 1s
96 | X-Rate-Limit-Remaining: 0
97 | X-Rate-Limit-Reset: 2019-09-17T18:51:02.4401731Z
98 |
99 | */
100 |
101 | [TestMethod]
102 | public async Task TestWithSemaphoreSlim()
103 | {
104 | using var factory = new WebApplicationFactory();
105 | var httpClient = factory.CreateClient();
106 | var numbers = new List { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 0, 11, 13, 17, 19, 23, 29, 31, 41, 43, 1763, 7400854980481283 };
107 |
108 | var throttler = new SemaphoreSlim(2);
109 | var tasks = numbers.Select(async n =>
110 | {
111 | await throttler.WaitAsync();
112 |
113 | var task = httpClient.GetStringAsync($"api/values/isPrime?number={n}");
114 | _ = task.ContinueWith(async _ =>
115 | {
116 | await Task.Delay(1000);
117 | throttler.Release();
118 | });
119 | try
120 | {
121 | var isPrime = await task;
122 | return new
123 | {
124 | Number = n,
125 | IsPrime = isPrime
126 | };
127 | }
128 | catch (HttpRequestException)
129 | {
130 | return new
131 | {
132 | Number = n,
133 | IsPrime = "NA"
134 | };
135 | }
136 | });
137 | var results = await Task.WhenAll(tasks);
138 |
139 | // assert
140 | var expectedPrimeNumbers = new List { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 41, 43 };
141 | var expectedNonPrimeNumbers = new List { 1, 4, 6, 8, 9, 10, 1763, 7400854980481283 };
142 | CollectionAssert.AreEquivalent(expectedPrimeNumbers, results.Where(x => x.IsPrime == "true").Select(x => x.Number).ToList());
143 | CollectionAssert.AreEquivalent(expectedNonPrimeNumbers, results.Where(x => x.IsPrime == "false").Select(x => x.Number).ToList());
144 | Assert.AreEqual("NA", results.First(x => x.Number == 0).IsPrime);
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/ThrottledWebApi.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.29230.47
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThrottledWebApi", "ThrottledWebApi\ThrottledWebApi.csproj", "{6D5B567A-BED1-4874-B60B-A8F8C4CFE248}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThrottledWebApi.IntegrationTests", "ThrottledWebApi.IntegrationTests\ThrottledWebApi.IntegrationTests.csproj", "{31BA9603-08AF-4724-9E8C-FC6D87E1DACF}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ThrottledWebApi.ClientDemo", "ThrottledWebApi.ClientDemo\ThrottledWebApi.ClientDemo.csproj", "{202E8665-ACD6-497E-88A4-4FB822F8E8AC}"
11 | EndProject
12 | Global
13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
14 | Debug|Any CPU = Debug|Any CPU
15 | Release|Any CPU = Release|Any CPU
16 | EndGlobalSection
17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
18 | {6D5B567A-BED1-4874-B60B-A8F8C4CFE248}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
19 | {6D5B567A-BED1-4874-B60B-A8F8C4CFE248}.Debug|Any CPU.Build.0 = Debug|Any CPU
20 | {6D5B567A-BED1-4874-B60B-A8F8C4CFE248}.Release|Any CPU.ActiveCfg = Release|Any CPU
21 | {6D5B567A-BED1-4874-B60B-A8F8C4CFE248}.Release|Any CPU.Build.0 = Release|Any CPU
22 | {31BA9603-08AF-4724-9E8C-FC6D87E1DACF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {31BA9603-08AF-4724-9E8C-FC6D87E1DACF}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {31BA9603-08AF-4724-9E8C-FC6D87E1DACF}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {31BA9603-08AF-4724-9E8C-FC6D87E1DACF}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {202E8665-ACD6-497E-88A4-4FB822F8E8AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {202E8665-ACD6-497E-88A4-4FB822F8E8AC}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {202E8665-ACD6-497E-88A4-4FB822F8E8AC}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {202E8665-ACD6-497E-88A4-4FB822F8E8AC}.Release|Any CPU.Build.0 = Release|Any CPU
30 | EndGlobalSection
31 | GlobalSection(SolutionProperties) = preSolution
32 | HideSolutionNode = FALSE
33 | EndGlobalSection
34 | GlobalSection(ExtensibilityGlobals) = postSolution
35 | SolutionGuid = {A91053CA-7466-4E02-88A1-5414AED230CD}
36 | EndGlobalSection
37 | EndGlobal
38 |
--------------------------------------------------------------------------------
/ThrottledWebApi/Controllers/ValuesController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.AspNetCore.Mvc;
3 |
4 | namespace ThrottledWebApi.Controllers
5 | {
6 | [Route("api/[controller]")]
7 | [ApiController]
8 | public class ValuesController : ControllerBase
9 | {
10 | [HttpGet("isPrime")]
11 | public ActionResult GetIsPrime(long number)
12 | {
13 | try
14 | {
15 | return Ok(IsPrime(number));
16 | }
17 | catch (ArgumentException e)
18 | {
19 | ModelState.AddModelError(nameof(number), e.Message);
20 | return BadRequest(ModelState);
21 | }
22 | }
23 |
24 | // TEST CASES:
25 | // 1,4,6,8,9,10 --> false;
26 | // 2,3,5,7,11 --> true;
27 | // 11,13,17,19,23,29,31,41,43 -> true;
28 | // 1763=41*43 --> false;
29 | // 7400854980481283=86028221*86028223 -> false;
30 | private static bool IsPrime(long number)
31 | {
32 | if (number <= 0)
33 | {
34 | throw new ArgumentException("Only valid positive numbers are supported.");
35 | }
36 | if (number == 1)
37 | {
38 | return false;
39 | }
40 | if (number == 2 || number == 3 || number == 5)
41 | {
42 | return true;
43 | }
44 | if (number % 2 == 0 || number % 3 == 0 || number % 5 == 0)
45 | {
46 | return false;
47 | }
48 |
49 | var boundary = (long)Math.Floor(Math.Sqrt(number));
50 | var i = 6;
51 | while (i <= boundary)
52 | {
53 | if (number % (i + 1) == 0 || number % (i + 5) == 0) return false;
54 | i += 6;
55 | }
56 | return true;
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/ThrottledWebApi/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore;
2 | using Microsoft.AspNetCore.Hosting;
3 |
4 | namespace ThrottledWebApi
5 | {
6 | public class Program
7 | {
8 | public static void Main(string[] args)
9 | {
10 | CreateWebHostBuilder(args).Build().Run();
11 | }
12 |
13 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
14 | WebHost.CreateDefaultBuilder(args)
15 | .UseStartup();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/ThrottledWebApi/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:5001/",
7 | "sslPort": 44369
8 | }
9 | },
10 | "profiles": {
11 | "IIS Express": {
12 | "commandName": "IISExpress",
13 | "launchBrowser": true,
14 | "launchUrl": "api/values/isPrime?number=123",
15 | "environmentVariables": {
16 | "ASPNETCORE_ENVIRONMENT": "Development"
17 | }
18 | },
19 | "ThrottledWebApi": {
20 | "commandName": "Project",
21 | "launchBrowser": true,
22 | "environmentVariables": {
23 | "ASPNETCORE_ENVIRONMENT": "Development"
24 | },
25 | "applicationUrl": "http://localhost:5000/"
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/ThrottledWebApi/Startup.cs:
--------------------------------------------------------------------------------
1 | using AspNetCoreRateLimit;
2 | using Microsoft.AspNetCore.Builder;
3 | using Microsoft.Extensions.Configuration;
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.Extensions.Hosting;
6 |
7 | namespace ThrottledWebApi
8 | {
9 | public class Startup
10 | {
11 | public Startup(IConfiguration configuration)
12 | {
13 | Configuration = configuration;
14 | }
15 |
16 | public IConfiguration Configuration { get; }
17 |
18 | // This method gets called by the runtime. Use this method to add services to the container.
19 | public void ConfigureServices(IServiceCollection services)
20 | {
21 | services.AddControllers();
22 |
23 | services.AddOptions();
24 | services.AddMemoryCache();
25 | services.Configure(Configuration.GetSection("IpRateLimiting"));
26 | services.AddSingleton();
27 | services.AddSingleton();
28 | services.AddSingleton();
29 | services.AddHttpContextAccessor();
30 | }
31 |
32 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
33 | public void Configure(IApplicationBuilder app, IHostEnvironment env)
34 | {
35 | if (env.IsDevelopment())
36 | {
37 | app.UseDeveloperExceptionPage();
38 | }
39 | app.UseHttpsRedirection();
40 |
41 | app.UseIpRateLimiting();
42 |
43 | app.UseRouting();
44 |
45 | app.UseAuthorization();
46 |
47 | app.UseEndpoints(endpoints =>
48 | {
49 | endpoints.MapControllers();
50 | });
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/ThrottledWebApi/ThrottledWebApi.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net5.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/ThrottledWebApi/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Debug",
5 | "System": "Information",
6 | "Microsoft": "Information"
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/ThrottledWebApi/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Warning"
5 | }
6 | },
7 | "AllowedHosts": "*",
8 | "IpRateLimiting": {
9 | "EnableEndpointRateLimiting": true,
10 | "StackBlockedRequests": false,
11 | "RealIPHeader": "X-Real-IP",
12 | "ClientIdHeader": "X-ClientId",
13 | "HttpStatusCode": 429,
14 | "GeneralRules": [
15 | {
16 | "Endpoint": "*:/api/*",
17 | "Period": "1s",
18 | "Limit": 2
19 | }
20 | ]
21 | }
22 | }
23 |
--------------------------------------------------------------------------------