Wednesday, August 23, 2023

New Video: 'await' Return Types

A new video is available on my YouTube channel: Why do you have to return a Task when you use "await" in a C# method?. The video is a quick walkthrough of code based on an article from earlier this week (link below).

Whenever we "await" something in a C# method, the return value is automatically wrapped in a Task, and the return type for the method must include the Task as well. This leads to some strange looking code: the code in the method returns one thing (such as a Person object), but the return type for the method returns another (a Task of Person). In this video, we will look at some code to try to understand this a bit better.

The code compares using "await" with what happens if we were to use "Task" manually. The comparison helps my brain process the disconnect between return types. Hopefully it will help you as well.


Article:

More videos are on the way.

Happy Coding!

Monday, August 21, 2023

Why Do You Have to Return "Task" Whenever You "await" Something in a Method in C#?

There is something that has always bothered me in C#: Whenever you "await" something in a method, the return value must be wrapped in a Task.

Note: If you prefer video to text, take a look here: YouTube: Why do you have to return a Task when you use await in a C# method?

The Behavior

Let's start with an example:


    public Person GetPerson(int id)
    {
        List<Person> people = GetPeople();
        Person selectedPerson = people.Single(p => p.Id == id);
        return selectedPerson;
    }

    public async Task<Person> GetPersonAsync(int id)
    {
        List<Person> people = await GetPeopleAsync();
        Person selectedPerson = people.Single(p => p.Id == id);
        return selectedPerson;
    }

The first method (GetPerson) does not have any asynchronous code. It calls the "GetPeople" method that returns a List of Person objects. Then it uses a LINQ method (Single) to pick out an individual Person. Lastly it returns that person. As expected, the return type for the "GetPerson" method is "Person".

The second method (GetPersonAsync) does have asynchronous code. It calls the "GetPeopleAsync" method (which returns a Task<List<Person>>) and then awaits the result. This also gives us a List of Person objects. Then it uses the same LINQ method (Single) to get the selected Person. Lastly it returns that person.

But here's where things are a bit odd: even though our return statement ("return selectedPerson") returns a "Person" type, the method itself ("GetPeopleAsync") returns "Task<Person>".

A Leaky Abstraction

I love using "await" in my code. It is so much easier than dealing with the underlying Tasks directly, and it handles the 95% scenario for me (meaning, 95% of the time, it does what I need -- I only need to drop back to using Task directly when I need more flexibility).

I also really like how "await" lets me write asynchronous code very similarly to how I write non-asynchronous code. Exceptions are raised as expected, and I can use standard try/catch/finally blocks. For the most part, I do not have to worry about "Task" at all when using "await".

It is a very good abstraction over Task.

But it does "leak" in this one spot. A leaky abstraction is one where the underlying implementation shows through. And the return type of a method that uses "await" is one spot where the underlying "Task" implementation leaks through.

This isn't necessarily a bad thing. All abstractions leak to a certain extent. But this particular one has bugged me for quite a while. And it can be difficult to grasp for developers who may not have worked with Task directly.

A Better Understanding

Most of the descriptions I've seen have just said "If you use 'await' in your method, the return type is automatically wrapped in a Task." And there's not much more in the way of explanation.

To get a better understanding of why things work this way, let's get rid of the abstraction and look at using "Task" directly for this code, building things up one line at a time.

If you would like more information on how to use Task manually (along with how to use 'await'), you can take a look at the resources available here: I'll Get Back to You: Task, Await, and Asynchronous Methods in C#.

Step 1: The Method Signature

Let's start with the signature that we would like to have:


    public Person GetPersonAsyncWithTask(int id)
    {

    }

The idea is that I can pass the "id" of a person to this method and get a Person instance back. So this would be my ideal signature.

Step 2: Call the Async Method

