I'll make no secret of it: I LOVE asynchronous programming in ASP.NET!
I love it so much that I submitted a talk for CodeMash 2019 called "Hold Up, Wait a Minute, Let Me Put Some Async In It," which I actually got to present despite that ridiculous title.
This post is a direct result of that talk, in which I took an existing synchronous ASP.NET web application and slowly refactored it to use asynchronous programming. I figure that more than just CodeMash's attendees might find this useful.
In this post, we will begin the process of refactoring a synchronous ASP.NET web app to an asynchronous one, and show what kinds of problems might arise from doing so.
Let's get started!
The Sample Project
As you might have guessed, this post includes a complete sample project hosted on GitHub. Check it out!
The App and the Object Model
The web app we're going to refactor (the source for which is over on GitHub) is a fairly straightforward one. It makes calls to an external service (in this case, to JSONPlaceholder) to display data returned by said service. The data we are returning is related to one of four objects: Users, Posts, Albums, and Photos.
Here's the hierarchical object model this app is using:
If you read the previous post in this series, you'll know that in order to refactor a synchronous app to asynchronous and not break everything, we generally want to start at the lowest level of the data hierarchy. In our case, that is the Photo object.
So, without further ado, let's refactor Photos!
Refactoring the PhotoRepository
Here's the code for the PhotoRepository
class:
public class PhotoRepository : IPhotoRepository
{
public List<Photo> GetAll()
{
using (WebClient client = new WebClient())
{
var photoJson = client.DownloadString("...");
return JsonConvert.DeserializeObject<List<Photo>>(photoJson);
}
}
public Photo GetByID(int id)
{
using (WebClient client = new WebClient())
{
var photoJson = client.DownloadString("..." + id.ToString());
return JsonConvert.DeserializeObject<Photo>(photoJson);
}
}
}
I want to do as little work as possible, and I also know that there's this thing called a Task which can wrap code and make code blocks "asynchronous" using the Task.Run()
method.
So, I decide to do just that:
public class PhotoRepository : IPhotoRepository
{
public List<Photo> GetAll()
{
using (WebClient client = new WebClient())
{
var photoJson = Task.Run(() => client.DownloadString("..."));
return JsonConvert.DeserializeObject<List<Photo>>(photoJson.Result);
}
}
public Photo GetByID(int id)
{
using (WebClient client = new WebClient())
{
var photoJson =
Task.Run(() => client.DownloadString("..." + id.ToString()));
return JsonConvert.DeserializeObject<Photo>(photoJson.Result);
}
}
}
The great thing about this solution is that I didn't even need to change the IPhotoRepository interface, so there was very little work done. And if we run the code, we see that, amazingly, it actually works!...
...Except, it works very, very poorly.
Problem #1: Async-Over-Sync
What we have done is implemented an anti-pattern known as async-over-sync. A fantastic article called "Should I expose asynchronous wrappers for synchronous methods" has all the details as to why this is a bad idea, but I'm going to break it down. Here's an annotated snapshot of our code:
When executing this method:
- A thread is summoned to execute a call to the
GetAll()
method. - Said thread executes the method until the point at which
Task.Run()
is invoked.Task.Run()
returns immediately, and the thread proceeds to step 3. - At this point, the original thread is blocked, waiting for the
photoTask.Result
to return a value. A entirely different thread is summoned to execute the contents of the call toTask.Run()
.
In short, we're now using two threads for what, in a synchronous application, would only use one.
At small loads, this kind of anti-pattern won't be noticeable, but since asynchronous programming is all about improving throughput, at larger loads this quickly bites us. Our systems can now handle fewer simultaneous requests than a straight-synchronous system could.
So now the question becomes: how do we fix this? In order to answer that, we must solve two problems.
Finishing the PhotoRepository
The first problem is this: the WebClient
class has a new, more suitable successor for the kind of API calls we are making: the HttpClient
class. I, as the project owner, want to switch my entire project from using WebClient
to using HttpClient
. When I try to do this, though, I will immediately run into a problem: HttpClient
does not expose synchronous methods!
The second problem is that, as we saw above, wrapping synchronous code in a Task
is bad idea.
So now I am "forced" to begin refactoring toward asynchronous programming (in the same way I would be "forced" into eating a cookie). However, I'm also a developer in the real world, and I'd rather like to get the most work done with the minimum effort. So here's what I'm going to do: I'm just going to refactor the PhotoRepository
to asynchronous, and not the higher-level PhotoController
.
Here's the refactored code for the PhotoRepository
...
public interface IPhotoRepository
{
Task<Photo> GetByID(int id);
Task<List<Photo>> GetAll();
}
public class PhotoRepository : IPhotoRepository
{
public async Task<List<Photo>> GetAll()
{
using (HttpClient client = new HttpClient())
{
var photoJson = await client.GetStringAsync("...");
return JsonConvert.DeserializeObject<List<Photo>>(photoJson);
}
}
public async Task<Photo> GetByID(int id)
{
using (HttpClient client = new HttpClient())
{
var photoJson = await client.GetStringAsync("..." + id.ToString());
return JsonConvert.DeserializeObject<Photo>(photoJson);
}
}
}
...and here's the code for the PhotoController:
public class PhotoController : Controller
{
private IPhotoRepository _photoRepo;
public PhotoController(IPhotoRepository photoRepo)
{
_photoRepo = photoRepo;
}
[HttpGet]
public ActionResult Index()
{
var allPhotos = _photoRepo.GetAll().Result;
return View(allPhotos);
}
[HttpGet]
public ActionResult GetByID(int id)
{
var photo = _photoRepo.GetByID(id).Result;
return View(photo);
}
}
Notice the use of Task.Result
. What this should mean is that when the controller calls the repository, it will wait for the result of the method to return, then use that result. But does that actually happen?
No. What really happens is far, far worse, and it is not at all obvious.
Problem #2: Deadlock
For reasons that are probably not clear at the moment, the code above will result in deadlock. To find out why, we must first know something about what happens behind the scenes when an asynchronous task is called.
Whenever a asynchronous task is created, if the thread executing that task has to "wait" for something to happen, it can leave the execution context to go do something else. But before it leaves, it creates something called a SynchronizationContext
, which stores the context surrounding the execution of the task and is needed to resume said execution in the future.
NOTE: The above paragraph applies to ASP.NET Framework only, not ASP.NET Core. See Stephen Cleary's blog for why this is.
With the SynchronizationContext
in mind, here's a slide from the presentation this blog post is based upon:
I know this slide is pretty busy and confusing, but hopefully it will make sense in a minute.
Let's break down why this deadlock is happening:
- First, the
Index()
controller action calls theGetAll()
method in thePhotoRepository
. GetAll()
starts an async request forGetStringAsync()
.GetStringAsync()
returns an uncompleted task.- We are now awaiting the result of
GetStringAsync()
. The context is captured at this point and will be used to restore the task at some point in the future. - The controller method is now synchronously blocking on the result from
GetAll()
. This blocks the restoration of the context. - At some point in the future, the call to
GetStringAsync()
finishes. This completes theTask
instance returned byGetStringAsync()
. - The continuation for the task is now ready to run, but must wait for the context to be available in order to do so.
- Because of 5 and 7, the context will never be made available, and so we have DEADLOCK.
So what do we do about this? You may remember from the previous post that async tends to spread like a virus. The solution here is to let it do so, and refactor the controller to use async/await.
Here's the final code for the PhotoController
:
public class PhotoController : Controller
{
private IPhotoRepository _photoRepo;
public PhotoController(IPhotoRepository photoRepo)
{
_photoRepo = photoRepo;
}
[HttpGet]
public async Task<ActionResult> Index()
{
var allPhotos = await _photoRepo.GetAll();
return View(allPhotos);
}
[HttpGet]
public async Task<ActionResult> GetByID(int id)
{
var photo = await _photoRepo.GetByID(id);
return View(photo);
}
}
This is as far as we need to go in the refactoring of the Photo section; ASP.NET automatically includes asynchronous handlers for Controller actions.
All right! We have now fully refactored the Photo section to be completely asynchronous!
Summary and Reader Exercise
We have now seen a practical example of how we might begin to refactor an existing synchronous ASP.NET application into asynchronous programming.
I leave it as an exercise for my readers to try refactoring the Album, Post, and User sections. Feel free to submit any questions that arise from doing that on this post or on the GitHub project, and I'll do my best to answer them.
Happy Coding!