Asynchronous Lambdas and Local Functions

Just as a lambda expression converted to a delegate can be used as a concise syntax for declaring a normal method, lambdas containing await expressions can be converted to delegates.14 To do so, just precede the lambda expression with the async keyword. In Listing 20.10, we first assign an async lambda to a Func<string, Task> writeWebRequestSizeAsync variable. We then use the await operator to invoke it.

Listing 20.10: An Asynchronous Client-Server Interaction as a Lambda Expression
using System;
using System.IO;
using System.Net;
using System.Linq;
using System.Threading.Tasks;
 
public class Program
{
 
    public static void Main(string[] args)
    {
        string url = "http://www.IntelliTect.com";
        if(args.Length > 0)
        {
            url = args[0];
        }
 
        Console.Write(url);
 
        Func<string, Task> writeWebRequestSizeAsync =
            async (string webRequestUrl) =>
            {
                // Error handling omitted for 
                // elucidation
                WebRequest webRequest =
                   WebRequest.Create(url);
 
                WebResponse response =
                    await webRequest.GetResponseAsync();
 
                // Explicitly counting rather than invoking
                // webRequest.ContentLength to demonstrate
                //  multiple await operators
                using (StreamReader reader =
                    new(response.GetResponseStream()))
                {
                    string text =
                        (await reader.ReadToEndAsync());
                    Console.WriteLine(
                        FormatBytes(text.Length));
                }
            };
 
        Task task = writeWebRequestSizeAsync(url);
 
        while (!task.Wait(100))
        {
            Console.Write(".");
        }
    }
    // ...
}

Similarly, the same effect can be achieved with a local function.15 For example, in Listing 20.10, you could change the lambda expression header (everything up to and including the => operator) to

async Task WriteWebRequestSizeAsync(string webRequestUrl)

leaving everything in the body, including the curly braces, unchanged.

Note that an async lambda expression has the same restrictions as the named async method:

An async lambda expression must be converted to a delegate whose return type is a valid async return type.
The lambda is rewritten so that return statements become signals that the task returned by the lambda has completed with the given result.
Execution within the lambda expression occurs synchronously until the first await on an incomplete awaitable is executed.
All instructions following the await will execute as continuations on the return from the invoked asynchronous method (or, if the awaitable is already complete, simply will be executed synchronously rather than as continuations).
An async lambda expression can be invoked with an await operator (not shown in Listing 20.10).
AdVanced/Beginner Topic
Implementing a Custom Asynchronous Method

Implementing an asynchronous method by relying on other asynchronous methods (which, in turn, rely on more asynchronous methods) is relatively easy with the await keyword. However, at some point in the call hierarchy, it becomes necessary to write a “leaf” asynchronous Task-returning method. Consider, for example, an asynchronous method for running a command-line program with the eventual goal that the output could be accessed. Such a method would be declared as follows:

public static Task<Process> RunProcessAsync(string filename)

The simplest implementation would, of course, be to rely on Task.Run() again and call both the System.Diagnostics.Process’s Start() and WaitForExit() methods. However, creating an additional thread in the current process is unnecessary when the invoked process itself will have its own collection of one or more threads. To implement the RunProcessAsync() method and return to the caller’s synchronization context when the invoked process completes, we can rely on a TaskCompletionSource<T> object, as shown in Listing 20.11.

Listing 20.11: Implementing a Custom Asynchronous Method
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
 
public class Program
{
    public static Task<Process> RunProcessAsync(
        string fileName,
        string arguments = "",
        CancellationToken cancellationToken = default)
    {
        TaskCompletionSource<Process> taskCS =
                      new();
 
        Process process = new()
        {
            StartInfo = new ProcessStartInfo(fileName)
            {
                UseShellExecute = false,
                Arguments = arguments
            },
            EnableRaisingEvents = true
        };
 
        process.Exited += (sender, localEventArgs) =>
        {
            taskCS.SetResult(process);
        };
 
        cancellationToken
            .ThrowIfCancellationRequested();
 
        process.Start();
 
        cancellationToken.Register(() =>
        {
            Process.GetProcessById(process.Id).Kill();
        });
 
        return taskCS.Task;
    }
    // ...
}

Ignore the highlighting for the moment and instead focus on the pattern of using an event for notification when the process completes. Since System.Diagnostics.Process includes a notification upon exit, we register for this notification and use it as a callback from which we can invoke TaskCompletionSource.SetResult(). The code in Listing 20.11 follows a fairly common pattern that you can use to create an asynchronous method without having to resort to Task.Run().

Another important characteristic that an async method might require is cancellation. TAP relies on the same methods for cancellation as the TPL does—namely, a System.Threading.CancellationToken. Listing 20.11 highlights the code necessary to support cancellation. In this example, we allow for canceling before the process ever starts, as well as an attempt to close the application’s main window (if there is one). A more aggressive approach would be to call Process.Kill(), but this method could potentially cause problems for the program that is executing.

Notice that we don’t register for the cancellation event until after the process is started. This avoids any race conditions that might occur if cancellation is triggered before the process actually begins.

One last feature to consider supporting is a progress update. Listing 20.12 is the full version of RunProcessAsync() with just such an update.

Listing 20.12: Implementing a Custom Asynchronous Method with Progress Support
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
 
public class Program
{
    public static Task<Process> RunProcessAsync(
        string fileName,
        string arguments = "",
        IProgress<ProcessProgressEventArgs>? progress = 
            nullobject? objectState = null,
        CancellationToken cancellationToken =
            default(CancellationToken))
    {
        TaskCompletionSource<Process> taskCS = new();
 
        Process process = new()
        {
            StartInfo = new ProcessStartInfo(fileName)
            {
                UseShellExecute = false,
                Arguments = arguments,
                RedirectStandardOutput =
                   progress is not null
            },
            EnableRaisingEvents = true
        };
 
        process.Exited += (sender, localEventArgs) =>
        {
            taskCS.SetResult(process);
        };
 
        if (progress is not null)
        {
            process.OutputDataReceived +=
                (sender, localEventArgs) =>
            {
                progress.Report(
                    new ProcessProgressEventArgs(
                        localEventArgs.Data,
                        objectState));
            };
        }
 
        cancellationToken
            .ThrowIfCancellationRequested();
 
        process.Start();
 
        if (progress !=null)
        {
            process.BeginOutputReadLine();
        }
 
        cancellationToken.Register(() =>
        {
            Process.GetProcessById(process.Id).Kill();
        });
 
        return taskCS.Task;
    }
 
    // ...
}
 
public class ProcessProgressEventArgs
{
    // ...
}

Wrapping your head around precisely what is happening in an async method can be difficult, but it is far less difficult than trying to figure out what asynchronous code written with explicit continuations in lambdas is doing. The key points to remember are as follows:

When control reaches an await keyword, the expression that follows it produces a task.16 Control then returns to the caller so that it can continue to do work while the task completes asynchronously (assuming it hadn’t already completed).
Sometime after the task completes, control resumes at the point following the await. If the awaited task produces a result, that result is then obtained. If it faulted, the exception is thrown.
A return statement in an async method causes the task associated with the method invocation to become completed; if the return statement has a value, the value returned becomes the result of the task.

________________________________________

14. Starting in C# 5.0.
15. Starting in C# 7.0.
16. Technically, it is an awaitable type, as described in the “Advanced Topic: Valid Async Return Types Expanded.”
{{ snackbarMessage }}
;