Concurrent HTTP Call Using SemaphoreSlim in .NET
Tags: webdev, csharp, programming, dotnet
Published: 2022-12-06 17:35:52

## Introduction

I'm exploring concurrent HTTP calls and learning how to handle many tasks in .NET. I found some references about `SemaphoreSlim` to handle concurrent tasks. If you want to know more about `SemaphoreSlim`, please see [this reference](https://learn.microsoft.com/en-us/dotnet/api/system.threading.semaphoreslim?view=net-7.0).

## Preparing the Web API

I just use the Web API from the template. You can use this command to generate the project.

```bash
dotnet new webapi -o WebServer
```

Don't forget to run the Web API using this command: `dotnet run --project WebServer`.

## Generating the Console App to Call the Web API

You can use this command to generate the console app.

```bash
dotnet new console -o WebServer.Client
```

## My Learning Process

I found many patterns and suggestions to use this code.

```csharp
// just for generate 10,000 data
var result = Enumerable.Range(0, 10_000);
// generate tasks
var tasks = result.Select(async x => {
   // API Call
  await CallServer();
});
await Task.WhenAll(tasks);
```

But the code introduces a big problem. Some tasks will reach the request timeout because they exceed the concurrent limit to create connections and can't do API calls. The queue happens in HttpClient.

Thank you, Josef Ottosson. [This post](https://josef.codes/c-sharp-throttle-http-requests-concurrent/) inspires me so much. I've tried to use SemaphoreSlim, and that is my expectation to do concurrent calls.

Here are my final codes.

```csharp
using System.Collections.Concurrent;
using System.Diagnostics;

var stopwatch = new Stopwatch();
using HttpClient client = new();

var semaphoreSlim = new SemaphoreSlim(initialCount: 10,
          maxCount: 10);
Console.WriteLine("{0} tasks can enter the semaphore.",
                          semaphoreSlim.CurrentCount);
var result = Enumerable.Range(0, 20_000);
var dictionaryResult = new ConcurrentBag<string>();
stopwatch.Start();
var tasks = result.Select(async x =>
{
    Console.WriteLine("Task {0} begins and waits for the semaphore.",
                                  x);
    int semaphoreCount;
    await semaphoreSlim.WaitAsync();
    try
    {
        Console.WriteLine("Task {0} enters the semaphore.", x);
        var response = await GetForecastAsync(x, client);
        dictionaryResult.Add(response);
    }
    finally
    {
        semaphoreCount = semaphoreSlim.Release();
    }
    Console.WriteLine("Task {0} releases the semaphore; previous count: {1}.",
                                  x, semaphoreCount);
});
Console.WriteLine("Waiting task");
await Task.WhenAll(tasks);
stopwatch.Stop();
Console.WriteLine(dictionaryResult.Count);
// Get the elapsed time as a TimeSpan value.
TimeSpan ts = stopwatch.Elapsed;

// Format and display the TimeSpan value.
string elapsedTime = String.Format("{0:00}:{1:00}:{2:00}.{3:00}",
    ts.Hours, ts.Minutes, ts.Seconds,
    ts.Milliseconds / 10);
Console.WriteLine("RunTime " + elapsedTime);

static async Task<string> GetForecastAsync(int i, HttpClient client)
{
    Console.WriteLine($"Request from: {i}");
    var response = await client.GetAsync("http://localhost:5153/weatherforecast");
    return await response.Content.ReadAsStringAsync();
}
```

We move the "queue" process to SemaphoreSlim. I think this will be the best approach if you don't need to have guaranteed order. How about having guaranteed order results?

I have a "hacky" way, but this is not the best approach. You can use the `ConcurrentDictionary`, but you will need to sort the `ConcurrentDictionary` using the index. Please see these code changes.

```csharp
// set to ConcurrentDictionary<int, string>
var dictionaryResult = new ConcurrentDictionary<int, string>();

// ...

// store response, x is the index of the task lists
dictionaryResult.TryAdd(x, response);

// sort by the key and just take the response
var responses = dictionaryResult.OrderBy(x => x.Key).Select(x => x.Value).ToList();
```

This approach might become slow because it's run sequentially. Feel free to give feedback on this post if you have another suggestion that runs concurrently or parallels.

You can access the code [here](https://github.com/berviantoleo/HttpClientConcurrent).

{% github berviantoleo/HttpClientConcurrent %}

## Thank you

Thank you for reading this post.

![GIF Thank You](https://media.giphy.com/media/3otPoOxyDTXjzpMbIY/giphy.gif)

## Resources/References

* [C# - Throttle outgoing http requests - Concurrent by Josef Ottosson](https://josef.codes/c-sharp-throttle-http-requests-concurrent/)
* [C# – How to make concurrent requests with HttpClient by Mak](https://makolyte.com/csharp-how-to-make-concurrent-requests-with-httpclient/)
* [SemaphoreSlim](https://learn.microsoft.com/en-us/dotnet/api/system.threading.semaphoreslim?view=net-7.0)
* [ConcurrentDictionary](https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent.concurrentdictionary-2?view=net-7.0)