Search
Close this search box.

Task and Async: Magically Wrapping Returned Objects into Tasks

References

Like many developers working with Microsoft’s MVC platform, we have frequently leveraged the trio of C# language features, Task, await and async, to help improve performance of long running, I/O blocking processes in our Controller actions and Repositories.

When working with many different languages, platforms and third party tools in a consulting context, it sometimes takes a simple problem to challenge our deeper understanding of how certain API’s, libraries and language features, really work.

Multi-threaded, parallel programming remains a very a deep and challenging area of knowledge, despite the significant convenience features Microsoft has provided with the TPL (Task), await and async language features in C#. The following problem caused us to take a deeper dive into how each of these three features work together and the discovery that the Task class is essentially the same thing as a JavaScript Promise (or Futures in Microsoft-speak), a concept we are very familiar with from working with AngularJS and other front end frameworks. This post describes our discoveries around the Task, async and await language features while attempting to resolve this simple problem.

The Problem

We needed to update an existing WebAPI Controller Action to return a BadRequest based on a condition in the posted Model.

public Task<IHttpActionResult> Post(ClaimsEditViewModel model)
{
     // Example of the code we needed to add
     if(model.DateReceived < DateTime.Now)
     {
         return BadRequest(); // PROBLEM: Compiler didn't like this (red squiggles in Visual Studio)
     }

     // SaveAsync is awaitable, returns Task<HttpActionResult>
     return SaveAsync(model, "Index", "Claims", "Claim successfully saved."); 
}

Seems like a simple task. However, the project would not build; the compiler complained about returning BadRequest (System.Web.Http.Results.ErrorMessageResult) in this method. Seems obvious at first, because the method signature requires returning a Task, not an ErrorMessageResult.

However, in the same WebApi controller, there was another Action method returning a BadRequest just fine without any complaint from the compiler:

//fictional method for this example
public async Task<IHttpActionResult> Post2(ClaimsEditViewModel model, int someValue)
{
     //pseudo code
    if (someValue < 0) {
       return BadRequest();
    }

    // SaveAsync is awaitable, returns Task<HttpActionResult>
     return await SaveAsync(model, "Index", "Claims", "Claim successfully saved.");
}

This compiled fine. Huh?

Discoveries

This gave us pause. Why could we not return a BadRequest in the first method, but return one in the second method? At first blush, both methods appeared asynchronous – both method signatures return a Task<IHttpActionResult>, and SaveAsync is obviously asynchronous, right? And BadRequest can be cast to IHttpActionResult, so what gives?

Well, a few key discoveries/refreshers:

  • C# Tasks are Futures (what?) which are Promises (ah, ok!).
  • Using Async with Task<T> enables some magic, where objects of type T are automatically wrapped in new Task objects for us. We do not have to manually wrap our objects of Type T into new Task objects before returning them.
  • This automatic wrapping happens regardless of whether or not the method is awaitable (asynchronous) or synchronous.
  • Both methods are not asynchronous. Post1 is synchronous, Post2, asynchronous. Post 1 is synchronous because returning an awaitable Task<T> like SaveAsync without the await keyword in a method returning a Task<T> without the async keyword will execute synchronously. We briefly erroneously assumed returning Task<T> meant asynchronous.
  • If the original action method could be improved: if it did not need to be asynchronous, returning a Task in a synchronous method is required and may cause a performance hit.

What follows is a paraphrased excerpt from an internal Slack thread where we walked through a deductive process to make the discovery that Task with Async automagically wraps returned objects in Tasks.

Breaking it Down – Returning BadRequest

Let’s start by validating our understanding that BadRequest can cast to IHttpActionResultin a simplified Action method with a return type of just IHttpActionResult:

public IHttpActionResult Post(ClaimsEditViewModel model)
{
   return BadRequest();
}

This builds and compiles, so we have validated that BadRequest can be cast to the method’s return type, IHttpActionResult because of it’s parentage.

But can we return a BadRequest when the method’s return type is Task<IHttpActionResult>?

public Task<IHttpActionResult> Post(ClaimsEditViewModel model)
{
     return BadRequest(); // red squiggle here: BadRequest is not castable to Task<IHttpActionResult>
}

Another fairly obvious sanity check. This won’t work because the method now expects the BadRequest wrapped in a Task <IHttpActionResult>.

To make the above work, we need to do manually wrap the BadRequest in a Task. Let’s look at doing this using the original method to which we want to add a conditional test and return a BadRequest.

public Task<IHttpActionResult> Post(ClaimsEditViewModel model)
{
 // Example of the code we needed to add
     if(model.DateReceived < DateTime.Now)
     {
          // manually wrap the BadRequest in a new Task
          return new Task<IHttpActionResult>(() => { return BadRequest(); });
     }

     // SaveAsync is awaitable, returns Task<HttpActionResult>
     return SaveAsync(model, "Index", "Claims", "Claim successfully saved."); 
}

This compiles because now every IHttpActionResult is returned, wrapped in a Task.

But wait, why do we have to wrap the BadRequest in a new Task in Post1, when we don’t have to in Post2?

public async Task<IHttpActionResult> Post2(ClaimsEditViewModel model, int someValue)
{

    if (someValue < 0) {

       // Why don't we have to wrap this in a new Task?  Why does this work?
       return BadRequest();

    }

    // SaveAsync is awaitable, returns Task<HttpActionResult>
     return await SaveAsync(model, "Index", "Claims", "Claim successfully saved.");
}

Obviously, BadRequest was being wrapped automagically into a Task<IHttpActionResult>. But why? What it the fact the action method is asynchronous? The fact the await keyword is used together with the async keyword?

It turns out that awaiting execution from methods invoked within the action method really has nothing to do with automagically wrapping returned objects into Tasks. It’s solely the combination of the async keyword with returned Task<T> that enables this syntactical sugar.

Here, we combine async with Task<T> to automatically wrap returned objects into Tasks without any asynchronous execution. This compiles and runs.

public async Task<IHttpActionResult> PostSync(ClaimsEditViewModel model, int someValue)
{
            if (someValue > 0)
            {
                return BadRequest(); 
            }

            return Save(model, "Index", "Claims");
}

With the above, Visual Studio will generate a green squiggle under PostSync and inform us that the action will execute synchronously. So it will execute synchronously despite the use of the async keyword and returning a Task. It will wrap the returned BadRequest into a Task.

So Task<T> with the async keyword can be used to automagically wrap return objects of type T to Task<T>.

But what is the use of returning a Task for a synchronous method? As far as we are aware, none. In fact we suspect it might cause a performance hit (please let us know if we’re wrong).

Wrapping It Up: The Final Method with Task, Await and Async

Our original challenge was was to add a conditional check to an existing MVC Controller Action and return a BadRequest if it was met. Because the original method invoked the awaitable, asynchronous SaveAsync method, we chose to update the method to be fully asynchronous, and take advantage of the automagic wrapping of BadRequest into a Task.

public async Task<IHttpActionResult> Post(ClaimsEditViewModel model)
{
     if (model.DateReceived < DateTime.Now)
     {
        return BadRequest(); 
     }
     return await SaveAsync(model, "Index", "Claims", "Claim successfully saved.");
}
Share This:

Stay up to date with the latest customer data news, expert guidance, and resources.

More Resources

Your Unified Data, Analytics & AI Partner

Experience the Skypoint AI platform tailored for healthcare, financial services, and the public sector. Securely harness AI with generative AI Copilots and AI Agents to enhance analytics, accurate question answering, automate tasks, and to 10X productivity and efficiency in one compound AI system.