Asynchronous calls and assignments using Tuples to reduce if...else hell
Tags: csharp, tutorial, codequality
Published: 2020-02-26 15:38:19

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.

```csharp

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** !

```csharp
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.

```csharp
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.

```csharp
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`.

```csharp
await Task.WhenAll(callGroupsAPI.Select(x => x.Item1));
```

### 3. Go through every callGroup

```csharp
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.

```csharp
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.

```csharp
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!

```csharp
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`](https://docs.microsoft.com/en-us/dotnet/api/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.

```csharp
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 !