monthFebruary 2020

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 !

Your codebase is like your house.

Today, I was reading my LinkedIn feed, trying to find some inspiration, and I came across a post describing code reviews in a way I never thought before.

The post talked about how you should treat the codebase as if it was your house, which sounds profoundly asinine at first, but… is it really?

I’ve found some good wisdom from this post and I wanted to expand on it. But first, let’s take a step back.

Imagine you’re renting an appartment. The floor and walls are old and door hinges haven’t been fixed in a while but they still can pivot.

You sign the paperwork, get your furniture in, and everyday you will get inside and live in it. As time goes on, you will keep on adding more things in it, sometimes consciously or unconsciously. Those things can be the unread mail piling on, unfinished drinks that were left on your coffee table, the bookshelves slowly filling with dust, the trash bags filling in with smelly shrimp outer shells, the dirty cauldron you used for that delicious chili you’re so proud of, the leftovers of said chili you left in the fridge for way too long…

All those things will inevitably happen as you spend time living in your home every waking moment. And while you could pay for services that will reduce or mitigate the problems (ex. deliveries and cleaning), you still haven’t fixed the core problem : You.

Yes, you. Your habits in spendings, for social and pleasure, your discipline, your anxiety, and maybe even your alcoholism or drug habits. Those are the key points why you’re not taking care of these issues. It’s not the landlord’s fault nor is it your roommate’s, despite feeling the need to blame that landlord for selling you that house or the roommate for not doing anything about it and setting a precedent. Responsibility must be taken by someone, and that someone is gonna be you, because it is your house after all.

You might be thinking “Well, I still am able to live here, and to do my daily routine and make it feel like home” but soon you’ll find you have a sink tap that seldom unlocks from its inner mechanism, the water runs on both the bath and shower resulting in lower pressure from the showerhead, or the wall-hung coat hanger that unbolted itself from the wall due to having too many coats pulling outward, so your only solution is to leave your winter coats on the living room chairs…

Ignoring the problems will probably win your more time for relaxing and living in your home without worries, but those debts will only keep increasing and causing more compromises to be made.

And then you invite people over.

If you don’t keep your home clean when they arrive inside, they’ll see it instantly. And they will understand where your priorities are.

Fortunately, in the context of code reviews, they will be more willing to give feedback, but in both cases, the fact you invite people over to look at your code or spend time at your crib will make you care more about your work.

Essentially, this has been a core issue I felt about every work contracts I’ve had for the past year and a half. All were under monolithic codebases that focused on delivering business value and rarely ever cared about ensuring the next guy who reads the code feels “at home”, because not a lot of people really did code reviews.

Add that I forced myself to work extra hours to fix issues that were ommited by past contributors/tenants and I was on a prescribed 40mg dose of Adderall (a drug that helps with my ADHD but at 26 years old only really makes me grumpy, anxious and force myself to ), to say I felt miserable would be an understatement.

And then they let me go because I had enough and used a loud drill to attempt at fixing those issues, which none of the other tenants were happy about.

Code quality is like keeping your home tidy. It’s important.

By the way, I’m a young developer who’s looking for work in Québec City and in a year around Dallas, TX. Hit me up with offers if you want me in your team !