Reputation: 125
Currently i am working on a .net core web api project on which it is getting data from an external web api. And they have a concurrent rate limiter of 25(25 concurrent api calls are permitted) at their end. the 26th API call will be failed.
So i want to implement a concurrent API rate limiter on my web API project and need to track 26th API call which is failed and need to retry that(it may be get or post call). i have multiple get request as well as post request in my api code
following is my httpservice.cs in my web api
public HttpClient GetHttpClient()
{
HttpClient client = new HttpClient
{
BaseAddress = new Uri(APIServer),
};
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Add("Authorization", ("Bearer " + Access_Token));
return client;
}
private HttpClient Client;
public async Task<Object> Get(string apiEndpoint)
{
Client = GetHttpClient();
HttpResponseMessage httpResponseMessage = await Client.GetAsync(apiEndpoint);
if (httpResponseMessage.IsSuccessStatusCode)
{
Object response = await httpResponseMessage.Content.ReadAsStringAsync();
return response;
}
else if (httpResponseMessage.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
{
//need to track failed calls
return StatusCode(httpResponseMessage.StatusCode.GetHashCode());
}
}
public async Task<Object> Post(string apiEndpoint, Object request)
{
Client = GetHttpClient();
HttpResponseMessage httpResponseMessage = await Client.PostAsJsonAsync(apiEndpoint, request);
if (httpResponseMessage.IsSuccessStatusCode)
{
return await httpResponseMessage.Content.ReadAsAsync<Object>();
}
else if (httpResponseMessage.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
{
//need to track
return StatusCode(httpResponseMessage.StatusCode.GetHashCode());
}
}
how can i limit concurrent api calls in above example
SemaphoreSlim _semaphoregate = new SemaphoreSlim(25);
await _semaphoregate.WaitAsync();
_semaphoregate.Release();
Will this work ?
AspNetCoreRateLimit nuget package is useful here ? will that limit concurrency on above sample?
Please help.
Upvotes: 5
Views: 5897
Reputation: 7338
The simplest solution I'm aware of in limiting the number of concurrent access to a piece of code is using a SemaphoreSlim
object, in order to implement a throttling mechanism.
You can consider the approach showed below, which you should adapt to your current scenario (the following code is simplistic and it is only meant to show you the general idea):
public class Program
{
private static async Task DoSomethingAsync()
{
// this is the code for which you want to limit the concurrent execution
}
// this is meant to guarantee at most 5 concurrent execution of the code in DoSomethingAsync
private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(5);
// here we execute 100 calls to DoSomethingAsync, by ensuring that at most 5 calls are executed concurrently
public static async Task Main(string[] args)
{
var tasks = new List<Task>();
for(int i = 0; i < 100; i++)
{
tasks.Add(ThrottledDoSomethingAsync());
}
await Task.WhenAll(tasks);
}
private static async Task ThrottledDoSomethingAsync()
{
await _semaphore.WaitAsync();
try
{
await DoSomethingAsync();
}
finally
{
_semaphore.Release();
}
}
}
Here you can find the documentation for the SemaphoreSlim
class.
If you want something like a ForEachAsync
method, you can consider reading my own question on the subject.
If you are looking for an elegant solution to use a SemaphoreSlim
as a throttling mechanism for your service, you can consider defining an interface for the service itself and use the decorator pattern. In the decorator you can implement the throttling logic by using the SemaphoreSlim
as showed above, while leaving the service logic simple and untouched in the core implementation of the service. This is not strictly related with your question, it's just a tip to write down the actual implementation for your HTTP service. The core idea of the SemaphoreSlim
used as a throttling mechanism is the one showed in the code above.
The bare minimum to adapt your code is which follows:
public sealed class HttpService
{
// this must be static in order to be shared between different instances
// this code is based on a max of 25 concurrent requests to the API
// both GET and POST requests are taken into account (they are globally capped to a maximum of 25 concurrent requests to the API)
private static readonly SemaphoreSlim _semaphore = new SemaphoreSlim(25);
public HttpClient GetHttpClient()
{
HttpClient client = new HttpClient
{
BaseAddress = new Uri(APIServer),
};
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Add("Authorization", ("Bearer " + Access_Token));
return client;
}
private HttpClient Client;
public async Task<Object> Get(string apiEndpoint)
{
Client = GetHttpClient();
HttpResponseMessage httpResponseMessage = await this.ExecuteGetRequest(apiEndpoint);
if (httpResponseMessage.IsSuccessStatusCode)
{
Object response = await httpResponseMessage.Content.ReadAsStringAsync();
return response;
}
else if (httpResponseMessage.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
{
//need to track failed calls
return StatusCode(httpResponseMessage.StatusCode.GetHashCode());
}
}
private async Task<HttpResponseMessage> ExecuteGetRequest(string url)
{
await _semaphore.WaitAsync();
try
{
return await this.Client.GetAsync(url);
}
finally
{
_semaphore.Release();
}
}
public async Task<Object> Post(string apiEndpoint, Object request)
{
Client = GetHttpClient();
HttpResponseMessage httpResponseMessage = await this.ExecutePostRequest(apiEndpoint, request);
if (httpResponseMessage.IsSuccessStatusCode)
{
return await httpResponseMessage.Content.ReadAsAsync<Object>();
}
else if (httpResponseMessage.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
{
//need to track
return StatusCode(httpResponseMessage.StatusCode.GetHashCode());
}
}
private async Task<HttpResponseMessage> ExecutePostRequest(string url, Object request)
{
await _semaphore.WaitAsync();
try
{
return await this.Client.PostAsJsonAsync(url, request);
}
finally
{
_semaphore.Release();
}
}
}
IMPORTANT NOTE: the code you posted creates a brand new HttpClient
instance each time you need to perform an HTTP request to your API. This is problematic for reasons that go beyond the scope of your question. I strongly suggest you to read this article and this one too.
Upvotes: 3