Don't you hate having a huge amount of if...else
for multiple async operations in a single function? Those are the kind of code smells we're gonna look at here in this article.
Take a look at the example below. It is heavily inspired from real production code in a basic ASP.NET controller method for a form that has an external REST API to fetch dynamic dropdown values.
Task<HttpResponseData<DropdownValue>> obtainCategoriesTask = _restApiClient.obtainDropdownValues("categories");
Task<HttpResponseData<DropdownValue>> obtainTagsTask = _restApiClient.obtainDropdownValues("tags");
Task<HttpResponseData<DropdownValue>> obtainAuthorsTask = _restApiClient.obtainDropdownValues("authors");
var categoriesList = await obtainCategoriesTask;
if (categoriesList.Success)
{
var tagsList = await obtainTagsTask;
if (tagsList.Success)
{
var authorsList = await obtainAuthorsTask;
if (authorsList.Success)
{
// Do stuff with the data
}
else
{
throw new Exception("One of the API calls has resulted in an error.", callGroup.Item1.Exception);
}
}
else
{
throw new Exception("One of the API calls has resulted in an error.", callGroup.Item1.Exception);
}
}
else
{
throw new Exception("One of the API calls has resulted in an error.", callGroup.Item1.Exception);
}
So, what's wrong with this code ?
1. It's not DRY.
DRY means "Don't Repeat Yourself", which is a methodology that encourages processing a structured data pattern (i.e. key/value for a dictionary) instead of typing more logic code (if..else
) when adding more functionalities.
2. It isn't very scalable.
Try adding a new task. You'll have to add another indentation of if...else
inside the if (authorsList.Success)
condition, and you'll also have to add an else
for errors, which most likely won't be as simple as I've shown.
First, make it DRY.
Here's how you can make your code more DRY : You could use a structured data pattern. For this example, we'll use Tuples !
var callGroupsAPI = new List<Tuple<Task<MyHttpResponse<DropdownValue>>, Action<IList<DropdownValue>>>>()
{
Tuple.Create<Task<MyHttpResponse<DropdownValue>>, Action<IList<DropdownValue>>>(
_restApiClient.obtainDropdownValues("categories"),
(result) => {
// Do something with that list.
}
),
Tuple.Create<Task<MyHttpResponse<DropdownValue>>, Action<IList<DropdownValue>>>(
_restApiClient.obtainDropdownValues("tags"),
(result) => {
// Do stuff with list
}
),
Tuple.Create<Task<MyHttpResponse<DropdownValue>>, Action<IList<DropdownValue>>>(
_restApiClient.obtainDropdownValues("authors"),
(result) => {
// Do some other stuff using said list
}
),
};
I've made a list of Tuple
objects where :
-
Item1
is of typeTask<HttpResponseData<DropdownValue>>
- You might be asking what
HttpResponseData
was this whole time. Simply, it's a custom response container. It could be anything else that an asynchronous method from your_restApiClient
wants to return.
- You might be asking what
-
Item2
is of typeAction<IList<DropdownValue>>
- The function we keep there does not return anything yet but expects to receive a response object as an entry parameter. You could, for example, take the list of dropdown values contained in the response and add them in the
ViewBag
to use them in your view.
- The function we keep there does not return anything yet but expects to receive a response object as an entry parameter. You could, for example, take the list of dropdown values contained in the response and add them in the
Then, scale the processing.
Here's the tricky part. We're gonna have to use this data structure in a generic way. This will ensure of its reusability, entensibility and having to write the same instruction multiple times, which helps with readability.
MyHttpResponse<DropdownValue> result;
await Task.WhenAll(callGroupsAPI.Select(x => x.Item1));
foreach (var callGroup in callGroupsAPI)
{
if (callGroup.Item1.IsFaulted || callGroup.Item1.IsCanceled)
{
throw new Exception("One of the API calls has resulted in an error.", callGroup.Item1.Exception);
}
result = callGroup.Item1.Result;
if (result.Succes)
{
callGroup.Item2.Invoke(result);
}
else
{
ViewBag.Error = result.Message;
break;
}
}
Understandably, this is a lot to take in. Let's go step-by-step with this snippet.
1. Declare the result
We declare a result object for convenience. 'Nuff said.
MyHttpResponse<DropdownValue> result;
2. Await all the tasks
Here, we await all the tasks at the same time by doing, for convenience, a quick LINQ query on Item1
of each Tuple
. This is necessary in order to "activate" the asynchronous tasks within each Tuple
.
await Task.WhenAll(callGroupsAPI.Select(x => x.Item1));
3. Go through every callGroup
foreach (var callGroup in callGroupsAPI)
{
Remember that var
is of type Tuple<Task<MyHttpResponse<DropdownValue>>, Action<IList<DropdownValue>>>
, which is quite a mouthful, I admit.
4. Check for issues once it's done.
if (callGroup.Item1.IsFaulted || callGroup.Item1.IsCanceled)
{
throw new Exception("One of the API calls has resulted in an error.", callGroup.Item1.Exception);
}
Since the Task
object contains the state of their respective asynchronous process, we don't lose any of the context from doing the previous await
on every Task
object; We just have to check their status, and if they failed, we throw an exception using the Task
object's internal exception.
By then, it will wait for that first task to be done before it gives us a result, but while this is happening, the other tasks could be still running (or be done by then), which is exactly what we want !
5. Keep the result
Protip: The previous step's throw
gets us out of our for
loop if it ends up true
, so no need to use a else
statement here.
We can just add the result (which we should have by now) to our temporary result
variable, for convenience.
result = callGroup.Item1.Result;
6. Reach for success
This is the cool part : We manually call Invoke()
on the callback contained in Item2
for the current callGroup
, but we pipe in the response directly. Thus, every async call does its own thing it needs to; The instructions were declared in each Tuple's second item!
if (result.Success)
{
callGroup.Item2.Invoke(result);
}
Do note, your mileage will vary on how you'll check for the API's success; MyHttpResponse<T>
I is a custom object I send and deserialize from every REST API calls done in this project, but you could use a System.Web.HttpResponse
just as easily and then deserialize the body. This is out of the scope from this lesson, however.
7. If all else fails...
In case the boolean indicating for success returns false, we indicate that by adding the error message from our result object.
else
{
ViewBag.Error = result.Message;
break;
}
The break
keyword is for quitting the for
loop, so we can show the error in our view without wasting any more time.
Feedback
I'd like to get as much feedback as possible for this tutorial, I'm new to writing here and I'll be honest, English isn't my main language. If there's anything that didn't make sense with the way I phrased things, or anything else, feel free to tell me about it in the comments, I'll adapt for my next articles.
Thanks for reading, and I hope you learned something new !
Top comments (0)