Representing an external operation with TaskCompletionSource

Using TaskCompletionSource to cancel an external asynchronous operation

Home DailyDrop

Daily Knowledge Drop

A TaskCompletionSource instance can be used to represent an external asynchronous operation, allowing the external operation to be awaited on.

This is especially useful when required to wait on a callback method to be invoked before proceeding with the execution of code.


Requirement

Consider the use case where an application can request that a file (containing website information in this example) be created asynchronous. Once the file is created, a callback handler method will be invoked to notify the caller that the file has been created.

Consider the below async method to request the file. The calling application has no control over this method - it just calls the RequestFileCreation method, specifying the folder and the callback event:

Task RequestFileCreation(string folder, Func<string, Task> callback)
{
    Task.Run(async () =>
    {
        // simulate the time it takes to create the file!
        await Task.Delay(1000);

        // invoke the callback method, notifying the called
        // the file has been created
        // filename is hardcoded in the example
        await callback.Invoke("alwaysdeveloping.txt");
    });

    return Task.CompletedTask;
}

When the method is invoked, a separate task is created (and not awaited) to run a process to have the file created asynchronously. Once the file is created, the specified callback back is invoked.

The issue with this approach is that when invoking the RequestFileCreation method there is no reliable way to wait for the file to be created if we need to perform some additional processing on the file after it has been created:

async Task GetWebsiteInformation()
{
    Console.WriteLine($"Start of '{nameof(GetWebsiteInformation)}' method");

    // call the method to request the file be created
    // the await is NOT awaiting the creation of the file, but is 
    // awaiting the request to create the file
    await RequestFileCreation("C:\\inputfiles", (file) =>
    {
        Console.WriteLine($"File '{file}' processed successfully");

        return Task.CompletedTask;
    });

    // Need to do some additional processing based on the
    // contents of the file here

    Console.WriteLine($"End of '{nameof(GetWebsiteInformation)}' method");
}

When executed, the result is the following:

Start of 'GetWebsiteInformation' method
End of 'GetWebsiteInformation' method

The method is executed and exits without waiting for the file to be created and subsequent processing to be done. We need a way to wait for the file to be created, before performing additional processing and exiting the method.

This is exactly the situation TaskCompletionSource is able to solve!


TaskCompletionSource

Incorporating the TaskCompletionSource into the process is very straight forward.

The RequestFileCreation method remains exactly as it was above, but the GetWebsiteInformation is updated to be as follows:

async Task GetWebsiteInformation()
{
    Console.WriteLine($"Start of '{nameof(GetWebsiteInformation)}' method");

    // create a TaskCompletionSource<T> instance where T
    // is the return type 
    TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();

    // in the callback handler method, call the SetResult
    // on TaskCompletionSource instance.
    await RequestFileCreation("C:\\inputfiles", (file) =>
    {
        Console.WriteLine($"File '{file}' processed successfully");

        tcs.SetResult(file);

        return Task.CompletedTask;
    });

    Console.WriteLine($"Waiting for a file to be created");
    // await the TaskCompletionSource instance task
    // Once the SetResult method has been set, the task will be 
    // considered completed
    var fileName = await tcs.Task;

    Console.WriteLine($"File created! Performing additional processing");
    // Need to do some additional processing based on the
    // contents of the file here
    
    Console.WriteLine($"End of '{nameof(GetWebsiteInformation)}' method");
}

There are a few moving parts here:

  1. Declare an instance of TaskCompletionSource<T> where T is the type to be returned
  2. When the callback method is invoked (once the file is created), the SetResult method of TaskCompletionSource is called with the information to be passed through the TaskCompletionSource Task (the string filename in this example)
  3. When required to wait for the file to be created, the TaskCompletionSource.Task is awaited. This task will be considered completed once the SetResult method is called in the callback

Executing the above method now has the following output:

Start of 'GetWebsiteInformation' method
Waiting for a file to be created
File 'alwaysdeveloping.txt' processed successfully
File created! Performing additional processing
End of 'GetWebsiteInformation' method

From this we can see, the output is exactly what we want. A request for the file to be created is initiated - the code then waits for the file to be created (the callback method being invoked), before processing continues.


Other Set methods

Not demonstrated in this post, but there are two other Set methods available on the _TaskCompletionSource:

  • SetException: transitions the underlying Task into a Failed state.
  • SetCanceled: transitions the underlying Task into a Canceled state.

Notes

An incredibly valuable class to be aware of when one needs to wait on an external asynchronous operation (such as a callback event), while also being able to convey information out of the external operation to the scope awaiting the call.


References

Async Web API testing with TaskCompletionSource (Microservices with .NET 6.0) - FeedR episode #11

Daily Drop 164: 20-09-2022

At the start of 2022 I set myself the goal of learning one new coding related piece of knowledge a day.
It could be anything - some.NET / C# functionality I wasn't aware of, a design practice, a cool new coding technique, or just something I find interesting. It could be something I knew at one point but had forgotten, or something completely new, which I may or may never actually use.

The Daily Drop is a record of these pieces of knowledge - writing about and summarizing them helps re-enforce the information for myself, as well as potentially helps others learn something new as well.
c# .net task taskcompletionsource handler async