How to use C# async/await for API calls & to stop blocking

Published: Monday 3 June 2024

Using the async and await keywords in C# helps prevents blocking of the main thread for intensive operations.

When we use a synchronous method, we add the type that we are expecting the method to return:

public Product GetProduct(int id)
{
    return new Product
    {
        Id = 1,
        Title = "Television",
        Description = "55 inch TV",
        Price = 699
    };
}

If we wish to make this method asynchronous, we have to mark the method as async and return a Task type. 

public async Task<Product> GetProduct(int id)
{
    return new Product
    {
        Id = 1,
        Title = "Television",
        Description = "55 inch TV",
        Price = 699
    };
}

It's only worth marking a method asynchronous when we have make an asynchronous call within it.

Before we look at doing that, let's us ask ourselves the difference between synchronous and asynchronous.

The differences between synchronous and asynchronous

Let's just say that you are in a shop and there is one person assisting to the customers.

You join the queue and you are the next person to be served.

A queue where there are two awaiting people

A queue where there are two awaiting people

This is fine as you are next in the queue and you shouldn't have to wait too long.

But what if three more customers join the queue?

Five customers have now joined the queue

Five customers have now joined the queue

The customer who has joined the back of the queue is going to have to wait until the other four customers in front of them have been served before it's their turn.

This will lead them to waiting along time in the queue and undoubtedly a lot of frustation!

This is an example of a synchronous operation as each customer has to wait for the customers ahead of them to be served by the same assistant before they reach the front of the queue.

But there is an alternative.

Asynchronous

In this same situation, just say two more assistants come out and help serve the customers.

Three queues are now set up for the customers

Three queues are now set up for the customers

All of a sudden, there are now three queues which are much shorter.

A customer can be served by anyone and still get the same result.

So does that mean I should always use asynchronous?

No it doesn't. Just say we had three assistances working, but only one person in the queue.

One person in a queue when there are three assistances

One person in a queue when there are three assistances

The other two assistants aren't doing anything meaning they are complete waste of resource.

When to use asynchronous

In our example, the assistant represents the thread, or main thread. Take this simple method:

public int Add(int a, int b)
{
    return a + b;
}

It would be reasonable to expect that adding two numbers together would take your application no time at all to calculate.

Therefore a simple operation could easily be handled by the main thread and would remain synchronous.

Asynchronous tasks are best for tasks that can take up a lot of resource. Such examples include:

  • Making an API call
  • Making a database call
  • Uploading a file

One common factor with these tasks is that they have to make a call outside of the application. Therefore, network connection and traffic congestion can be reasons why we make these calls async as we don't know for sure when we are going to be returned with a response.

The problem with blocking threads

The problem with blocking threads is almost certainly going to be an issue in production when there are too many requests made at the same time.

If a thread is blocked, it can't do anything until it's unblocked.

The alternative is to await an asynchronous command, meaning that the thread is able to do other operations whilst a response is awaited.

The web is asynchronous by design

Just say a request to an API takes on average 500ms to process and you are in 50th position in the queue. You'd have to wait 24.5 seconds before your request is processed if it was synchronous.

Fortunately, the web is asynchronous by design. When you load up a web page, you don't usually wait for the other 12 people in front of you to load up the website before it's your turn?

As well as that, when you load up the web page, the extra resources like images, CSS and JavaScript files seem to be loaded at the same time as these are handled by different threads.

With ASP.NET Core MVC and Web API, each request is handled asynchronously meaning that even an application with heavy traffic can still return responses in a fraction of a second.

But there are still operations within these requests that are resource intensive and can slow traffic down.

How to use asynchronous on an API call

To make a HTTP call in ASP.NET Core, we create a HTTP client from the IHttpClientFactory instance.

In the HttpClient class, there is a GetAsync method that makes a HTTP GET request to the inputted endpoint and returns a response. This is an asynchronous method as it returns a type of Task.

In-order to await this method, we use the await keyword before calling the method. This ensures that the compiler won't go onto the next line until we get a result.

public async Task<Product?> GetProduct(int id)
{
    using var httpClient = _httpClientFactory.CreateClient("DummyJson");

    var httpResponse = await httpClient.GetAsync($"/products/{id}");        

    if (httpResponse == null)
    {
        return null;
    }

    httpResponse.EnsureSuccessStatusCode();

    return await httpResponse.Content.ReadFromJsonAsync<Product>();
}

If we didn't use the await keyword, the application will continue executing the method whilst the HTTP GET request is running in the background. There is a good chance that the method would be returned before the HTTP GET request has finished. Therefore, we couldn't return the data that the method is set out to do.

Other Task operations

One of the good things about using asynchronous programming is that we can set off tasks and await for some, or all of them to complete meaning they can all run in the background.

In this example, we are making a call to the GetProduct method in an API controller. However, we want to return three products, so we can start to run each of the three requests.

To ensure we get results for all three of the requests, we await the Task.WhenAll method passing in each of the three tasks as parameters. The application will only return the result when we have a response for all three methods.

[ApiController]
[Route("api/http")]
public class HttpController : Controller
{
    private readonly IHttpService _httpService;

    public HttpController(IHttpService httpService)
    {
        _httpService = httpService;
    }

    [HttpGet("products")]
    public async Task<IActionResult> GetProducts()
    {
        var firstProduct = _httpService.GetProduct(1);
        var secondProduct = _httpService.GetProduct(2);
        var thirdProduct = _httpService.GetProduct(3);

        await Task.WhenAll(firstProduct, secondProduct, thirdProduct);

        return Ok(new
        {
            FirstProduct = firstProduct.Result,
            SecondProduct = secondProduct.Result,
            ThirdProduct = thirdProduct.Result
        });
    }
}

Other Task operations include Task.WhenAny which is similar to Task.WhenAll, but the application will continue when any of the tasks are turned.

Task.Delay allows you to add a delay to the application without blocking a thread which is a much better alternative to using Thread.Sleep.

[HttpGet("products")]
public async Task<IActionResult> GetProducts()
{
    var firstProduct = _httpService.GetProduct(1);
    var secondProduct = _httpService.GetProduct(2);
    var thirdProduct = _httpService.GetProduct(3);

    await Task.Delay(TimeSpan.FromMilliseconds(300)); // Adds a 300ms delay

    await Task.WhenAll(firstProduct, secondProduct, thirdProduct);        

    return Ok(new
    {
        FirstProduct = firstProduct.Result,
        SecondProduct = secondProduct.Result,
        ThirdProduct = thirdProduct.Result
    });
}

There is also Task.Run. This is similar to writing an asynchronous method and returning it as a Task, but it can be written inline.

[ApiController]
[Route("api/http")]
public class HttpController : Controller
{
    private readonly IHttpService _httpService;

    public HttpController(IHttpService httpService)
    {
        _httpService = httpService;
    }

    [HttpGet("products")]
    public async Task<IActionResult> GetProducts()
    {
        var firstProduct = _httpService.GetProduct(1);
        var secondProduct = _httpService.GetProduct(2);
        var thirdProduct = _httpService.GetProduct(3);

        var abc = Task.Run(() =>
        {
            // My TASK HERE
        });

        await Task.WhenAll(firstProduct, secondProduct, thirdProduct, abc);        

        return Ok(new
        {
            FirstProduct = firstProduct.Result,
            SecondProduct = secondProduct.Result,
            ThirdProduct = thirdProduct.Result
        });
    }
}

Watch the video

Watch the video where we show you the difference between synchronous and asynchronous calls and how to use them in an ASP.NET Core Web API.