Asynchronous calls and assignments using Tuples to reduce if…else hell

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 type Task<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.
  • Item2 is of type Action<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.

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 !

1 Comment

  1. Way cool! Some very valid points! I appreciate you writing this post and the rest of the site is very good. Austine Gottfried Skipper

Comments are closed.