Asynchronous programming has become an increasingly important concept over the years. In this week’s article, we’re going to take a high-level look at what it means; how to write asynchronous code; and how we can unit test it.
The Concept Behind Asynchronous Programming
Let’s imagine a hypothetical scenario. Alice is making a list of things to do while planning her day. She needs to take her car to get serviced; send a parcel at the post office; and write to her friend Bob (as promised) to tell him all about a recent holiday.
She decides to drive to the garage first thing in the morning. When she arrives, Charlie the mechanic tells Alice the service will take around half a day. Alice can either wait in the waiting area, or she can go elsewhere with Charlie offering to give Alice a call when the job is done. With other items left on her to-do list, Alice can choose how to approach the remaining tasks.
The first option is to keep focus on one item at a time. There are plenty of seats and a coffee machine in the waiting area. She can sit and wait until her car is ready. Once it is, she can drive to the post office, and then finally back home where she can write to Bob.
Alternatively, Alice can try to make the most of her time. The service will take a few hours, and there isn’t anything she can do to speed it up. Instead of sitting in the waiting area, she could go to the post office to send her friend Dave a birthday gift. Following that, she could go to a local café and write to Bob while sipping a latte. When Charlie calls, Alice can make her way back to the garage, collect her car, and go home.
In both cases, Alice will get everything on her list done. But the first option is likely to take longer because of the time spent doing nothing while waiting. This concept applies in code too: asynchronous programming can roughly be summarised as doing other things while waiting.
Asynchronous Programming in C#
Asynchronous methods have been available for a long time: .NET Framework 1.1 included various asynchronous operations. However, these required callbacks to use effectively.
A Callback is simply a method to run once an asynchronous operation is complete – much like how Alice would return to the garage once Charlie calls Alice to let her know her car is ready. While not difficult to write, they introduce a level of complexity to the code that synchronous programming isn’t affected by.
A new language feature was introduced in C# 5.0 that alleviated this: instead of requiring callbacks, we can simply await
asynchronous methods.
Reasons to be Asynchronous
Asynchronous programming is particularly helpful when writing CRUD APIs, i.e. those with operations to Create, Read, Update, and Delete data. Much of the work (from the API’s perspective) is either I/O or network bound, rather than being CPU bound.
In other words, performance bottlenecks will most likely be the read/write operations or network lag. Having additional CPU capacity on the API host won’t help to speed things up, as it is reliant on external services to complete the operation. Even if some of these processes only took a second or two, this can effectively be an eternity for a modern CPU running at 2.5GHz: 2,500,000,000 cycles would be wasted for every second that it spends doing nothing while waiting. Much like how Alice could visit the post office and then go on to write to Bob while waiting for her car, an API could spend the waiting time usefully by acting on other incoming requests for example.
Writing an async Method
Writing asynchronous code is simple using the async
/await
model. In the sample code below, we’ve created a service that encapsulates a .NET HttpClient
instance. There’s only one method so far: it wraps a method to perform a GET request.
public interface IHttpService
{
Task<T?> GetData<T>(string url);
}
public class HttpService : IHttpService
{
private readonly HttpClient _httpClient = new();
public async Task<T?> GetData<T>(string url)
{
var response = await _httpClient.GetAsync(url);
var content = await response.Content.ReadFromJsonAsync<T>();
return content;
}
}
There are four important points to note:
The return type is a typed
Task
. For methods that would bevoid
in synchronous code, the return type would beTask
(with no generic type).We use the
await
keyword to await asynchronous operations:_httpClient.GetAsync
andReadFromJsonAsync
in this case.The method signature in the interface does not include the
async
keyword.The method signature in the class includes the
async
keyword.
But other than that, there’s nothing else to it – no callbacks required.
One word of caution: when you write asynchronous code, make the entire callstack for calling your methods asynchronous where possible. Mixing synchronous and asynchronous code can lead to all sorts of headaches. An article introducing asynchronous programming isn’t the place to explore these issues, but please trust me on this one.
Testing async Methods
Now that we have an asynchronous method, let’s write a test for it. The following shows an NUnit/Moq test for our HttpService
.
[Test]
public async Task HttpServiceCanGetData()
{
// Arrange
var service = Mock.Of<IHttpService>(s =>
s.GetData<string>(It.IsAny<string>()) == Task.FromResult<string>("Some data"));
// Act
var response = await service.GetData<string>("url");
// Assert
Assert.That(response, Is.EqualTo("Some data"));
}
The syntax is nearly identical to that for other tests we’ve written in previous parts of this series. Three important things to note:
We wrap the return value with
Task.FromResult
in the LINQ to Mocks setup.The test is marked async and has a return type of
Task
.We
await
the asynchronous method being tested.
If we wanted to use the fluent Moq syntax, we could write the same test with the following code:
[Test]
public async Task HttpServiceCanGetData()
{
// Arrange
var serviceMock = new Mock<IHttpService>();
serviceMock
.Setup(s => s.GetData<string>(It.IsAny<string>()))
.ReturnsAsync((string s) => "Some data");
// Act
var response = await serviceMock.Object.GetData<string>("url");
// Assert
Assert.That(response, Is.EqualTo("Some data"));
}
Summary
Some programming operations wait for responses from external services/databases. Asynchronous code lets a host use its CPU as efficiently as possible. Instead of sitting idle, a thread can be repurposed to do other things during these delays. For example, they might react to new incoming requests to keep an API responsive for other users. When data is available for further processing, the original task can then be resumed.
While this concept has been available since .NET Framework 1.1, the necessary use of callbacks increased code complexity. C# 5.0 introduced the async
/await
syntax, which makes asynchronous code easier to work with.
To write an asynchronous method, you simply need to do three things: mark the method with the async
keyword, change the return type to be a Task
, and use the await
keyword to prefix method calls that need awaiting.
The same applies to tests for asynchronous methods too: you mark the tests as async
, change their return type to Task
, and use await
where necessary. If your tests include mocks, their setups can be adapted by wrapping their return values with Task.FromResult
.
Thanks for reading!
Software development evolves quickly. But we can all help each other learn. By sharing, we all grow together.
I write stories, tips, and insights as an Angular/C# developer.
If you’d like to never miss an issue, you’re invited to subscribe for free for more articles like this, delivered straight to your inbox (link goes to Substack).
Top comments (0)