Next, we'll call the "GetPersonAsync" method, but instead of awaiting it, we'll get the actual Task back. When I don't know the specific type that comes back from a method, I'll use "var result" to capture the value and then let Visual Studio help me. Here's that code:


    public Person GetPersonAsyncWithTask(int id)
    {
        var result = GetPeopleAsync();
    }

If we hover the cursor over "var", Visual Studio tells us the type we can expect:



This tells us that "result" is "Task<TResult>" and that "TResult is List<Person>". This means that "GetPeopleAsync" returns "Task<List<Person>>". So let's update our code to be more explict:


    public Person GetPersonAsyncWithTask(int id)
    {
        Task<List<Person>> peopleTask = GetPeopleAsync();
    }

Now we have our explicit type, along with a better name for the result: peopleTask.

Step 3: Add a Continuation

The next step is to add a continuation to the Task. By adding a continuation, we are telling our code that after the Task is complete, we would like to "continue" by doing something else.

This is done with the "ContinueWith" method on the task:


    public Person GetPersonAsyncWithTask(int id)
    {
        Task<List<Person>> peopleTask = GetPeopleAsync();
        peopleTask.ContinueWith(task =>
        {

        });        
    }

The "ContinueWith" method takes a delegate (commonly inlined using a lambda expression). In this case, the delegate takes the "peopleTask" as a parameter (which we'll call "task" in the continuation).

For more information on delegates and lambda expressions, you can take a look at the resources available here: Get Func-y: Understanding Delegates in C#.

Step 4: Fill in the Continuation Code

The next step is to fill in the body of our continuation. This is basically "the rest of the method" that we had in our non-asynchronous version:


    public Person GetPersonAsyncWithTask(int id)
    {
        Task<List<Person>> peopleTask = GetPeopleAsync();
        peopleTask.ContinueWith(task =>
        {
            List<Person> people = task.Result;
            Person selectedPerson = people.Single(p => p.Id == id);
            return selectedPerson;
        });        
    }

The first line in the continuation takes the "Result" property of the task (which is the List<Person>) and assigns it to a variable with a friendlier name: "people". Then we use the "Single" method like we did above to get an individual record from the list. Then we return that selected "Person" object.

But Now We Have a Problem

But now we have a problem: we want to return the "selectedPerson" from the "GetPersonAsyncWithTask" method, but it is being returned inside the continuation of the Task instead.

How do we get this out?

It turns out that "ContinueWith" returns a value. Let's use the same technique we used above to figure out what that is.

Step 5: Getting a Return from ContinueWith


    public Person GetPersonAsyncWithTask(int id)
    {
        Task<List<Person>> peopleTask = GetPeopleAsync();
        var result = peopleTask.ContinueWith(task =>
        {
            List<Person> people = task.Result;
            Person selectedPerson = people.Single(p => p.Id == id);
            return selectedPerson;
        });        
    }

Here we have added "var result = " in front of of the call to "peopleTask.ContinueWith". Then if we hover the mouse over "var", we see that this is "Task<TResult>" and "TResult is Person". So this tells us that "ContinueWith" returns a "Task<Person>".

So let's be more explicit with our variable:


    public Person GetPersonAsyncWithTask(int id)
    {
        Task<List<Person>> peopleTask = GetPeopleAsync();
        Task<Person> result = peopleTask.ContinueWith(task =>
        {
            List<Person> people = task.Result;
            Person selectedPerson = people.Single(p => p.Id == id);
            return selectedPerson;
        });        
    }

Now our "result" variable is specifically typed as "Task<Person>".

Step 6: Return the Result

The last step is to return the result:


    public Person GetPersonAsyncWithTask(int id)
    {
        Task<List<Person>> peopleTask = GetPeopleAsync();
        Task<Person> result = peopleTask.ContinueWith(task =>
        {
            List<Person> people = task.Result;
            Person selectedPerson = people.Single(p => p.Id == id);
            return selectedPerson;
        });
        return result;
    }

But we can't stop here. Our return types do not match. The method (GetPersonAsyncWithTask) returns a "Person", and the actual type we return is "Task<Person>".

So we need to update our method signature:


    public Task<Person> GetPersonAsyncWithTask(int id)
    {
        Task<List<Person>> peopleTask = GetPeopleAsync();
        Task<Person> result = peopleTask.ContinueWith(task =>
        {
            List<Person> people = task.Result;
            Person selectedPerson = people.Single(p => p.Id == id);
            return selectedPerson;
        });
        return result;
    }

Now the method returns a "Task<Person>". And this is what we want. If someone awaits what comes back from this method, they will get a "Person" object back. In addition, someone could take this task and set up their own continuation. This is just the nature of Task and how we set up asynchronous code using Task.

Back to the Comparison

So let's go back to our method comparison. But instead of comparing a method with "await" to a non-asynchronous method, let's compare a method with "await" to a method that handles the asynchronous Task manually.


    public async Task<Person> GetPersonAsync(int id)
    {
        List<Person> people = await GetPeopleAsync();
        Person selectedPerson = people.Single(p => p.Id == id);
        return selectedPerson;
    }

    public Task<Person> GetPersonAsyncWithTask(int id)
    {
        Task<List<Person>> peopleTask = GetPeopleAsync();
        Task<Person> result = peopleTask.ContinueWith(task =>
        {
            List<Person> people = task.Result;
            Person selectedPerson = people.Single(p => p.Id == id);
            return selectedPerson;
        });
        return result;
    }

Here's how we can think of this code now (and the compiler magic that happens behind it): anything after the "await" is automatically put into a continuation. What the compiler does is much more complicated, but this is how we can think of it from the programmer's perspective.

If we think about it this way, it's easier to see why the method returns "Task<Person>" instead of just "Person".

"await" is Pretty Magical

When we use "await" in our code, magical things happen behind the scenes. (And I'm saying that as a good thing.) The compiler gets to figure out all of the details of waiting for a Task to complete and what to do next. When I have this type of scenario (the 95% scenario), then it is pretty amazing. It saves me a lot of hassle and let's me work with code that looks more "normal".

The abstraction that "await" gives us is not perfect, though. It does "leak" a little bit. Whenever we "await" something in a method, the return value gets automatically wrapped in a Task. This means that we do need to change the return type of the method to Task. But it is a small price to pay for the ease of using "await".

Wrap Up

I've always had a bit of trouble with having to return a Task from any method that "awaits" something. I mean, I knew that I had to do it, but there was always a bit of cognitive dissonance with saying "the code in this method returns 'Person', but the method itself returns 'Task<Person>'."

By working through this manually, my brain is a little more comfortable. I have a better understanding of what is happening behind the "await", and that if we were to do everything manually, we would end up returning a Task. So it makes sense that after we add the magic of "await", the method will still need to return a Task.

I hope that this has helped you understand things a bit as well. If you have any questions or other ways of looking at this, feel free to leave a comment.

Happy Coding!

Friday, August 18, 2023

New Video: Nullable Reference Types and Null Operators in C#

A new video is available on my YouTube channel: Nullable Reference Types and Null Operators in C#. The video is a quick walkthrough of code based on a series of articles I did last year (links below).

Nullable Reference Types and Null Operators in C#

New projects in C# have nullable reference types enabled by default. This helps make the intent of our code more clear, and we can catch potential null references before they happen. But things can get confusing, particularly when migrating existing projects. In this video, we will look at the safeguards that nullability provides as well as the problems we still need to watch out for ourselves. In addition, we will learn about the various null operators in C# (including null conditional, null coalescing, and null forgiving operators). These can make our code more expressive and safe.

The code shows an existing application that can get a null reference exception at runtime. Then we enable nullable reference types to see what that feature does and does not do for us. Then we walk through the potential null warnings and use the various null operators to take care of them.

All in under 13 minutes!


The video is based on a series of articles. If you prefer text (or just want more detail), you can take a look at these:

More videos are on the way.

Happy Coding!